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
400    fn build_wrapper() -> LineWrapper {
401        let dispatcher = TestDispatcher::new(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        perform_test(
604            &mut wrapper,
605            "aa bbb cccc 🦀🦀🦀🦀🦀 eeee ffff gggg",
606            "aa bbb cccc 🦀🦀🦀🦀…",
607            "",
608        );
609    }
610
611    #[test]
612    fn test_truncate_line_start() {
613        let mut wrapper = build_wrapper();
614
615        #[track_caller]
616        fn perform_test(
617            wrapper: &mut LineWrapper,
618            text: &'static str,
619            expected: &'static str,
620            ellipsis: &str,
621        ) {
622            let dummy_run_lens = vec![text.len()];
623            let dummy_runs = generate_test_runs(&dummy_run_lens);
624            let (result, dummy_runs) = wrapper.truncate_line(
625                text.into(),
626                px(220.),
627                ellipsis,
628                &dummy_runs,
629                TruncateFrom::Start,
630            );
631            assert_eq!(result, expected);
632            assert_eq!(dummy_runs.first().unwrap().len, result.len());
633        }
634
635        perform_test(
636            &mut wrapper,
637            "aaaa bbbb cccc ddddd eeee fff gg",
638            "cccc ddddd eeee fff gg",
639            "",
640        );
641        perform_test(
642            &mut wrapper,
643            "aaaa bbbb cccc ddddd eeee fff gg",
644            "…ccc ddddd eeee fff gg",
645            "",
646        );
647        perform_test(
648            &mut wrapper,
649            "aaaa bbbb cccc ddddd eeee fff gg",
650            "......dddd eeee fff gg",
651            "......",
652        );
653        perform_test(
654            &mut wrapper,
655            "aaaa bbbb cccc 🦀🦀🦀🦀🦀 eeee fff gg",
656            "…🦀🦀🦀🦀 eeee fff gg",
657            "",
658        );
659    }
660
661    #[test]
662    fn test_truncate_multiple_runs_end() {
663        let mut wrapper = build_wrapper();
664
665        fn perform_test(
666            wrapper: &mut LineWrapper,
667            text: &'static str,
668            expected: &str,
669            run_lens: &[usize],
670            result_run_len: &[usize],
671            line_width: Pixels,
672        ) {
673            let dummy_runs = generate_test_runs(run_lens);
674            let (result, dummy_runs) =
675                wrapper.truncate_line(text.into(), line_width, "", &dummy_runs, TruncateFrom::End);
676            assert_eq!(result, expected);
677            for (run, result_len) in dummy_runs.iter().zip(result_run_len) {
678                assert_eq!(run.len, *result_len);
679            }
680        }
681        // Case 0: Normal
682        // Text: abcdefghijkl
683        // Runs: Run0 { len: 12, ... }
684        //
685        // Truncate res: abcd… (truncate_at = 4)
686        // Run res: Run0 { string: abcd…, len: 7, ... }
687        perform_test(&mut wrapper, "abcdefghijkl", "abcd…", &[12], &[7], px(50.));
688        // Case 1: Drop some runs
689        // Text: abcdefghijkl
690        // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
691        //
692        // Truncate res: abcdef… (truncate_at = 6)
693        // Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: ef…, len:
694        // 5, ... }
695        perform_test(
696            &mut wrapper,
697            "abcdefghijkl",
698            "abcdef…",
699            &[4, 4, 4],
700            &[4, 5],
701            px(70.),
702        );
703        // Case 2: Truncate at start of some run
704        // Text: abcdefghijkl
705        // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
706        //
707        // Truncate res: abcdefgh… (truncate_at = 8)
708        // Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: efgh, len:
709        // 4, ... }, Run2 { string: …, len: 3, ... }
710        perform_test(
711            &mut wrapper,
712            "abcdefghijkl",
713            "abcdefgh…",
714            &[4, 4, 4],
715            &[4, 4, 3],
716            px(90.),
717        );
718    }
719
720    #[test]
721    fn test_truncate_multiple_runs_start() {
722        let mut wrapper = build_wrapper();
723
724        #[track_caller]
725        fn perform_test(
726            wrapper: &mut LineWrapper,
727            text: &'static str,
728            expected: &str,
729            run_lens: &[usize],
730            result_run_len: &[usize],
731            line_width: Pixels,
732        ) {
733            let dummy_runs = generate_test_runs(run_lens);
734            let (result, dummy_runs) = wrapper.truncate_line(
735                text.into(),
736                line_width,
737                "",
738                &dummy_runs,
739                TruncateFrom::Start,
740            );
741            assert_eq!(result, expected);
742            for (run, result_len) in dummy_runs.iter().zip(result_run_len) {
743                assert_eq!(run.len, *result_len);
744            }
745        }
746        // Case 0: Normal
747        // Text: abcdefghijkl
748        // Runs: Run0 { len: 12, ... }
749        //
750        // Truncate res: …ijkl (truncate_at = 9)
751        // Run res: Run0 { string: …ijkl, len: 7, ... }
752        perform_test(&mut wrapper, "abcdefghijkl", "…ijkl", &[12], &[7], px(50.));
753        // Case 1: Drop some runs
754        // Text: abcdefghijkl
755        // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
756        //
757        // Truncate res: …ghijkl (truncate_at = 7)
758        // Runs res: Run0 { string: …gh, len: 5, ... }, Run1 { string: ijkl, len:
759        // 4, ... }
760        perform_test(
761            &mut wrapper,
762            "abcdefghijkl",
763            "…ghijkl",
764            &[4, 4, 4],
765            &[5, 4],
766            px(70.),
767        );
768        // Case 2: Truncate at start of some run
769        // Text: abcdefghijkl
770        // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
771        //
772        // Truncate res: abcdefgh… (truncate_at = 3)
773        // Runs res: Run0 { string: …, len: 3, ... }, Run1 { string: efgh, len:
774        // 4, ... }, Run2 { string: ijkl, len: 4, ... }
775        perform_test(
776            &mut wrapper,
777            "abcdefghijkl",
778            "…efghijkl",
779            &[4, 4, 4],
780            &[3, 4, 4],
781            px(90.),
782        );
783    }
784
785    #[test]
786    fn test_update_run_after_truncation_end() {
787        fn perform_test(result: &str, run_lens: &[usize], result_run_lens: &[usize]) {
788            let mut dummy_runs = generate_test_runs(run_lens);
789            update_runs_after_truncation(result, "", &mut dummy_runs, TruncateFrom::End);
790            for (run, result_len) in dummy_runs.iter().zip(result_run_lens) {
791                assert_eq!(run.len, *result_len);
792            }
793        }
794        // Case 0: Normal
795        // Text: abcdefghijkl
796        // Runs: Run0 { len: 12, ... }
797        //
798        // Truncate res: abcd… (truncate_at = 4)
799        // Run res: Run0 { string: abcd…, len: 7, ... }
800        perform_test("abcd…", &[12], &[7]);
801        // Case 1: Drop some runs
802        // Text: abcdefghijkl
803        // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
804        //
805        // Truncate res: abcdef… (truncate_at = 6)
806        // Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: ef…, len:
807        // 5, ... }
808        perform_test("abcdef…", &[4, 4, 4], &[4, 5]);
809        // Case 2: Truncate at start of some run
810        // Text: abcdefghijkl
811        // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
812        //
813        // Truncate res: abcdefgh… (truncate_at = 8)
814        // Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: efgh, len:
815        // 4, ... }, Run2 { string: …, len: 3, ... }
816        perform_test("abcdefgh…", &[4, 4, 4], &[4, 4, 3]);
817    }
818
819    #[test]
820    fn test_is_word_char() {
821        #[track_caller]
822        fn assert_word(word: &str) {
823            for c in word.chars() {
824                assert!(
825                    LineWrapper::is_word_char(c),
826                    "assertion failed for '{}' (unicode 0x{:x})",
827                    c,
828                    c as u32
829                );
830            }
831        }
832
833        #[track_caller]
834        fn assert_not_word(word: &str) {
835            let found = word.chars().any(|c| !LineWrapper::is_word_char(c));
836            assert!(found, "assertion failed for '{}'", word);
837        }
838
839        assert_word("Hello123");
840        assert_word("non-English");
841        assert_word("var_name");
842        assert_word("123456");
843        assert_word("3.1415");
844        assert_word("10^2");
845        assert_word("1~2");
846        assert_word("100%");
847        assert_word("@mention");
848        assert_word("#hashtag");
849        assert_word("$variable");
850        assert_word("a=1");
851        assert_word("Self::is_word_char");
852        assert_word("more⋯");
853
854        // Space
855        assert_not_word("foo bar");
856
857        // URL case
858        assert_word("github.com");
859        assert_not_word("zed-industries/zed");
860        assert_not_word("zed-industries\\zed");
861        assert_not_word("a=1&b=2");
862        assert_not_word("foo?b=2");
863
864        // Latin-1 Supplement
865        assert_word("ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏ");
866        // Latin Extended-A
867        assert_word("ĀāĂ㥹ĆćĈĉĊċČčĎď");
868        // Latin Extended-B
869        assert_word("ƀƁƂƃƄƅƆƇƈƉƊƋƌƍƎƏ");
870        // Cyrillic
871        assert_word("АБВГДЕЖЗИЙКЛМНОП");
872        // Vietnamese (https://github.com/zed-industries/zed/issues/23245)
873        assert_word("ThậmchíđếnkhithuachạychúngcònnhẫntâmgiếtnốtsốđôngtùchínhtrịởYênBáivàCaoBằng");
874
875        // non-word characters
876        assert_not_word("你好");
877        assert_not_word("안녕하세요");
878        assert_not_word("こんにちは");
879        assert_not_word("😀😁😂");
880        assert_not_word("()[]{}<>");
881    }
882
883    // For compatibility with the test macro
884    #[cfg(target_os = "macos")]
885    use crate as gpui;
886
887    // These seem to vary wildly based on the text system.
888    #[cfg(target_os = "macos")]
889    #[crate::test]
890    fn test_wrap_shaped_line(cx: &mut TestAppContext) {
891        cx.update(|cx| {
892            let text_system = WindowTextSystem::new(cx.text_system().clone());
893
894            let normal = TextRun {
895                len: 0,
896                font: font("Helvetica"),
897                color: Default::default(),
898                underline: Default::default(),
899                ..Default::default()
900            };
901            let bold = TextRun {
902                len: 0,
903                font: font("Helvetica").bold(),
904                ..Default::default()
905            };
906
907            let text = "aa bbb cccc ddddd eeee".into();
908            let lines = text_system
909                .shape_text(
910                    text,
911                    px(16.),
912                    &[
913                        normal.with_len(4),
914                        bold.with_len(5),
915                        normal.with_len(6),
916                        bold.with_len(1),
917                        normal.with_len(7),
918                    ],
919                    Some(px(72.)),
920                    None,
921                )
922                .unwrap();
923
924            assert_eq!(
925                lines[0].layout.wrap_boundaries(),
926                &[
927                    WrapBoundary {
928                        run_ix: 0,
929                        glyph_ix: 7
930                    },
931                    WrapBoundary {
932                        run_ix: 0,
933                        glyph_ix: 12
934                    },
935                    WrapBoundary {
936                        run_ix: 0,
937                        glyph_ix: 18
938                    }
939                ],
940            );
941        });
942    }
943}