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