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