line_wrapper.rs

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