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