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 => {
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}