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
399    fn build_wrapper() -> LineWrapper {
400        let dispatcher = TestDispatcher::new(0);
401        let cx = TestAppContext::build(dispatcher, None);
402        let id = cx.text_system().resolve_font(&font(".ZedMono"));
403        LineWrapper::new(id, px(16.), cx.text_system().platform_text_system.clone())
404    }
405
406    fn generate_test_runs(input_run_len: &[usize]) -> Vec<TextRun> {
407        input_run_len
408            .iter()
409            .map(|run_len| TextRun {
410                len: *run_len,
411                font: Font {
412                    family: "Dummy".into(),
413                    features: FontFeatures::default(),
414                    fallbacks: None,
415                    weight: FontWeight::default(),
416                    style: FontStyle::Normal,
417                },
418                ..Default::default()
419            })
420            .collect()
421    }
422
423    #[test]
424    fn test_wrap_line() {
425        let mut wrapper = build_wrapper();
426
427        assert_eq!(
428            wrapper
429                .wrap_line(&[LineFragment::text("aa bbb cccc ddddd eeee")], px(72.))
430                .collect::<Vec<_>>(),
431            &[
432                Boundary::new(7, 0),
433                Boundary::new(12, 0),
434                Boundary::new(18, 0)
435            ],
436        );
437        assert_eq!(
438            wrapper
439                .wrap_line(&[LineFragment::text("aaa aaaaaaaaaaaaaaaaaa")], px(72.0))
440                .collect::<Vec<_>>(),
441            &[
442                Boundary::new(4, 0),
443                Boundary::new(11, 0),
444                Boundary::new(18, 0)
445            ],
446        );
447        assert_eq!(
448            wrapper
449                .wrap_line(&[LineFragment::text("     aaaaaaa")], px(72.))
450                .collect::<Vec<_>>(),
451            &[
452                Boundary::new(7, 5),
453                Boundary::new(9, 5),
454                Boundary::new(11, 5),
455            ]
456        );
457        assert_eq!(
458            wrapper
459                .wrap_line(
460                    &[LineFragment::text("                            ")],
461                    px(72.)
462                )
463                .collect::<Vec<_>>(),
464            &[
465                Boundary::new(7, 0),
466                Boundary::new(14, 0),
467                Boundary::new(21, 0)
468            ]
469        );
470        assert_eq!(
471            wrapper
472                .wrap_line(&[LineFragment::text("          aaaaaaaaaaaaaa")], px(72.))
473                .collect::<Vec<_>>(),
474            &[
475                Boundary::new(7, 0),
476                Boundary::new(14, 3),
477                Boundary::new(18, 3),
478                Boundary::new(22, 3),
479            ]
480        );
481
482        // Test wrapping multiple text fragments
483        assert_eq!(
484            wrapper
485                .wrap_line(
486                    &[
487                        LineFragment::text("aa bbb "),
488                        LineFragment::text("cccc ddddd eeee")
489                    ],
490                    px(72.)
491                )
492                .collect::<Vec<_>>(),
493            &[
494                Boundary::new(7, 0),
495                Boundary::new(12, 0),
496                Boundary::new(18, 0)
497            ],
498        );
499
500        // Test wrapping with a mix of text and element fragments
501        assert_eq!(
502            wrapper
503                .wrap_line(
504                    &[
505                        LineFragment::text("aa "),
506                        LineFragment::element(px(20.), 1),
507                        LineFragment::text(" bbb "),
508                        LineFragment::element(px(30.), 1),
509                        LineFragment::text(" cccc")
510                    ],
511                    px(72.)
512                )
513                .collect::<Vec<_>>(),
514            &[
515                Boundary::new(5, 0),
516                Boundary::new(9, 0),
517                Boundary::new(11, 0)
518            ],
519        );
520
521        // Test with element at the beginning and text afterward
522        assert_eq!(
523            wrapper
524                .wrap_line(
525                    &[
526                        LineFragment::element(px(50.), 1),
527                        LineFragment::text(" aaaa bbbb cccc dddd")
528                    ],
529                    px(72.)
530                )
531                .collect::<Vec<_>>(),
532            &[
533                Boundary::new(2, 0),
534                Boundary::new(7, 0),
535                Boundary::new(12, 0),
536                Boundary::new(17, 0)
537            ],
538        );
539
540        // Test with a large element that forces wrapping by itself
541        assert_eq!(
542            wrapper
543                .wrap_line(
544                    &[
545                        LineFragment::text("short text "),
546                        LineFragment::element(px(100.), 1),
547                        LineFragment::text(" more text")
548                    ],
549                    px(72.)
550                )
551                .collect::<Vec<_>>(),
552            &[
553                Boundary::new(6, 0),
554                Boundary::new(11, 0),
555                Boundary::new(12, 0),
556                Boundary::new(18, 0)
557            ],
558        );
559    }
560
561    #[test]
562    fn test_truncate_line_end() {
563        let mut wrapper = build_wrapper();
564
565        fn perform_test(
566            wrapper: &mut LineWrapper,
567            text: &'static str,
568            expected: &'static str,
569            ellipsis: &str,
570        ) {
571            let dummy_run_lens = vec![text.len()];
572            let dummy_runs = generate_test_runs(&dummy_run_lens);
573            let (result, dummy_runs) = wrapper.truncate_line(
574                text.into(),
575                px(220.),
576                ellipsis,
577                &dummy_runs,
578                TruncateFrom::End,
579            );
580            assert_eq!(result, expected);
581            assert_eq!(dummy_runs.first().unwrap().len, result.len());
582        }
583
584        perform_test(
585            &mut wrapper,
586            "aa bbb cccc ddddd eeee ffff gggg",
587            "aa bbb cccc ddddd eeee",
588            "",
589        );
590        perform_test(
591            &mut wrapper,
592            "aa bbb cccc ddddd eeee ffff gggg",
593            "aa bbb cccc ddddd eee…",
594            "",
595        );
596        perform_test(
597            &mut wrapper,
598            "aa bbb cccc ddddd eeee ffff gggg",
599            "aa bbb cccc dddd......",
600            "......",
601        );
602    }
603
604    #[test]
605    fn test_truncate_line_start() {
606        let mut wrapper = build_wrapper();
607
608        fn perform_test(
609            wrapper: &mut LineWrapper,
610            text: &'static str,
611            expected: &'static str,
612            ellipsis: &str,
613        ) {
614            let dummy_run_lens = vec![text.len()];
615            let dummy_runs = generate_test_runs(&dummy_run_lens);
616            let (result, dummy_runs) = wrapper.truncate_line(
617                text.into(),
618                px(220.),
619                ellipsis,
620                &dummy_runs,
621                TruncateFrom::Start,
622            );
623            assert_eq!(result, expected);
624            assert_eq!(dummy_runs.first().unwrap().len, result.len());
625        }
626
627        perform_test(
628            &mut wrapper,
629            "aaaa bbbb cccc ddddd eeee fff gg",
630            "cccc ddddd eeee fff gg",
631            "",
632        );
633        perform_test(
634            &mut wrapper,
635            "aaaa bbbb cccc ddddd eeee fff gg",
636            "…ccc ddddd eeee fff gg",
637            "",
638        );
639        perform_test(
640            &mut wrapper,
641            "aaaa bbbb cccc ddddd eeee fff gg",
642            "......dddd eeee fff gg",
643            "......",
644        );
645    }
646
647    #[test]
648    fn test_truncate_multiple_runs_end() {
649        let mut wrapper = build_wrapper();
650
651        fn perform_test(
652            wrapper: &mut LineWrapper,
653            text: &'static str,
654            expected: &str,
655            run_lens: &[usize],
656            result_run_len: &[usize],
657            line_width: Pixels,
658        ) {
659            let dummy_runs = generate_test_runs(run_lens);
660            let (result, dummy_runs) =
661                wrapper.truncate_line(text.into(), line_width, "", &dummy_runs, TruncateFrom::End);
662            assert_eq!(result, expected);
663            for (run, result_len) in dummy_runs.iter().zip(result_run_len) {
664                assert_eq!(run.len, *result_len);
665            }
666        }
667        // Case 0: Normal
668        // Text: abcdefghijkl
669        // Runs: Run0 { len: 12, ... }
670        //
671        // Truncate res: abcd… (truncate_at = 4)
672        // Run res: Run0 { string: abcd…, len: 7, ... }
673        perform_test(&mut wrapper, "abcdefghijkl", "abcd…", &[12], &[7], px(50.));
674        // Case 1: Drop some runs
675        // Text: abcdefghijkl
676        // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
677        //
678        // Truncate res: abcdef… (truncate_at = 6)
679        // Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: ef…, len:
680        // 5, ... }
681        perform_test(
682            &mut wrapper,
683            "abcdefghijkl",
684            "abcdef…",
685            &[4, 4, 4],
686            &[4, 5],
687            px(70.),
688        );
689        // Case 2: Truncate at start of some run
690        // Text: abcdefghijkl
691        // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
692        //
693        // Truncate res: abcdefgh… (truncate_at = 8)
694        // Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: efgh, len:
695        // 4, ... }, Run2 { string: …, len: 3, ... }
696        perform_test(
697            &mut wrapper,
698            "abcdefghijkl",
699            "abcdefgh…",
700            &[4, 4, 4],
701            &[4, 4, 3],
702            px(90.),
703        );
704    }
705
706    #[test]
707    fn test_truncate_multiple_runs_start() {
708        let mut wrapper = build_wrapper();
709
710        #[track_caller]
711        fn perform_test(
712            wrapper: &mut LineWrapper,
713            text: &'static str,
714            expected: &str,
715            run_lens: &[usize],
716            result_run_len: &[usize],
717            line_width: Pixels,
718        ) {
719            let dummy_runs = generate_test_runs(run_lens);
720            let (result, dummy_runs) = wrapper.truncate_line(
721                text.into(),
722                line_width,
723                "",
724                &dummy_runs,
725                TruncateFrom::Start,
726            );
727            assert_eq!(result, expected);
728            for (run, result_len) in dummy_runs.iter().zip(result_run_len) {
729                assert_eq!(run.len, *result_len);
730            }
731        }
732        // Case 0: Normal
733        // Text: abcdefghijkl
734        // Runs: Run0 { len: 12, ... }
735        //
736        // Truncate res: …ijkl (truncate_at = 9)
737        // Run res: Run0 { string: …ijkl, len: 7, ... }
738        perform_test(&mut wrapper, "abcdefghijkl", "…ijkl", &[12], &[7], px(50.));
739        // Case 1: Drop some runs
740        // Text: abcdefghijkl
741        // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
742        //
743        // Truncate res: …ghijkl (truncate_at = 7)
744        // Runs res: Run0 { string: …gh, len: 5, ... }, Run1 { string: ijkl, len:
745        // 4, ... }
746        perform_test(
747            &mut wrapper,
748            "abcdefghijkl",
749            "…ghijkl",
750            &[4, 4, 4],
751            &[5, 4],
752            px(70.),
753        );
754        // Case 2: Truncate at start of some run
755        // Text: abcdefghijkl
756        // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
757        //
758        // Truncate res: abcdefgh… (truncate_at = 3)
759        // Runs res: Run0 { string: …, len: 3, ... }, Run1 { string: efgh, len:
760        // 4, ... }, Run2 { string: ijkl, len: 4, ... }
761        perform_test(
762            &mut wrapper,
763            "abcdefghijkl",
764            "…efghijkl",
765            &[4, 4, 4],
766            &[3, 4, 4],
767            px(90.),
768        );
769    }
770
771    #[test]
772    fn test_update_run_after_truncation_end() {
773        fn perform_test(result: &str, run_lens: &[usize], result_run_lens: &[usize]) {
774            let mut dummy_runs = generate_test_runs(run_lens);
775            update_runs_after_truncation(result, "", &mut dummy_runs, TruncateFrom::End);
776            for (run, result_len) in dummy_runs.iter().zip(result_run_lens) {
777                assert_eq!(run.len, *result_len);
778            }
779        }
780        // Case 0: Normal
781        // Text: abcdefghijkl
782        // Runs: Run0 { len: 12, ... }
783        //
784        // Truncate res: abcd… (truncate_at = 4)
785        // Run res: Run0 { string: abcd…, len: 7, ... }
786        perform_test("abcd…", &[12], &[7]);
787        // Case 1: Drop some runs
788        // Text: abcdefghijkl
789        // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
790        //
791        // Truncate res: abcdef… (truncate_at = 6)
792        // Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: ef…, len:
793        // 5, ... }
794        perform_test("abcdef…", &[4, 4, 4], &[4, 5]);
795        // Case 2: Truncate at start of some run
796        // Text: abcdefghijkl
797        // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
798        //
799        // Truncate res: abcdefgh… (truncate_at = 8)
800        // Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: efgh, len:
801        // 4, ... }, Run2 { string: …, len: 3, ... }
802        perform_test("abcdefgh…", &[4, 4, 4], &[4, 4, 3]);
803    }
804
805    #[test]
806    fn test_is_word_char() {
807        #[track_caller]
808        fn assert_word(word: &str) {
809            for c in word.chars() {
810                assert!(
811                    LineWrapper::is_word_char(c),
812                    "assertion failed for '{}' (unicode 0x{:x})",
813                    c,
814                    c as u32
815                );
816            }
817        }
818
819        #[track_caller]
820        fn assert_not_word(word: &str) {
821            let found = word.chars().any(|c| !LineWrapper::is_word_char(c));
822            assert!(found, "assertion failed for '{}'", word);
823        }
824
825        assert_word("Hello123");
826        assert_word("non-English");
827        assert_word("var_name");
828        assert_word("123456");
829        assert_word("3.1415");
830        assert_word("10^2");
831        assert_word("1~2");
832        assert_word("100%");
833        assert_word("@mention");
834        assert_word("#hashtag");
835        assert_word("$variable");
836        assert_word("a=1");
837        assert_word("Self::is_word_char");
838        assert_word("more⋯");
839
840        // Space
841        assert_not_word("foo bar");
842
843        // URL case
844        assert_word("github.com");
845        assert_not_word("zed-industries/zed");
846        assert_not_word("zed-industries\\zed");
847        assert_not_word("a=1&b=2");
848        assert_not_word("foo?b=2");
849
850        // Latin-1 Supplement
851        assert_word("ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏ");
852        // Latin Extended-A
853        assert_word("ĀāĂ㥹ĆćĈĉĊċČčĎď");
854        // Latin Extended-B
855        assert_word("ƀƁƂƃƄƅƆƇƈƉƊƋƌƍƎƏ");
856        // Cyrillic
857        assert_word("АБВГДЕЖЗИЙКЛМНОП");
858        // Vietnamese (https://github.com/zed-industries/zed/issues/23245)
859        assert_word("ThậmchíđếnkhithuachạychúngcònnhẫntâmgiếtnốtsốđôngtùchínhtrịởYênBáivàCaoBằng");
860
861        // non-word characters
862        assert_not_word("你好");
863        assert_not_word("안녕하세요");
864        assert_not_word("こんにちは");
865        assert_not_word("😀😁😂");
866        assert_not_word("()[]{}<>");
867    }
868
869    // For compatibility with the test macro
870    #[cfg(target_os = "macos")]
871    use crate as gpui;
872
873    // These seem to vary wildly based on the text system.
874    #[cfg(target_os = "macos")]
875    #[crate::test]
876    fn test_wrap_shaped_line(cx: &mut TestAppContext) {
877        cx.update(|cx| {
878            let text_system = WindowTextSystem::new(cx.text_system().clone());
879
880            let normal = TextRun {
881                len: 0,
882                font: font("Helvetica"),
883                color: Default::default(),
884                underline: Default::default(),
885                ..Default::default()
886            };
887            let bold = TextRun {
888                len: 0,
889                font: font("Helvetica").bold(),
890                ..Default::default()
891            };
892
893            let text = "aa bbb cccc ddddd eeee".into();
894            let lines = text_system
895                .shape_text(
896                    text,
897                    px(16.),
898                    &[
899                        normal.with_len(4),
900                        bold.with_len(5),
901                        normal.with_len(6),
902                        bold.with_len(1),
903                        normal.with_len(7),
904                    ],
905                    Some(px(72.)),
906                    None,
907                )
908                .unwrap();
909
910            assert_eq!(
911                lines[0].layout.wrap_boundaries(),
912                &[
913                    WrapBoundary {
914                        run_ix: 0,
915                        glyph_ix: 7
916                    },
917                    WrapBoundary {
918                        run_ix: 0,
919                        glyph_ix: 12
920                    },
921                    WrapBoundary {
922                        run_ix: 0,
923                        glyph_ix: 18
924                    }
925                ],
926            );
927        });
928    }
929}