Skip to main content

gpui/text_system/
line_wrapper.rs

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