1use crate::{FontId, Pixels, SharedString, TextRun, TextSystem, px};
2use collections::HashMap;
3use std::{borrow::Cow, iter, sync::Arc};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
7pub enum TruncateFrom {
8 Start,
10 End,
12 Middle,
14}
15
16pub struct LineWrapper {
18 text_system: Arc<TextSystem>,
19 pub(crate) font_id: FontId,
20 pub(crate) font_size: Pixels,
21 cached_ascii_char_widths: [Option<Pixels>; 128],
22 cached_other_char_widths: HashMap<char, Pixels>,
23}
24
25impl LineWrapper {
26 pub const MAX_INDENT: u32 = 256;
28
29 pub(crate) fn new(font_id: FontId, font_size: Pixels, text_system: Arc<TextSystem>) -> Self {
30 Self {
31 text_system,
32 font_id,
33 font_size,
34 cached_ascii_char_widths: [None; 128],
35 cached_other_char_widths: HashMap::default(),
36 }
37 }
38
39 pub fn wrap_line<'a>(
41 &'a mut self,
42 fragments: &'a [LineFragment],
43 wrap_width: Pixels,
44 ) -> impl Iterator<Item = Boundary> + 'a {
45 let mut width = px(0.);
46 let mut first_non_whitespace_ix = None;
47 let mut indent = None;
48 let mut last_candidate_ix = 0;
49 let mut last_candidate_width = px(0.);
50 let mut last_wrap_ix = 0;
51 let mut prev_c = '\0';
52 let mut index = 0;
53 let mut candidates = fragments
54 .iter()
55 .flat_map(move |fragment| fragment.wrap_boundary_candidates())
56 .peekable();
57 iter::from_fn(move || {
58 for candidate in candidates.by_ref() {
59 let ix = index;
60 index += candidate.len_utf8();
61 let mut new_prev_c = prev_c;
62 let item_width = match candidate {
63 WrapBoundaryCandidate::Char { character: c } => {
64 if c == '\n' {
65 continue;
66 }
67
68 if Self::is_word_char(c) {
69 if prev_c == ' ' && c != ' ' && first_non_whitespace_ix.is_some() {
70 last_candidate_ix = ix;
71 last_candidate_width = width;
72 }
73 } else {
74 if c != ' ' && first_non_whitespace_ix.is_some() {
76 last_candidate_ix = ix;
77 last_candidate_width = width;
78 }
79 }
80
81 if c != ' ' && first_non_whitespace_ix.is_none() {
82 first_non_whitespace_ix = Some(ix);
83 }
84
85 new_prev_c = c;
86
87 self.width_for_char(c)
88 }
89 WrapBoundaryCandidate::Element {
90 width: element_width,
91 ..
92 } => {
93 if prev_c == ' ' && first_non_whitespace_ix.is_some() {
94 last_candidate_ix = ix;
95 last_candidate_width = width;
96 }
97
98 if first_non_whitespace_ix.is_none() {
99 first_non_whitespace_ix = Some(ix);
100 }
101
102 element_width
103 }
104 };
105
106 width += item_width;
107 if width > wrap_width && ix > last_wrap_ix {
108 if let (None, Some(first_non_whitespace_ix)) = (indent, first_non_whitespace_ix)
109 {
110 indent = Some(
111 Self::MAX_INDENT.min((first_non_whitespace_ix - last_wrap_ix) as u32),
112 );
113 }
114
115 if last_candidate_ix > 0 {
116 last_wrap_ix = last_candidate_ix;
117 width -= last_candidate_width;
118 last_candidate_ix = 0;
119 } else {
120 last_wrap_ix = ix;
121 width = item_width;
122 }
123
124 if let Some(indent) = indent {
125 width += self.width_for_char(' ') * indent as f32;
126 }
127
128 return Some(Boundary::new(last_wrap_ix, indent.unwrap_or(0)));
129 }
130
131 prev_c = new_prev_c;
132 }
133
134 None
135 })
136 }
137
138 pub fn should_truncate_line(
142 &mut self,
143 line: &str,
144 truncate_width: Pixels,
145 truncation_affix: &str,
146 truncate_from: TruncateFrom,
147 ) -> Option<usize> {
148 let mut width = px(0.);
149 let suffix_width = truncation_affix
150 .chars()
151 .map(|c| self.width_for_char(c))
152 .fold(px(0.0), |a, x| a + x);
153 let mut truncate_ix = 0;
154
155 match truncate_from {
156 TruncateFrom::Start => {
157 for (ix, c) in line.char_indices().rev() {
158 if width + suffix_width < truncate_width {
159 truncate_ix = ix;
160 }
161
162 let char_width = self.width_for_char(c);
163 width += char_width;
164
165 if width.floor() > truncate_width {
166 return Some(truncate_ix);
167 }
168 }
169 }
170 TruncateFrom::End => {
171 for (ix, c) in line.char_indices() {
172 if width + suffix_width < truncate_width {
173 truncate_ix = ix;
174 }
175
176 let char_width = self.width_for_char(c);
177 width += char_width;
178
179 if width.floor() > truncate_width {
180 return Some(truncate_ix);
181 }
182 }
183 }
184 TruncateFrom::Middle => {}
185 }
186
187 None
188 }
189
190 fn should_truncate_line_middle(
191 &mut self,
192 line: &str,
193 truncate_width: Pixels,
194 truncation_affix: &str,
195 ) -> Option<(usize, usize)> {
196 let suffix_width = truncation_affix
197 .chars()
198 .map(|c| self.width_for_char(c))
199 .fold(px(0.0), |a, x| a + x);
200
201 let total_width: Pixels = line
202 .chars()
203 .map(|c| self.width_for_char(c))
204 .fold(px(0.0), |a, x| a + x);
205
206 if total_width <= truncate_width {
207 return None;
208 }
209
210 let content_budget = truncate_width - suffix_width;
211 if content_budget <= px(0.) {
212 return Some((0, line.len()));
213 }
214
215 let front_budget = content_budget * (2.0 / 3.0);
216 let back_budget = content_budget - front_budget;
217
218 let mut front_width = px(0.);
219 let mut front_end_ix = 0usize;
220 for (ix, c) in line.char_indices() {
221 let char_width = self.width_for_char(c);
222 if front_width + char_width > front_budget {
223 break;
224 }
225 front_width += char_width;
226 front_end_ix = ix + c.len_utf8();
227 }
228
229 let mut back_width = px(0.);
230 let mut back_start_ix = line.len();
231 for (ix, c) in line.char_indices().rev() {
232 let char_width = self.width_for_char(c);
233 if back_width + char_width > back_budget {
234 break;
235 }
236 back_width += char_width;
237 back_start_ix = ix;
238 }
239
240 if front_end_ix >= back_start_ix {
241 return Some((0, line.len()));
242 }
243
244 Some((front_end_ix, back_start_ix))
245 }
246
247 pub fn truncate_line<'a>(
249 &mut self,
250 line: SharedString,
251 truncate_width: Pixels,
252 truncation_affix: &str,
253 runs: &'a [TextRun],
254 truncate_from: TruncateFrom,
255 ) -> (SharedString, Cow<'a, [TextRun]>) {
256 if truncate_from == TruncateFrom::Middle {
257 if let Some((front_end_ix, back_start_ix)) =
258 self.should_truncate_line_middle(&line, truncate_width, truncation_affix)
259 {
260 let result = SharedString::from(format!(
261 "{}{truncation_affix}{}",
262 &line[..front_end_ix],
263 &line[back_start_ix..]
264 ));
265 let mut runs = runs.to_vec();
266 update_runs_after_middle_truncation(
267 truncation_affix,
268 &mut runs,
269 front_end_ix,
270 back_start_ix,
271 );
272 return (result, Cow::Owned(runs));
273 } else {
274 return (line, Cow::Borrowed(runs));
275 }
276 }
277
278 if let Some(truncate_ix) =
279 self.should_truncate_line(&line, truncate_width, truncation_affix, truncate_from)
280 {
281 let result = match truncate_from {
282 TruncateFrom::Start => SharedString::from(format!(
283 "{truncation_affix}{}",
284 &line[line.ceil_char_boundary(truncate_ix + 1)..]
285 )),
286 TruncateFrom::End => SharedString::from(format!(
287 "{}{truncation_affix}",
288 line[..truncate_ix]
289 .trim_end_matches(|c: char| c.is_whitespace() || c.is_ascii_punctuation())
290 )),
291 TruncateFrom::Middle => unreachable!("Middle truncation is handled above"),
292 };
293 let mut runs = runs.to_vec();
294 update_runs_after_truncation(&result, truncation_affix, &mut runs, truncate_from);
295 (result, Cow::Owned(runs))
296 } else {
297 (line, Cow::Borrowed(runs))
298 }
299 }
300
301 pub fn truncate_wrapped_line<'a>(
311 &mut self,
312 text: SharedString,
313 wrap_width: Pixels,
314 max_lines: usize,
315 truncation_affix: &str,
316 runs: &'a [TextRun],
317 truncate_from: TruncateFrom,
318 ) -> (SharedString, Cow<'a, [TextRun]>) {
319 if max_lines <= 1 || truncate_from == TruncateFrom::Start {
320 return self.truncate_line(
321 text,
322 wrap_width * max_lines,
323 truncation_affix,
324 runs,
325 truncate_from,
326 );
327 }
328 if truncate_from == TruncateFrom::Middle {
329 return self.truncate_line(text, wrap_width, truncation_affix, runs, truncate_from);
330 }
331
332 let affix_width: Pixels = truncation_affix
333 .chars()
334 .map(|c| self.width_for_char(c))
335 .sum();
336
337 let mut width = px(0.);
338 let mut line = 0usize;
339 let mut first_non_whitespace_ix = None;
340 let mut last_candidate_ix = 0usize;
341 let mut last_candidate_width = px(0.);
342 let mut last_wrap_ix = 0usize;
343 let mut prev_c = '\0';
344 let mut indent: Option<u32> = None;
345 let mut truncate_ix = 0usize;
346
347 for (ix, c) in text.char_indices() {
348 if c == '\n' {
349 if line >= max_lines - 1 && !text[ix + 1..].trim().is_empty() {
350 let truncated = text[..truncate_ix]
353 .trim_end_matches(|c: char| c.is_whitespace() || c.is_ascii_punctuation());
354 let result = SharedString::from(format!("{truncated}{truncation_affix}"));
355 let mut runs = runs.to_vec();
356 update_runs_after_truncation(
357 &result,
358 truncation_affix,
359 &mut runs,
360 TruncateFrom::End,
361 );
362 return (result, Cow::Owned(runs));
363 }
364
365 line += 1;
367 width = px(0.);
368 first_non_whitespace_ix = None;
369 last_candidate_ix = 0;
370 last_candidate_width = px(0.);
371 last_wrap_ix = ix + 1;
372 prev_c = '\0';
373 indent = None;
374 truncate_ix = ix + 1;
375 continue;
376 }
377
378 let char_width = self.width_for_char(c);
379
380 if Self::is_word_char(c) {
381 if prev_c == ' ' && first_non_whitespace_ix.is_some() {
382 last_candidate_ix = ix;
383 last_candidate_width = width;
384 }
385 } else if c != ' ' && first_non_whitespace_ix.is_some() {
386 last_candidate_ix = ix;
387 last_candidate_width = width;
388 }
389
390 if c != ' ' && first_non_whitespace_ix.is_none() {
391 first_non_whitespace_ix = Some(ix);
392 }
393
394 width += char_width;
395
396 if line < max_lines - 1 {
397 if width > wrap_width && ix > last_wrap_ix {
399 if let (None, Some(first_nw)) = (indent, first_non_whitespace_ix) {
400 indent = Some(Self::MAX_INDENT.min((first_nw - last_wrap_ix) as u32));
401 }
402
403 if last_candidate_ix > last_wrap_ix {
404 last_wrap_ix = last_candidate_ix;
405 width -= last_candidate_width;
406 last_candidate_ix = 0;
407 } else {
408 last_wrap_ix = ix;
409 width = char_width;
410 }
411
412 if let Some(ind) = indent {
413 width += self.width_for_char(' ') * ind as f32;
414 }
415
416 line += 1;
417 truncate_ix = last_wrap_ix;
418 }
419 } else {
420 if width + affix_width <= wrap_width {
423 truncate_ix = ix + c.len_utf8();
424 }
425
426 if width > wrap_width {
427 let truncated = text[..truncate_ix]
428 .trim_end_matches(|c: char| c.is_whitespace() || c.is_ascii_punctuation());
429 let result = SharedString::from(format!("{truncated}{truncation_affix}"));
430 let mut runs = runs.to_vec();
431 update_runs_after_truncation(
432 &result,
433 truncation_affix,
434 &mut runs,
435 TruncateFrom::End,
436 );
437 return (result, Cow::Owned(runs));
438 }
439 }
440
441 prev_c = c;
442 }
443
444 (text, Cow::Borrowed(runs))
446 }
447
448 pub(crate) fn is_word_char(c: char) -> bool {
451 c.is_ascii_alphanumeric() ||
453 matches!(c, '\u{00C0}'..='\u{00FF}') ||
457 matches!(c, '\u{0100}'..='\u{017F}') ||
460 matches!(c, '\u{0180}'..='\u{024F}') ||
463 matches!(c, '\u{0400}'..='\u{04FF}') ||
466
467 matches!(c, '\u{1E00}'..='\u{1EFF}') || matches!(c, '\u{0300}'..='\u{036F}') || matches!(c, '\u{0980}'..='\u{09FF}') ||
473
474 matches!(c, '-' | '_' | '.' | '\'' | '’' | '‘' | '$' | '%' | '@' | '#' | '^' | '~' | ',' | '=' | ':' | ';') ||
479 matches!(c, '!' | ')' | ']' | '}' | '"' | '”' | '»' | '…') ||
485 matches!(c, '⋯') ||
487
488 matches!(c, '\u{202F}' | '\u{00A0}' | '\u{2011}')
490 }
491
492 #[inline(always)]
493 fn width_for_char(&mut self, c: char) -> Pixels {
494 if (c as u32) < 128 {
495 if let Some(cached_width) = self.cached_ascii_char_widths[c as usize] {
496 cached_width
497 } else {
498 let width = self
499 .text_system
500 .layout_width(self.font_id, self.font_size, c);
501 self.cached_ascii_char_widths[c as usize] = Some(width);
502 width
503 }
504 } else if let Some(cached_width) = self.cached_other_char_widths.get(&c) {
505 *cached_width
506 } else {
507 let width = self
508 .text_system
509 .layout_width(self.font_id, self.font_size, c);
510 self.cached_other_char_widths.insert(c, width);
511 width
512 }
513 }
514}
515
516fn update_runs_after_truncation(
517 result: &str,
518 ellipsis: &str,
519 runs: &mut Vec<TextRun>,
520 truncate_from: TruncateFrom,
521) {
522 let mut truncate_at = result.len() - ellipsis.len();
523 match truncate_from {
524 TruncateFrom::Start => {
525 for (run_index, run) in runs.iter_mut().enumerate().rev() {
526 if run.len <= truncate_at {
527 truncate_at -= run.len;
528 } else {
529 run.len = truncate_at + ellipsis.len();
530 runs.splice(..run_index, std::iter::empty());
531 break;
532 }
533 }
534 }
535 TruncateFrom::End => {
536 for (run_index, run) in runs.iter_mut().enumerate() {
537 if run.len <= truncate_at {
538 truncate_at -= run.len;
539 } else {
540 run.len = truncate_at + ellipsis.len();
541 runs.truncate(run_index + 1);
542 break;
543 }
544 }
545 }
546 TruncateFrom::Middle => {
547 unreachable!("Middle truncation calls this function with TruncateFrom::End directly")
548 }
549 }
550}
551
552fn update_runs_after_middle_truncation(
553 ellipsis: &str,
554 runs: &mut Vec<TextRun>,
555 front_end_ix: usize,
556 back_start_ix: usize,
557) {
558 let original_runs = std::mem::take(runs);
559 let mut result_runs: Vec<TextRun> = Vec::with_capacity(original_runs.len());
560
561 let mut front_remaining = front_end_ix;
565 let mut front_done = false;
566 for run in &original_runs {
567 if front_done {
568 break;
569 }
570 if run.len <= front_remaining {
571 result_runs.push(run.clone());
572 front_remaining -= run.len;
573 } else {
574 let mut partial = run.clone();
575 partial.len = front_remaining + ellipsis.len();
576 result_runs.push(partial);
577 front_done = true;
578 }
579 }
580 if !front_done {
581 if let Some(last) = result_runs.last_mut() {
584 last.len += ellipsis.len();
585 } else if let Some(first) = original_runs.first() {
586 let mut affix_run = first.clone();
587 affix_run.len = ellipsis.len();
588 result_runs.push(affix_run);
589 }
590 }
591
592 let mut byte_pos = 0usize;
595 for run in &original_runs {
596 let run_end = byte_pos + run.len;
597 if run_end > back_start_ix {
598 if byte_pos < back_start_ix {
599 let mut partial = run.clone();
601 partial.len = run_end - back_start_ix;
602 result_runs.push(partial);
603 } else {
604 result_runs.push(run.clone());
605 }
606 }
607 byte_pos = run_end;
608 }
609
610 *runs = result_runs;
611}
612
613pub enum LineFragment<'a> {
615 Text {
617 text: &'a str,
619 },
620 Element {
622 width: Pixels,
624 len_utf8: usize,
626 },
627}
628
629impl<'a> LineFragment<'a> {
630 pub fn text(text: &'a str) -> Self {
632 LineFragment::Text { text }
633 }
634
635 pub fn element(width: Pixels, len_utf8: usize) -> Self {
637 LineFragment::Element { width, len_utf8 }
638 }
639
640 fn wrap_boundary_candidates(&self) -> impl Iterator<Item = WrapBoundaryCandidate> {
641 let text = match self {
642 LineFragment::Text { text } => text,
643 LineFragment::Element { .. } => "\0",
644 };
645 text.chars().map(move |character| {
646 if let LineFragment::Element { width, len_utf8 } = self {
647 WrapBoundaryCandidate::Element {
648 width: *width,
649 len_utf8: *len_utf8,
650 }
651 } else {
652 WrapBoundaryCandidate::Char { character }
653 }
654 })
655 }
656}
657
658enum WrapBoundaryCandidate {
659 Char { character: char },
660 Element { width: Pixels, len_utf8: usize },
661}
662
663impl WrapBoundaryCandidate {
664 pub fn len_utf8(&self) -> usize {
665 match self {
666 WrapBoundaryCandidate::Char { character } => character.len_utf8(),
667 WrapBoundaryCandidate::Element { len_utf8: len, .. } => *len,
668 }
669 }
670}
671
672#[derive(Copy, Clone, Debug, PartialEq, Eq)]
674pub struct Boundary {
675 pub ix: usize,
677 pub next_indent: u32,
679}
680
681impl Boundary {
682 fn new(ix: usize, next_indent: u32) -> Self {
683 Self { ix, next_indent }
684 }
685}
686
687#[cfg(test)]
688mod tests {
689 use super::*;
690 use crate::{Font, FontFeatures, FontStyle, FontWeight, TestAppContext, TestDispatcher, font};
691 #[cfg(target_os = "macos")]
692 use crate::{TextRun, WindowTextSystem, WrapBoundary};
693
694 fn build_wrapper() -> LineWrapper {
695 let dispatcher = TestDispatcher::new(0);
696 let cx = TestAppContext::build(dispatcher, None);
697 let id = cx.text_system().resolve_font(&font(".ZedMono"));
698 LineWrapper::new(id, px(16.), cx.text_system().clone())
699 }
700
701 fn generate_test_runs(input_run_len: &[usize]) -> Vec<TextRun> {
702 input_run_len
703 .iter()
704 .map(|run_len| TextRun {
705 len: *run_len,
706 font: Font {
707 family: "Dummy".into(),
708 features: FontFeatures::default(),
709 fallbacks: None,
710 weight: FontWeight::default(),
711 style: FontStyle::Normal,
712 },
713 ..Default::default()
714 })
715 .collect()
716 }
717
718 #[test]
719 fn test_wrap_line() {
720 let mut wrapper = build_wrapper();
721
722 assert_eq!(
723 wrapper
724 .wrap_line(&[LineFragment::text("aa bbb cccc ddddd eeee")], px(72.))
725 .collect::<Vec<_>>(),
726 &[
727 Boundary::new(7, 0),
728 Boundary::new(12, 0),
729 Boundary::new(18, 0)
730 ],
731 );
732 assert_eq!(
733 wrapper
734 .wrap_line(&[LineFragment::text("aaa aaaaaaaaaaaaaaaaaa")], px(72.0))
735 .collect::<Vec<_>>(),
736 &[
737 Boundary::new(4, 0),
738 Boundary::new(11, 0),
739 Boundary::new(18, 0)
740 ],
741 );
742 assert_eq!(
743 wrapper
744 .wrap_line(&[LineFragment::text(" aaaaaaa")], px(72.))
745 .collect::<Vec<_>>(),
746 &[
747 Boundary::new(7, 5),
748 Boundary::new(9, 5),
749 Boundary::new(11, 5),
750 ]
751 );
752 assert_eq!(
753 wrapper
754 .wrap_line(
755 &[LineFragment::text(" ")],
756 px(72.)
757 )
758 .collect::<Vec<_>>(),
759 &[
760 Boundary::new(7, 0),
761 Boundary::new(14, 0),
762 Boundary::new(21, 0)
763 ]
764 );
765 assert_eq!(
766 wrapper
767 .wrap_line(&[LineFragment::text(" aaaaaaaaaaaaaa")], px(72.))
768 .collect::<Vec<_>>(),
769 &[
770 Boundary::new(7, 0),
771 Boundary::new(14, 3),
772 Boundary::new(18, 3),
773 Boundary::new(22, 3),
774 ]
775 );
776
777 assert_eq!(
779 wrapper
780 .wrap_line(
781 &[
782 LineFragment::text("aa bbb "),
783 LineFragment::text("cccc ddddd eeee")
784 ],
785 px(72.)
786 )
787 .collect::<Vec<_>>(),
788 &[
789 Boundary::new(7, 0),
790 Boundary::new(12, 0),
791 Boundary::new(18, 0)
792 ],
793 );
794
795 assert_eq!(
797 wrapper
798 .wrap_line(
799 &[
800 LineFragment::text("aa "),
801 LineFragment::element(px(20.), 1),
802 LineFragment::text(" bbb "),
803 LineFragment::element(px(30.), 1),
804 LineFragment::text(" cccc")
805 ],
806 px(72.)
807 )
808 .collect::<Vec<_>>(),
809 &[
810 Boundary::new(5, 0),
811 Boundary::new(9, 0),
812 Boundary::new(11, 0)
813 ],
814 );
815
816 assert_eq!(
818 wrapper
819 .wrap_line(
820 &[
821 LineFragment::element(px(50.), 1),
822 LineFragment::text(" aaaa bbbb cccc dddd")
823 ],
824 px(72.)
825 )
826 .collect::<Vec<_>>(),
827 &[
828 Boundary::new(2, 0),
829 Boundary::new(7, 0),
830 Boundary::new(12, 0),
831 Boundary::new(17, 0)
832 ],
833 );
834
835 assert_eq!(
837 wrapper
838 .wrap_line(
839 &[
840 LineFragment::text("short text "),
841 LineFragment::element(px(100.), 1),
842 LineFragment::text(" more text")
843 ],
844 px(72.)
845 )
846 .collect::<Vec<_>>(),
847 &[
848 Boundary::new(6, 0),
849 Boundary::new(11, 0),
850 Boundary::new(12, 0),
851 Boundary::new(18, 0)
852 ],
853 );
854
855 assert_eq!(
857 wrapper
858 .wrap_line(
859 &[LineFragment::text("a\u{202F}b\u{00A0}c\u{2011}d e")],
860 px(72.0)
861 )
862 .collect::<Vec<_>>(),
863 &[Boundary::new(12, 0),], );
865 }
866
867 #[test]
868 fn test_truncate_line_end() {
869 let mut wrapper = build_wrapper();
870
871 fn perform_test(
872 wrapper: &mut LineWrapper,
873 text: &'static str,
874 expected: &'static str,
875 ellipsis: &str,
876 ) {
877 let dummy_run_lens = vec![text.len()];
878 let dummy_runs = generate_test_runs(&dummy_run_lens);
879 let (result, dummy_runs) = wrapper.truncate_line(
880 text.into(),
881 px(220.),
882 ellipsis,
883 &dummy_runs,
884 TruncateFrom::End,
885 );
886 assert_eq!(result, expected);
887 assert_eq!(dummy_runs.first().unwrap().len, result.len());
888 }
889
890 perform_test(
891 &mut wrapper,
892 "aa bbb cccc ddddd eeee ffff gggg",
893 "aa bbb cccc ddddd eeee",
894 "",
895 );
896 perform_test(
897 &mut wrapper,
898 "aa bbb cccc ddddd eeee ffff gggg",
899 "aa bbb cccc ddddd eee…",
900 "…",
901 );
902 perform_test(
903 &mut wrapper,
904 "aa bbb cccc ddddd eeee ffff gggg",
905 "aa bbb cccc dddd......",
906 "......",
907 );
908 perform_test(
909 &mut wrapper,
910 "aa bbb cccc 🦀🦀🦀🦀🦀 eeee ffff gggg",
911 "aa bbb cccc 🦀🦀🦀🦀…",
912 "…",
913 );
914 }
915
916 #[test]
917 fn test_truncate_line_start() {
918 let mut wrapper = build_wrapper();
919
920 #[track_caller]
921 fn perform_test(
922 wrapper: &mut LineWrapper,
923 text: &'static str,
924 expected: &'static str,
925 ellipsis: &str,
926 ) {
927 let dummy_run_lens = vec![text.len()];
928 let dummy_runs = generate_test_runs(&dummy_run_lens);
929 let (result, dummy_runs) = wrapper.truncate_line(
930 text.into(),
931 px(220.),
932 ellipsis,
933 &dummy_runs,
934 TruncateFrom::Start,
935 );
936 assert_eq!(result, expected);
937 assert_eq!(dummy_runs.first().unwrap().len, result.len());
938 }
939
940 perform_test(
941 &mut wrapper,
942 "aaaa bbbb cccc ddddd eeee fff gg",
943 "cccc ddddd eeee fff gg",
944 "",
945 );
946 perform_test(
947 &mut wrapper,
948 "aaaa bbbb cccc ddddd eeee fff gg",
949 "…ccc ddddd eeee fff gg",
950 "…",
951 );
952 perform_test(
953 &mut wrapper,
954 "aaaa bbbb cccc ddddd eeee fff gg",
955 "......dddd eeee fff gg",
956 "......",
957 );
958 perform_test(
959 &mut wrapper,
960 "aaaa bbbb cccc 🦀🦀🦀🦀🦀 eeee fff gg",
961 "…🦀🦀🦀🦀 eeee fff gg",
962 "…",
963 );
964 }
965
966 #[test]
967 fn test_truncate_multiple_runs_end() {
968 let mut wrapper = build_wrapper();
969
970 fn perform_test(
971 wrapper: &mut LineWrapper,
972 text: &'static str,
973 expected: &str,
974 run_lens: &[usize],
975 result_run_len: &[usize],
976 line_width: Pixels,
977 ) {
978 let dummy_runs = generate_test_runs(run_lens);
979 let (result, dummy_runs) =
980 wrapper.truncate_line(text.into(), line_width, "…", &dummy_runs, TruncateFrom::End);
981 assert_eq!(result, expected);
982 for (run, result_len) in dummy_runs.iter().zip(result_run_len) {
983 assert_eq!(run.len, *result_len);
984 }
985 }
986 perform_test(&mut wrapper, "abcdefghijkl", "abcd…", &[12], &[7], px(50.));
993 perform_test(
1001 &mut wrapper,
1002 "abcdefghijkl",
1003 "abcdef…",
1004 &[4, 4, 4],
1005 &[4, 5],
1006 px(70.),
1007 );
1008 perform_test(
1016 &mut wrapper,
1017 "abcdefghijkl",
1018 "abcdefgh…",
1019 &[4, 4, 4],
1020 &[4, 4, 3],
1021 px(90.),
1022 );
1023 }
1024
1025 #[test]
1026 fn test_truncate_multiple_runs_start() {
1027 let mut wrapper = build_wrapper();
1028
1029 #[track_caller]
1030 fn perform_test(
1031 wrapper: &mut LineWrapper,
1032 text: &'static str,
1033 expected: &str,
1034 run_lens: &[usize],
1035 result_run_len: &[usize],
1036 line_width: Pixels,
1037 ) {
1038 let dummy_runs = generate_test_runs(run_lens);
1039 let (result, dummy_runs) = wrapper.truncate_line(
1040 text.into(),
1041 line_width,
1042 "…",
1043 &dummy_runs,
1044 TruncateFrom::Start,
1045 );
1046 assert_eq!(result, expected);
1047 for (run, result_len) in dummy_runs.iter().zip(result_run_len) {
1048 assert_eq!(run.len, *result_len);
1049 }
1050 }
1051 perform_test(&mut wrapper, "abcdefghijkl", "…ijkl", &[12], &[7], px(50.));
1058 perform_test(
1066 &mut wrapper,
1067 "abcdefghijkl",
1068 "…ghijkl",
1069 &[4, 4, 4],
1070 &[5, 4],
1071 px(70.),
1072 );
1073 perform_test(
1081 &mut wrapper,
1082 "abcdefghijkl",
1083 "…efghijkl",
1084 &[4, 4, 4],
1085 &[3, 4, 4],
1086 px(90.),
1087 );
1088 }
1089
1090 #[test]
1091 fn test_update_run_after_truncation_end() {
1092 fn perform_test(result: &str, run_lens: &[usize], result_run_lens: &[usize]) {
1093 let mut dummy_runs = generate_test_runs(run_lens);
1094 update_runs_after_truncation(result, "…", &mut dummy_runs, TruncateFrom::End);
1095 for (run, result_len) in dummy_runs.iter().zip(result_run_lens) {
1096 assert_eq!(run.len, *result_len);
1097 }
1098 }
1099 perform_test("abcd…", &[12], &[7]);
1106 perform_test("abcdef…", &[4, 4, 4], &[4, 5]);
1114 perform_test("abcdefgh…", &[4, 4, 4], &[4, 4, 3]);
1122 }
1123
1124 #[test]
1125 fn test_is_word_char() {
1126 #[track_caller]
1127 fn assert_word(word: &str) {
1128 for c in word.chars() {
1129 assert!(
1130 LineWrapper::is_word_char(c),
1131 "assertion failed for '{}' (unicode 0x{:x})",
1132 c,
1133 c as u32
1134 );
1135 }
1136 }
1137
1138 #[track_caller]
1139 fn assert_not_word(word: &str) {
1140 let found = word.chars().any(|c| !LineWrapper::is_word_char(c));
1141 assert!(found, "assertion failed for '{}'", word);
1142 }
1143
1144 assert_word("Hello123");
1145 assert_word("non-English");
1146 assert_word("var_name");
1147 assert_word("123456");
1148 assert_word("3.1415");
1149 assert_word("10^2");
1150 assert_word("1~2");
1151 assert_word("100%");
1152 assert_word("@mention");
1153 assert_word("#hashtag");
1154 assert_word("$variable");
1155 assert_word("a=1");
1156 assert_word("Self::is_word_char");
1157 assert_word("on;");
1158 assert_word("more⋯");
1159 assert_word("won’t");
1160 assert_word("‘twas");
1161 assert_word("plz!");
1162 assert_word("see)");
1163 assert_word("quoted”");
1164 assert_word("well…");
1165
1166 assert_not_word("foo bar");
1168
1169 assert_word("github.com");
1171 assert_not_word("zed-industries/zed");
1172 assert_not_word("zed-industries\\zed");
1173 assert_not_word("a=1&b=2");
1174 assert_not_word("foo?b=2");
1175
1176 assert_word("ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏ");
1178 assert_word("ĀāĂ㥹ĆćĈĉĊċČčĎď");
1180 assert_word("ƀƁƂƃƄƅƆƇƈƉƊƋƌƍƎƏ");
1182 assert_word("АБВГДЕЖЗИЙКЛМНОП");
1184 assert_word("ThậmchíđếnkhithuachạychúngcònnhẫntâmgiếtnốtsốđôngtùchínhtrịởYênBáivàCaoBằng");
1186 assert_word("গিয়েছিলেন");
1188 assert_word("ছেলে");
1189 assert_word("হচ্ছিল");
1190
1191 assert_not_word("你好");
1193 assert_not_word("안녕하세요");
1194 assert_not_word("こんにちは");
1195 assert_not_word("😀😁😂");
1196 assert_not_word("()[]{}<>");
1197
1198 assert_word("\u{202F}"); assert_word("\u{00A0}"); assert_word("\u{2011}"); }
1204
1205 #[cfg(target_os = "macos")]
1207 use crate as gpui;
1208
1209 #[cfg(target_os = "macos")]
1211 #[crate::test]
1212 fn test_wrap_shaped_line(cx: &mut TestAppContext) {
1213 cx.update(|cx| {
1214 let text_system = WindowTextSystem::new(cx.text_system().clone());
1215
1216 let normal = TextRun {
1217 len: 0,
1218 font: font("Helvetica"),
1219 color: Default::default(),
1220 underline: Default::default(),
1221 ..Default::default()
1222 };
1223 let bold = TextRun {
1224 len: 0,
1225 font: font("Helvetica").bold(),
1226 ..Default::default()
1227 };
1228
1229 let text = "aa bbb cccc ddddd eeee".into();
1230 let lines = text_system
1231 .shape_text(
1232 text,
1233 px(16.),
1234 &[
1235 normal.with_len(4),
1236 bold.with_len(5),
1237 normal.with_len(6),
1238 bold.with_len(1),
1239 normal.with_len(7),
1240 ],
1241 Some(px(72.)),
1242 None,
1243 )
1244 .unwrap();
1245
1246 assert_eq!(
1247 lines[0].layout.wrap_boundaries(),
1248 &[
1249 WrapBoundary {
1250 run_ix: 0,
1251 glyph_ix: 7
1252 },
1253 WrapBoundary {
1254 run_ix: 0,
1255 glyph_ix: 12
1256 },
1257 WrapBoundary {
1258 run_ix: 0,
1259 glyph_ix: 18
1260 }
1261 ],
1262 );
1263 });
1264 }
1265
1266 #[test]
1267 fn test_multiline_truncation_fits_within_wrapped_lines() {
1268 let mut wrapper = build_wrapper();
1269
1270 let text: &str = "aa bbbbbb cccccc dddddd eeee ffff";
1282 let wrap_width = px(72.);
1283 let max_lines: usize = 2;
1284
1285 let runs = generate_test_runs(&[text.len()]);
1286 let (truncated, _) = wrapper.truncate_wrapped_line(
1287 text.into(),
1288 wrap_width,
1289 max_lines,
1290 "\u{2026}",
1291 &runs,
1292 TruncateFrom::End,
1293 );
1294
1295 let wrap_count = wrapper
1297 .wrap_line(&[LineFragment::text(&truncated)], wrap_width)
1298 .count();
1299
1300 assert!(
1301 wrap_count < max_lines,
1302 "Truncated text '{}' wraps into {} visual lines, expected at most {}",
1303 truncated,
1304 wrap_count + 1,
1305 max_lines
1306 );
1307
1308 assert!(
1310 truncated.ends_with('\u{2026}'),
1311 "Truncated text '{}' should end with ellipsis",
1312 truncated
1313 );
1314 }
1315
1316 #[test]
1317 fn test_multiline_truncation_no_truncation_needed() {
1318 let mut wrapper = build_wrapper();
1319
1320 let text: &str = "aa bbb cccccc";
1323 let wrap_width = px(72.);
1324 let max_lines: usize = 2;
1325
1326 let runs = generate_test_runs(&[text.len()]);
1327 let (result, _) = wrapper.truncate_wrapped_line(
1328 text.into(),
1329 wrap_width,
1330 max_lines,
1331 "\u{2026}",
1332 &runs,
1333 TruncateFrom::End,
1334 );
1335
1336 assert_eq!(
1337 result.as_ref(),
1338 text,
1339 "Text that fits should not be modified"
1340 );
1341 }
1342
1343 #[test]
1344 fn test_multiline_truncation_three_lines() {
1345 let mut wrapper = build_wrapper();
1346
1347 let text: &str = "aa bbb cccc ddddd eeee ffff gggg hhhh iiii jjjj";
1348 let wrap_width = px(72.);
1349 let max_lines: usize = 3;
1350
1351 let runs = generate_test_runs(&[text.len()]);
1352 let (truncated, _) = wrapper.truncate_wrapped_line(
1353 text.into(),
1354 wrap_width,
1355 max_lines,
1356 "\u{2026}",
1357 &runs,
1358 TruncateFrom::End,
1359 );
1360
1361 let wrap_count = wrapper
1362 .wrap_line(&[LineFragment::text(&truncated)], wrap_width)
1363 .count();
1364
1365 assert!(
1366 wrap_count < max_lines,
1367 "Truncated text '{}' wraps into {} visual lines, expected at most {}",
1368 truncated,
1369 wrap_count + 1,
1370 max_lines
1371 );
1372
1373 assert!(
1374 truncated.ends_with('\u{2026}'),
1375 "Truncated text '{}' should end with ellipsis",
1376 truncated
1377 );
1378 }
1379
1380 #[test]
1381 fn test_multiline_truncation_with_newlines() {
1382 let mut wrapper = build_wrapper();
1383
1384 let text: &str = "hello\nworld foo bar baz";
1389 let wrap_width = px(72.);
1390 let max_lines: usize = 2;
1391
1392 let runs = generate_test_runs(&[text.len()]);
1393 let (truncated, _) = wrapper.truncate_wrapped_line(
1394 text.into(),
1395 wrap_width,
1396 max_lines,
1397 "\u{2026}",
1398 &runs,
1399 TruncateFrom::End,
1400 );
1401
1402 let parts: Vec<&str> = truncated.splitn(2, '\n').collect();
1404 assert_eq!(
1405 parts.len(),
1406 2,
1407 "Newline should be preserved: '{}'",
1408 truncated
1409 );
1410 assert_eq!(parts[0], "hello");
1411
1412 let second_line_width: Pixels = parts[1].chars().map(|c| wrapper.width_for_char(c)).sum();
1414 assert!(
1415 second_line_width <= wrap_width,
1416 "Second line '{}' ({}px) exceeds wrap_width ({}px)",
1417 parts[1],
1418 second_line_width,
1419 wrap_width
1420 );
1421 assert!(
1422 truncated.ends_with('\u{2026}'),
1423 "Should end with ellipsis: '{}'",
1424 truncated
1425 );
1426 }
1427
1428 #[test]
1429 fn test_multiline_truncation_newline_on_last_line() {
1430 let mut wrapper = build_wrapper();
1431
1432 let text: &str = "hello\nworld\nmore";
1436 let wrap_width = px(72.);
1437 let max_lines: usize = 2;
1438
1439 let runs = generate_test_runs(&[text.len()]);
1440 let (truncated, _) = wrapper.truncate_wrapped_line(
1441 text.into(),
1442 wrap_width,
1443 max_lines,
1444 "\u{2026}",
1445 &runs,
1446 TruncateFrom::End,
1447 );
1448
1449 let parts: Vec<&str> = truncated.splitn(2, '\n').collect();
1450 assert_eq!(parts[0], "hello");
1451 assert!(
1452 truncated.ends_with('\u{2026}'),
1453 "Should end with ellipsis since there's more content: '{}'",
1454 truncated
1455 );
1456 }
1457
1458 #[test]
1459 fn test_truncate_line_middle() {
1460 let mut wrapper = build_wrapper();
1461
1462 let short_text = "hello world";
1464 let runs = generate_test_runs(&[short_text.len()]);
1465 let (result, result_runs) = wrapper.truncate_line(
1466 short_text.into(),
1467 px(10000.),
1468 "…",
1469 &runs,
1470 TruncateFrom::Middle,
1471 );
1472 assert_eq!(result.as_ref(), short_text);
1473 assert_eq!(result_runs.len(), 1);
1474 assert_eq!(result_runs[0].len, short_text.len());
1475
1476 let long_text = "abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz";
1478 let runs = generate_test_runs(&[long_text.len()]);
1479 let (result, _result_runs) =
1480 wrapper.truncate_line(long_text.into(), px(100.), "…", &runs, TruncateFrom::Middle);
1481 assert!(
1482 result.contains('…'),
1483 "Middle-truncated result should contain '…', got: '{}'",
1484 result
1485 );
1486 assert!(
1487 result.chars().count() < long_text.chars().count(),
1488 "Middle-truncated result should be shorter than original"
1489 );
1490 assert_eq!(
1491 result.chars().next(),
1492 long_text.chars().next(),
1493 "Result should start with the same first character as original"
1494 );
1495 assert_eq!(
1496 result.chars().last(),
1497 long_text.chars().last(),
1498 "Result should end with the same last character as original"
1499 );
1500
1501 let text = "abcdef";
1504 let runs = generate_test_runs(&[text.len()]);
1505 let (result, result_runs) =
1506 wrapper.truncate_line(text.into(), px(1.), "…", &runs, TruncateFrom::Middle);
1507 assert_eq!(result.as_ref(), "…");
1508 assert_eq!(result_runs.len(), 1);
1509 assert_eq!(result_runs[0].len, "…".len());
1510
1511 let multi_run_text = "abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz";
1514 let run_lens = [20, 20, multi_run_text.len() - 40];
1515 let runs = generate_test_runs(&run_lens);
1516 let (result, result_runs) = wrapper.truncate_line(
1517 multi_run_text.into(),
1518 px(100.),
1519 "…",
1520 &runs,
1521 TruncateFrom::Middle,
1522 );
1523 let total_run_len: usize = result_runs.iter().map(|r| r.len).sum();
1524 assert_eq!(
1525 total_run_len,
1526 result.len(),
1527 "Sum of run lengths ({}) should equal result byte length ({})",
1528 total_run_len,
1529 result.len()
1530 );
1531 }
1532
1533 #[test]
1534 fn test_multiline_truncation_trailing_newline() {
1535 let mut wrapper = build_wrapper();
1536
1537 let text: &str = "hello\nworld\n";
1540 let wrap_width = px(72.);
1541 let max_lines: usize = 2;
1542
1543 let runs = generate_test_runs(&[text.len()]);
1544 let (result, _) = wrapper.truncate_wrapped_line(
1545 text.into(),
1546 wrap_width,
1547 max_lines,
1548 "\u{2026}",
1549 &runs,
1550 TruncateFrom::End,
1551 );
1552
1553 assert!(
1554 !result.ends_with('\u{2026}'),
1555 "Trailing newline with no content should not add ellipsis: '{}'",
1556 result
1557 );
1558 }
1559
1560 #[test]
1561 fn test_multiline_truncation_newline_fits_exactly() {
1562 let mut wrapper = build_wrapper();
1563
1564 let text: &str = "hello\nworld";
1567 let wrap_width = px(72.);
1568 let max_lines: usize = 2;
1569
1570 let runs = generate_test_runs(&[text.len()]);
1571 let (result, _) = wrapper.truncate_wrapped_line(
1572 text.into(),
1573 wrap_width,
1574 max_lines,
1575 "\u{2026}",
1576 &runs,
1577 TruncateFrom::End,
1578 );
1579
1580 assert_eq!(
1581 result.as_ref(),
1582 text,
1583 "Text that fits exactly should not be modified: '{}'",
1584 result
1585 );
1586 }
1587}