1use crate::{FontId, FontRun, Pixels, PlatformTextSystem, SharedString, TextRun, px};
2use collections::HashMap;
3use std::{borrow::Cow, iter, sync::Arc};
4
5/// Determines whether to truncate text from the start or end.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
7pub enum TruncateFrom {
8 /// Truncate text from the start.
9 Start,
10 /// Truncate text from the end.
11 End,
12}
13
14/// The GPUI line wrapper, used to wrap lines of text to a given width.
15pub struct LineWrapper {
16 platform_text_system: Arc<dyn PlatformTextSystem>,
17 pub(crate) font_id: FontId,
18 pub(crate) font_size: Pixels,
19 cached_ascii_char_widths: [Option<Pixels>; 128],
20 cached_other_char_widths: HashMap<char, Pixels>,
21}
22
23impl LineWrapper {
24 /// The maximum indent that can be applied to a line.
25 pub const MAX_INDENT: u32 = 256;
26
27 pub(crate) fn new(
28 font_id: FontId,
29 font_size: Pixels,
30 text_system: Arc<dyn PlatformTextSystem>,
31 ) -> Self {
32 Self {
33 platform_text_system: text_system,
34 font_id,
35 font_size,
36 cached_ascii_char_widths: [None; 128],
37 cached_other_char_widths: HashMap::default(),
38 }
39 }
40
41 /// Wrap a line of text to the given width with this wrapper's font and font size.
42 pub fn wrap_line<'a>(
43 &'a mut self,
44 fragments: &'a [LineFragment],
45 wrap_width: Pixels,
46 ) -> impl Iterator<Item = Boundary> + 'a {
47 let mut width = px(0.);
48 let mut first_non_whitespace_ix = None;
49 let mut indent = None;
50 let mut last_candidate_ix = 0;
51 let mut last_candidate_width = px(0.);
52 let mut last_wrap_ix = 0;
53 let mut prev_c = '\0';
54 let mut index = 0;
55 let mut candidates = fragments
56 .iter()
57 .flat_map(move |fragment| fragment.wrap_boundary_candidates())
58 .peekable();
59 iter::from_fn(move || {
60 for candidate in candidates.by_ref() {
61 let ix = index;
62 index += candidate.len_utf8();
63 let mut new_prev_c = prev_c;
64 let item_width = match candidate {
65 WrapBoundaryCandidate::Char { character: c } => {
66 if c == '\n' {
67 continue;
68 }
69
70 if Self::is_word_char(c) {
71 if prev_c == ' ' && c != ' ' && first_non_whitespace_ix.is_some() {
72 last_candidate_ix = ix;
73 last_candidate_width = width;
74 }
75 } else {
76 // CJK may not be space separated, e.g.: `Hello world你好世界`
77 if c != ' ' && first_non_whitespace_ix.is_some() {
78 last_candidate_ix = ix;
79 last_candidate_width = width;
80 }
81 }
82
83 if c != ' ' && first_non_whitespace_ix.is_none() {
84 first_non_whitespace_ix = Some(ix);
85 }
86
87 new_prev_c = c;
88
89 self.width_for_char(c)
90 }
91 WrapBoundaryCandidate::Element {
92 width: element_width,
93 ..
94 } => {
95 if prev_c == ' ' && first_non_whitespace_ix.is_some() {
96 last_candidate_ix = ix;
97 last_candidate_width = width;
98 }
99
100 if first_non_whitespace_ix.is_none() {
101 first_non_whitespace_ix = Some(ix);
102 }
103
104 element_width
105 }
106 };
107
108 width += item_width;
109 if width > wrap_width && ix > last_wrap_ix {
110 if let (None, Some(first_non_whitespace_ix)) = (indent, first_non_whitespace_ix)
111 {
112 indent = Some(
113 Self::MAX_INDENT.min((first_non_whitespace_ix - last_wrap_ix) as u32),
114 );
115 }
116
117 if last_candidate_ix > 0 {
118 last_wrap_ix = last_candidate_ix;
119 width -= last_candidate_width;
120 last_candidate_ix = 0;
121 } else {
122 last_wrap_ix = ix;
123 width = item_width;
124 }
125
126 if let Some(indent) = indent {
127 width += self.width_for_char(' ') * indent as f32;
128 }
129
130 return Some(Boundary::new(last_wrap_ix, indent.unwrap_or(0)));
131 }
132
133 prev_c = new_prev_c;
134 }
135
136 None
137 })
138 }
139
140 /// Determines if a line should be truncated based on its width.
141 ///
142 /// Returns the truncation index in `line`.
143 pub fn should_truncate_line(
144 &mut self,
145 line: &str,
146 truncate_width: Pixels,
147 truncation_affix: &str,
148 truncate_from: TruncateFrom,
149 ) -> Option<usize> {
150 let mut width = px(0.);
151 let suffix_width = truncation_affix
152 .chars()
153 .map(|c| self.width_for_char(c))
154 .fold(px(0.0), |a, x| a + x);
155 let mut truncate_ix = 0;
156
157 match truncate_from {
158 TruncateFrom::Start => {
159 for (ix, c) in line.char_indices().rev() {
160 if width + suffix_width < truncate_width {
161 truncate_ix = ix;
162 }
163
164 let char_width = self.width_for_char(c);
165 width += char_width;
166
167 if width.floor() > truncate_width {
168 return Some(truncate_ix);
169 }
170 }
171 }
172 TruncateFrom::End => {
173 for (ix, c) in line.char_indices() {
174 if width + suffix_width < truncate_width {
175 truncate_ix = ix;
176 }
177
178 let char_width = self.width_for_char(c);
179 width += char_width;
180
181 if width.floor() > truncate_width {
182 return Some(truncate_ix);
183 }
184 }
185 }
186 }
187
188 None
189 }
190
191 /// Truncate a line of text to the given width with this wrapper's font and font size.
192 pub fn truncate_line<'a>(
193 &mut self,
194 line: SharedString,
195 truncate_width: Pixels,
196 truncation_affix: &str,
197 runs: &'a [TextRun],
198 truncate_from: TruncateFrom,
199 ) -> (SharedString, Cow<'a, [TextRun]>) {
200 if let Some(truncate_ix) =
201 self.should_truncate_line(&line, truncate_width, truncation_affix, truncate_from)
202 {
203 let result = match truncate_from {
204 TruncateFrom::Start => {
205 SharedString::from(format!("{truncation_affix}{}", &line[truncate_ix + 1..]))
206 }
207 TruncateFrom::End => {
208 SharedString::from(format!("{}{truncation_affix}", &line[..truncate_ix]))
209 }
210 };
211 let mut runs = runs.to_vec();
212 update_runs_after_truncation(&result, truncation_affix, &mut runs, truncate_from);
213 (result, Cow::Owned(runs))
214 } else {
215 (line, Cow::Borrowed(runs))
216 }
217 }
218
219 /// Any character in this list should be treated as a word character,
220 /// meaning it can be part of a word that should not be wrapped.
221 pub(crate) fn is_word_char(c: char) -> bool {
222 // ASCII alphanumeric characters, for English, numbers: `Hello123`, etc.
223 c.is_ascii_alphanumeric() ||
224 // Latin script in Unicode for French, German, Spanish, etc.
225 // Latin-1 Supplement
226 // https://en.wikipedia.org/wiki/Latin-1_Supplement
227 matches!(c, '\u{00C0}'..='\u{00FF}') ||
228 // Latin Extended-A
229 // https://en.wikipedia.org/wiki/Latin_Extended-A
230 matches!(c, '\u{0100}'..='\u{017F}') ||
231 // Latin Extended-B
232 // https://en.wikipedia.org/wiki/Latin_Extended-B
233 matches!(c, '\u{0180}'..='\u{024F}') ||
234 // Cyrillic for Russian, Ukrainian, etc.
235 // https://en.wikipedia.org/wiki/Cyrillic_script_in_Unicode
236 matches!(c, '\u{0400}'..='\u{04FF}') ||
237
238 // Vietnamese (https://vietunicode.sourceforge.net/charset/)
239 matches!(c, '\u{1E00}'..='\u{1EFF}') || // Latin Extended Additional
240 matches!(c, '\u{0300}'..='\u{036F}') || // Combining Diacritical Marks
241
242 // Some other known special characters that should be treated as word characters,
243 // e.g. `a-b`, `var_name`, `I'm`, '@mention`, `#hashtag`, `100%`, `3.1415`,
244 // `2^3`, `a~b`, `a=1`, `Self::new`, etc.
245 matches!(c, '-' | '_' | '.' | '\'' | '$' | '%' | '@' | '#' | '^' | '~' | ',' | '=' | ':') ||
246 // `⋯` character is special used in Zed, to keep this at the end of the line.
247 matches!(c, '⋯')
248 }
249
250 #[inline(always)]
251 fn width_for_char(&mut self, c: char) -> Pixels {
252 if (c as u32) < 128 {
253 if let Some(cached_width) = self.cached_ascii_char_widths[c as usize] {
254 cached_width
255 } else {
256 let width = self.compute_width_for_char(c);
257 self.cached_ascii_char_widths[c as usize] = Some(width);
258 width
259 }
260 } else if let Some(cached_width) = self.cached_other_char_widths.get(&c) {
261 *cached_width
262 } else {
263 let width = self.compute_width_for_char(c);
264 self.cached_other_char_widths.insert(c, width);
265 width
266 }
267 }
268
269 fn compute_width_for_char(&self, c: char) -> Pixels {
270 let mut buffer = [0; 4];
271 let buffer = c.encode_utf8(&mut buffer);
272 self.platform_text_system
273 .layout_line(
274 buffer,
275 self.font_size,
276 &[FontRun {
277 len: buffer.len(),
278 font_id: self.font_id,
279 }],
280 )
281 .width
282 }
283}
284
285fn update_runs_after_truncation(
286 result: &str,
287 ellipsis: &str,
288 runs: &mut Vec<TextRun>,
289 truncate_from: TruncateFrom,
290) {
291 let mut truncate_at = result.len() - ellipsis.len();
292 match truncate_from {
293 TruncateFrom::Start => {
294 for (run_index, run) in runs.iter_mut().enumerate().rev() {
295 if run.len <= truncate_at {
296 truncate_at -= run.len;
297 } else {
298 run.len = truncate_at + ellipsis.len();
299 runs.splice(..run_index, std::iter::empty());
300 break;
301 }
302 }
303 }
304 TruncateFrom::End => {
305 for (run_index, run) in runs.iter_mut().enumerate() {
306 if run.len <= truncate_at {
307 truncate_at -= run.len;
308 } else {
309 run.len = truncate_at + ellipsis.len();
310 runs.truncate(run_index + 1);
311 break;
312 }
313 }
314 }
315 }
316}
317
318/// A fragment of a line that can be wrapped.
319pub enum LineFragment<'a> {
320 /// A text fragment consisting of characters.
321 Text {
322 /// The text content of the fragment.
323 text: &'a str,
324 },
325 /// A non-text element with a fixed width.
326 Element {
327 /// The width of the element in pixels.
328 width: Pixels,
329 /// The UTF-8 encoded length of the element.
330 len_utf8: usize,
331 },
332}
333
334impl<'a> LineFragment<'a> {
335 /// Creates a new text fragment from the given text.
336 pub fn text(text: &'a str) -> Self {
337 LineFragment::Text { text }
338 }
339
340 /// Creates a new non-text element with the given width and UTF-8 encoded length.
341 pub fn element(width: Pixels, len_utf8: usize) -> Self {
342 LineFragment::Element { width, len_utf8 }
343 }
344
345 fn wrap_boundary_candidates(&self) -> impl Iterator<Item = WrapBoundaryCandidate> {
346 let text = match self {
347 LineFragment::Text { text } => text,
348 LineFragment::Element { .. } => "\0",
349 };
350 text.chars().map(move |character| {
351 if let LineFragment::Element { width, len_utf8 } = self {
352 WrapBoundaryCandidate::Element {
353 width: *width,
354 len_utf8: *len_utf8,
355 }
356 } else {
357 WrapBoundaryCandidate::Char { character }
358 }
359 })
360 }
361}
362
363enum WrapBoundaryCandidate {
364 Char { character: char },
365 Element { width: Pixels, len_utf8: usize },
366}
367
368impl WrapBoundaryCandidate {
369 pub fn len_utf8(&self) -> usize {
370 match self {
371 WrapBoundaryCandidate::Char { character } => character.len_utf8(),
372 WrapBoundaryCandidate::Element { len_utf8: len, .. } => *len,
373 }
374 }
375}
376
377/// A boundary between two lines of text.
378#[derive(Copy, Clone, Debug, PartialEq, Eq)]
379pub struct Boundary {
380 /// The index of the last character in a line
381 pub ix: usize,
382 /// The indent of the next line.
383 pub next_indent: u32,
384}
385
386impl Boundary {
387 fn new(ix: usize, next_indent: u32) -> Self {
388 Self { ix, next_indent }
389 }
390}
391
392#[cfg(test)]
393mod tests {
394 use super::*;
395 use crate::{Font, FontFeatures, FontStyle, FontWeight, TestAppContext, TestDispatcher, font};
396 #[cfg(target_os = "macos")]
397 use crate::{TextRun, WindowTextSystem, WrapBoundary};
398 use rand::prelude::*;
399
400 fn build_wrapper() -> LineWrapper {
401 let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(0));
402 let cx = TestAppContext::build(dispatcher, None);
403 let id = cx.text_system().resolve_font(&font(".ZedMono"));
404 LineWrapper::new(id, px(16.), cx.text_system().platform_text_system.clone())
405 }
406
407 fn generate_test_runs(input_run_len: &[usize]) -> Vec<TextRun> {
408 input_run_len
409 .iter()
410 .map(|run_len| TextRun {
411 len: *run_len,
412 font: Font {
413 family: "Dummy".into(),
414 features: FontFeatures::default(),
415 fallbacks: None,
416 weight: FontWeight::default(),
417 style: FontStyle::Normal,
418 },
419 ..Default::default()
420 })
421 .collect()
422 }
423
424 #[test]
425 fn test_wrap_line() {
426 let mut wrapper = build_wrapper();
427
428 assert_eq!(
429 wrapper
430 .wrap_line(&[LineFragment::text("aa bbb cccc ddddd eeee")], px(72.))
431 .collect::<Vec<_>>(),
432 &[
433 Boundary::new(7, 0),
434 Boundary::new(12, 0),
435 Boundary::new(18, 0)
436 ],
437 );
438 assert_eq!(
439 wrapper
440 .wrap_line(&[LineFragment::text("aaa aaaaaaaaaaaaaaaaaa")], px(72.0))
441 .collect::<Vec<_>>(),
442 &[
443 Boundary::new(4, 0),
444 Boundary::new(11, 0),
445 Boundary::new(18, 0)
446 ],
447 );
448 assert_eq!(
449 wrapper
450 .wrap_line(&[LineFragment::text(" aaaaaaa")], px(72.))
451 .collect::<Vec<_>>(),
452 &[
453 Boundary::new(7, 5),
454 Boundary::new(9, 5),
455 Boundary::new(11, 5),
456 ]
457 );
458 assert_eq!(
459 wrapper
460 .wrap_line(
461 &[LineFragment::text(" ")],
462 px(72.)
463 )
464 .collect::<Vec<_>>(),
465 &[
466 Boundary::new(7, 0),
467 Boundary::new(14, 0),
468 Boundary::new(21, 0)
469 ]
470 );
471 assert_eq!(
472 wrapper
473 .wrap_line(&[LineFragment::text(" aaaaaaaaaaaaaa")], px(72.))
474 .collect::<Vec<_>>(),
475 &[
476 Boundary::new(7, 0),
477 Boundary::new(14, 3),
478 Boundary::new(18, 3),
479 Boundary::new(22, 3),
480 ]
481 );
482
483 // Test wrapping multiple text fragments
484 assert_eq!(
485 wrapper
486 .wrap_line(
487 &[
488 LineFragment::text("aa bbb "),
489 LineFragment::text("cccc ddddd eeee")
490 ],
491 px(72.)
492 )
493 .collect::<Vec<_>>(),
494 &[
495 Boundary::new(7, 0),
496 Boundary::new(12, 0),
497 Boundary::new(18, 0)
498 ],
499 );
500
501 // Test wrapping with a mix of text and element fragments
502 assert_eq!(
503 wrapper
504 .wrap_line(
505 &[
506 LineFragment::text("aa "),
507 LineFragment::element(px(20.), 1),
508 LineFragment::text(" bbb "),
509 LineFragment::element(px(30.), 1),
510 LineFragment::text(" cccc")
511 ],
512 px(72.)
513 )
514 .collect::<Vec<_>>(),
515 &[
516 Boundary::new(5, 0),
517 Boundary::new(9, 0),
518 Boundary::new(11, 0)
519 ],
520 );
521
522 // Test with element at the beginning and text afterward
523 assert_eq!(
524 wrapper
525 .wrap_line(
526 &[
527 LineFragment::element(px(50.), 1),
528 LineFragment::text(" aaaa bbbb cccc dddd")
529 ],
530 px(72.)
531 )
532 .collect::<Vec<_>>(),
533 &[
534 Boundary::new(2, 0),
535 Boundary::new(7, 0),
536 Boundary::new(12, 0),
537 Boundary::new(17, 0)
538 ],
539 );
540
541 // Test with a large element that forces wrapping by itself
542 assert_eq!(
543 wrapper
544 .wrap_line(
545 &[
546 LineFragment::text("short text "),
547 LineFragment::element(px(100.), 1),
548 LineFragment::text(" more text")
549 ],
550 px(72.)
551 )
552 .collect::<Vec<_>>(),
553 &[
554 Boundary::new(6, 0),
555 Boundary::new(11, 0),
556 Boundary::new(12, 0),
557 Boundary::new(18, 0)
558 ],
559 );
560 }
561
562 #[test]
563 fn test_truncate_line_end() {
564 let mut wrapper = build_wrapper();
565
566 fn perform_test(
567 wrapper: &mut LineWrapper,
568 text: &'static str,
569 expected: &'static str,
570 ellipsis: &str,
571 ) {
572 let dummy_run_lens = vec![text.len()];
573 let dummy_runs = generate_test_runs(&dummy_run_lens);
574 let (result, dummy_runs) = wrapper.truncate_line(
575 text.into(),
576 px(220.),
577 ellipsis,
578 &dummy_runs,
579 TruncateFrom::End,
580 );
581 assert_eq!(result, expected);
582 assert_eq!(dummy_runs.first().unwrap().len, result.len());
583 }
584
585 perform_test(
586 &mut wrapper,
587 "aa bbb cccc ddddd eeee ffff gggg",
588 "aa bbb cccc ddddd eeee",
589 "",
590 );
591 perform_test(
592 &mut wrapper,
593 "aa bbb cccc ddddd eeee ffff gggg",
594 "aa bbb cccc ddddd eee…",
595 "…",
596 );
597 perform_test(
598 &mut wrapper,
599 "aa bbb cccc ddddd eeee ffff gggg",
600 "aa bbb cccc dddd......",
601 "......",
602 );
603 }
604
605 #[test]
606 fn test_truncate_line_start() {
607 let mut wrapper = build_wrapper();
608
609 fn perform_test(
610 wrapper: &mut LineWrapper,
611 text: &'static str,
612 expected: &'static str,
613 ellipsis: &str,
614 ) {
615 let dummy_run_lens = vec![text.len()];
616 let dummy_runs = generate_test_runs(&dummy_run_lens);
617 let (result, dummy_runs) = wrapper.truncate_line(
618 text.into(),
619 px(220.),
620 ellipsis,
621 &dummy_runs,
622 TruncateFrom::Start,
623 );
624 assert_eq!(result, expected);
625 assert_eq!(dummy_runs.first().unwrap().len, result.len());
626 }
627
628 perform_test(
629 &mut wrapper,
630 "aaaa bbbb cccc ddddd eeee fff gg",
631 "cccc ddddd eeee fff gg",
632 "",
633 );
634 perform_test(
635 &mut wrapper,
636 "aaaa bbbb cccc ddddd eeee fff gg",
637 "…ccc ddddd eeee fff gg",
638 "…",
639 );
640 perform_test(
641 &mut wrapper,
642 "aaaa bbbb cccc ddddd eeee fff gg",
643 "......dddd eeee fff gg",
644 "......",
645 );
646 }
647
648 #[test]
649 fn test_truncate_multiple_runs_end() {
650 let mut wrapper = build_wrapper();
651
652 fn perform_test(
653 wrapper: &mut LineWrapper,
654 text: &'static str,
655 expected: &str,
656 run_lens: &[usize],
657 result_run_len: &[usize],
658 line_width: Pixels,
659 ) {
660 let dummy_runs = generate_test_runs(run_lens);
661 let (result, dummy_runs) =
662 wrapper.truncate_line(text.into(), line_width, "…", &dummy_runs, TruncateFrom::End);
663 assert_eq!(result, expected);
664 for (run, result_len) in dummy_runs.iter().zip(result_run_len) {
665 assert_eq!(run.len, *result_len);
666 }
667 }
668 // Case 0: Normal
669 // Text: abcdefghijkl
670 // Runs: Run0 { len: 12, ... }
671 //
672 // Truncate res: abcd… (truncate_at = 4)
673 // Run res: Run0 { string: abcd…, len: 7, ... }
674 perform_test(&mut wrapper, "abcdefghijkl", "abcd…", &[12], &[7], px(50.));
675 // Case 1: Drop some runs
676 // Text: abcdefghijkl
677 // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
678 //
679 // Truncate res: abcdef… (truncate_at = 6)
680 // Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: ef…, len:
681 // 5, ... }
682 perform_test(
683 &mut wrapper,
684 "abcdefghijkl",
685 "abcdef…",
686 &[4, 4, 4],
687 &[4, 5],
688 px(70.),
689 );
690 // Case 2: Truncate at start of some run
691 // Text: abcdefghijkl
692 // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
693 //
694 // Truncate res: abcdefgh… (truncate_at = 8)
695 // Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: efgh, len:
696 // 4, ... }, Run2 { string: …, len: 3, ... }
697 perform_test(
698 &mut wrapper,
699 "abcdefghijkl",
700 "abcdefgh…",
701 &[4, 4, 4],
702 &[4, 4, 3],
703 px(90.),
704 );
705 }
706
707 #[test]
708 fn test_truncate_multiple_runs_start() {
709 let mut wrapper = build_wrapper();
710
711 #[track_caller]
712 fn perform_test(
713 wrapper: &mut LineWrapper,
714 text: &'static str,
715 expected: &str,
716 run_lens: &[usize],
717 result_run_len: &[usize],
718 line_width: Pixels,
719 ) {
720 let dummy_runs = generate_test_runs(run_lens);
721 let (result, dummy_runs) = wrapper.truncate_line(
722 text.into(),
723 line_width,
724 "…",
725 &dummy_runs,
726 TruncateFrom::Start,
727 );
728 assert_eq!(result, expected);
729 for (run, result_len) in dummy_runs.iter().zip(result_run_len) {
730 assert_eq!(run.len, *result_len);
731 }
732 }
733 // Case 0: Normal
734 // Text: abcdefghijkl
735 // Runs: Run0 { len: 12, ... }
736 //
737 // Truncate res: …ijkl (truncate_at = 9)
738 // Run res: Run0 { string: …ijkl, len: 7, ... }
739 perform_test(&mut wrapper, "abcdefghijkl", "…ijkl", &[12], &[7], px(50.));
740 // Case 1: Drop some runs
741 // Text: abcdefghijkl
742 // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
743 //
744 // Truncate res: …ghijkl (truncate_at = 7)
745 // Runs res: Run0 { string: …gh, len: 5, ... }, Run1 { string: ijkl, len:
746 // 4, ... }
747 perform_test(
748 &mut wrapper,
749 "abcdefghijkl",
750 "…ghijkl",
751 &[4, 4, 4],
752 &[5, 4],
753 px(70.),
754 );
755 // Case 2: Truncate at start of some run
756 // Text: abcdefghijkl
757 // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
758 //
759 // Truncate res: abcdefgh… (truncate_at = 3)
760 // Runs res: Run0 { string: …, len: 3, ... }, Run1 { string: efgh, len:
761 // 4, ... }, Run2 { string: ijkl, len: 4, ... }
762 perform_test(
763 &mut wrapper,
764 "abcdefghijkl",
765 "…efghijkl",
766 &[4, 4, 4],
767 &[3, 4, 4],
768 px(90.),
769 );
770 }
771
772 #[test]
773 fn test_update_run_after_truncation_end() {
774 fn perform_test(result: &str, run_lens: &[usize], result_run_lens: &[usize]) {
775 let mut dummy_runs = generate_test_runs(run_lens);
776 update_runs_after_truncation(result, "…", &mut dummy_runs, TruncateFrom::End);
777 for (run, result_len) in dummy_runs.iter().zip(result_run_lens) {
778 assert_eq!(run.len, *result_len);
779 }
780 }
781 // Case 0: Normal
782 // Text: abcdefghijkl
783 // Runs: Run0 { len: 12, ... }
784 //
785 // Truncate res: abcd… (truncate_at = 4)
786 // Run res: Run0 { string: abcd…, len: 7, ... }
787 perform_test("abcd…", &[12], &[7]);
788 // Case 1: Drop some runs
789 // Text: abcdefghijkl
790 // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
791 //
792 // Truncate res: abcdef… (truncate_at = 6)
793 // Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: ef…, len:
794 // 5, ... }
795 perform_test("abcdef…", &[4, 4, 4], &[4, 5]);
796 // Case 2: Truncate at start of some run
797 // Text: abcdefghijkl
798 // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
799 //
800 // Truncate res: abcdefgh… (truncate_at = 8)
801 // Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: efgh, len:
802 // 4, ... }, Run2 { string: …, len: 3, ... }
803 perform_test("abcdefgh…", &[4, 4, 4], &[4, 4, 3]);
804 }
805
806 #[test]
807 fn test_is_word_char() {
808 #[track_caller]
809 fn assert_word(word: &str) {
810 for c in word.chars() {
811 assert!(
812 LineWrapper::is_word_char(c),
813 "assertion failed for '{}' (unicode 0x{:x})",
814 c,
815 c as u32
816 );
817 }
818 }
819
820 #[track_caller]
821 fn assert_not_word(word: &str) {
822 let found = word.chars().any(|c| !LineWrapper::is_word_char(c));
823 assert!(found, "assertion failed for '{}'", word);
824 }
825
826 assert_word("Hello123");
827 assert_word("non-English");
828 assert_word("var_name");
829 assert_word("123456");
830 assert_word("3.1415");
831 assert_word("10^2");
832 assert_word("1~2");
833 assert_word("100%");
834 assert_word("@mention");
835 assert_word("#hashtag");
836 assert_word("$variable");
837 assert_word("a=1");
838 assert_word("Self::is_word_char");
839 assert_word("more⋯");
840
841 // Space
842 assert_not_word("foo bar");
843
844 // URL case
845 assert_word("github.com");
846 assert_not_word("zed-industries/zed");
847 assert_not_word("zed-industries\\zed");
848 assert_not_word("a=1&b=2");
849 assert_not_word("foo?b=2");
850
851 // Latin-1 Supplement
852 assert_word("ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏ");
853 // Latin Extended-A
854 assert_word("ĀāĂ㥹ĆćĈĉĊċČčĎď");
855 // Latin Extended-B
856 assert_word("ƀƁƂƃƄƅƆƇƈƉƊƋƌƍƎƏ");
857 // Cyrillic
858 assert_word("АБВГДЕЖЗИЙКЛМНОП");
859 // Vietnamese (https://github.com/zed-industries/zed/issues/23245)
860 assert_word("ThậmchíđếnkhithuachạychúngcònnhẫntâmgiếtnốtsốđôngtùchínhtrịởYênBáivàCaoBằng");
861
862 // non-word characters
863 assert_not_word("你好");
864 assert_not_word("안녕하세요");
865 assert_not_word("こんにちは");
866 assert_not_word("😀😁😂");
867 assert_not_word("()[]{}<>");
868 }
869
870 // For compatibility with the test macro
871 #[cfg(target_os = "macos")]
872 use crate as gpui;
873
874 // These seem to vary wildly based on the text system.
875 #[cfg(target_os = "macos")]
876 #[crate::test]
877 fn test_wrap_shaped_line(cx: &mut TestAppContext) {
878 cx.update(|cx| {
879 let text_system = WindowTextSystem::new(cx.text_system().clone());
880
881 let normal = TextRun {
882 len: 0,
883 font: font("Helvetica"),
884 color: Default::default(),
885 underline: Default::default(),
886 ..Default::default()
887 };
888 let bold = TextRun {
889 len: 0,
890 font: font("Helvetica").bold(),
891 ..Default::default()
892 };
893
894 let text = "aa bbb cccc ddddd eeee".into();
895 let lines = text_system
896 .shape_text(
897 text,
898 px(16.),
899 &[
900 normal.with_len(4),
901 bold.with_len(5),
902 normal.with_len(6),
903 bold.with_len(1),
904 normal.with_len(7),
905 ],
906 Some(px(72.)),
907 None,
908 )
909 .unwrap();
910
911 assert_eq!(
912 lines[0].layout.wrap_boundaries(),
913 &[
914 WrapBoundary {
915 run_ix: 0,
916 glyph_ix: 7
917 },
918 WrapBoundary {
919 run_ix: 0,
920 glyph_ix: 12
921 },
922 WrapBoundary {
923 run_ix: 0,
924 glyph_ix: 18
925 }
926 ],
927 );
928 });
929 }
930}