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}