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
16#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
18pub enum IndentAdjustment {
19 NoIndent,
21 #[default]
23 SameIndent,
24 ExtraColumns(u32),
26}
27
28pub struct LineWrapper {
30 text_system: Arc<TextSystem>,
31 pub(crate) font_id: FontId,
32 pub(crate) font_size: Pixels,
33 char_widths: CharWidthCache,
34}
35
36pub(crate) struct CharWidthCache {
38 ascii: [Option<Pixels>; 128],
39 other: HashMap<char, Pixels>,
40}
41
42impl Default for CharWidthCache {
43 fn default() -> Self {
44 Self {
45 ascii: [None; 128],
46 other: HashMap::default(),
47 }
48 }
49}
50
51impl LineWrapper {
52 pub const MAX_INDENT: u32 = 256;
54
55 pub(crate) fn new(
56 font_id: FontId,
57 font_size: Pixels,
58 text_system: Arc<TextSystem>,
59 char_widths: CharWidthCache,
60 ) -> Self {
61 Self {
62 text_system,
63 font_id,
64 font_size,
65 char_widths,
66 }
67 }
68
69 pub(crate) fn into_char_widths(self) -> CharWidthCache {
70 self.char_widths
71 }
72
73 pub fn wrap_line<'a>(
75 &'a mut self,
76 fragments: &'a [LineFragment],
77 wrap_width: Pixels,
78 indent_adjustment: IndentAdjustment,
79 ) -> impl Iterator<Item = Boundary> + 'a {
80 let mut width = px(0.);
81 let mut first_non_whitespace_ix = None;
82 let mut base_indent = None;
83 let mut last_candidate_ix = 0;
84 let mut last_candidate_width = px(0.);
85 let mut last_wrap_ix = 0;
86 let mut prev_c = '\0';
87 let mut index = 0;
88 let mut candidates = fragments
89 .iter()
90 .flat_map(move |fragment| fragment.wrap_boundary_candidates())
91 .peekable();
92 iter::from_fn(move || {
93 for candidate in candidates.by_ref() {
94 let ix = index;
95 index += candidate.len_utf8();
96 let mut new_prev_c = prev_c;
97 let item_width = match candidate {
98 WrapBoundaryCandidate::Char { character: c } => {
99 if c == '\n' {
100 continue;
101 }
102
103 if Self::is_word_char(c) {
104 if prev_c == ' ' && c != ' ' && first_non_whitespace_ix.is_some() {
105 last_candidate_ix = ix;
106 last_candidate_width = width;
107 }
108 } else {
109 if c != ' ' && first_non_whitespace_ix.is_some() {
111 last_candidate_ix = ix;
112 last_candidate_width = width;
113 }
114 }
115
116 if c != ' ' && first_non_whitespace_ix.is_none() {
117 first_non_whitespace_ix = Some(ix);
118 }
119
120 new_prev_c = c;
121
122 self.width_for_char(c)
123 }
124 WrapBoundaryCandidate::Element {
125 width: element_width,
126 ..
127 } => {
128 if prev_c == ' ' && first_non_whitespace_ix.is_some() {
129 last_candidate_ix = ix;
130 last_candidate_width = width;
131 }
132
133 if first_non_whitespace_ix.is_none() {
134 first_non_whitespace_ix = Some(ix);
135 }
136
137 element_width
138 }
139 };
140
141 width += item_width;
142 if width > wrap_width && ix > last_wrap_ix {
143 let wrap_at_candidate =
144 last_candidate_ix > 0 && width - last_candidate_width <= wrap_width;
145
146 let carried_width = if wrap_at_candidate {
147 width - last_candidate_width
148 } else {
149 item_width
150 };
151
152 if base_indent.is_none() {
158 if let Some(first_non_whitespace_ix) = first_non_whitespace_ix {
159 base_indent =
160 Some(Self::MAX_INDENT.min(
161 first_non_whitespace_ix.saturating_sub(last_wrap_ix) as u32,
162 ));
163 }
164 }
165
166 let next_indent = match indent_adjustment {
167 IndentAdjustment::NoIndent => 0,
168 IndentAdjustment::SameIndent => base_indent.unwrap_or(0),
169 IndentAdjustment::ExtraColumns(extra) => {
170 if let Some(base_indent) = base_indent {
171 let candidate = base_indent.saturating_add(extra);
172 let candidate_indent_width =
173 self.width_for_char(' ') * candidate as f32;
174 let min_headroom =
178 carried_width.max(self.width_for_char(' ') * 2.0);
179 if candidate_indent_width + min_headroom > wrap_width {
180 0
181 } else {
182 Self::MAX_INDENT.min(candidate)
183 }
184 } else {
185 0
186 }
187 }
188 };
189
190 if wrap_at_candidate {
191 last_wrap_ix = last_candidate_ix;
192 width -= last_candidate_width;
193 } else {
194 last_wrap_ix = ix;
195 width = item_width;
196 }
197 last_candidate_ix = 0;
198
199 width += self.width_for_char(' ') * next_indent as f32;
200
201 return Some(Boundary::new(last_wrap_ix, next_indent));
202 }
203
204 prev_c = new_prev_c;
205 }
206
207 None
208 })
209 }
210
211 pub fn should_truncate_line(
215 &mut self,
216 line: &str,
217 truncate_width: Pixels,
218 truncation_affix: &str,
219 truncate_from: TruncateFrom,
220 ) -> Option<usize> {
221 let mut width = px(0.);
222 let suffix_width = truncation_affix
223 .chars()
224 .map(|c| self.width_for_char(c))
225 .fold(px(0.0), |a, x| a + x);
226 let mut truncate_ix = 0;
227
228 match truncate_from {
229 TruncateFrom::Start => {
230 for (ix, c) in line.char_indices().rev() {
231 if width + suffix_width < truncate_width {
232 truncate_ix = ix;
233 }
234
235 let char_width = self.width_for_char(c);
236 width += char_width;
237
238 if width.floor() > truncate_width {
239 return Some(truncate_ix);
240 }
241 }
242 }
243 TruncateFrom::End => {
244 for (ix, c) in line.char_indices() {
245 if width + suffix_width < truncate_width {
246 truncate_ix = ix;
247 }
248
249 let char_width = self.width_for_char(c);
250 width += char_width;
251
252 if width.floor() > truncate_width {
253 return Some(truncate_ix);
254 }
255 }
256 }
257 TruncateFrom::Middle => {}
258 }
259
260 None
261 }
262
263 fn should_truncate_line_middle(
264 &mut self,
265 line: &str,
266 truncate_width: Pixels,
267 truncation_affix: &str,
268 ) -> Option<(usize, usize)> {
269 let suffix_width = truncation_affix
270 .chars()
271 .map(|c| self.width_for_char(c))
272 .fold(px(0.0), |a, x| a + x);
273
274 let total_width: Pixels = line
275 .chars()
276 .map(|c| self.width_for_char(c))
277 .fold(px(0.0), |a, x| a + x);
278
279 if total_width <= truncate_width {
280 return None;
281 }
282
283 let content_budget = truncate_width - suffix_width;
284 if content_budget <= px(0.) {
285 return Some((0, line.len()));
286 }
287
288 let front_budget = content_budget * (2.0 / 3.0);
289 let back_budget = content_budget - front_budget;
290
291 let mut front_width = px(0.);
292 let mut front_end_ix = 0usize;
293 for (ix, c) in line.char_indices() {
294 let char_width = self.width_for_char(c);
295 if front_width + char_width > front_budget {
296 break;
297 }
298 front_width += char_width;
299 front_end_ix = ix + c.len_utf8();
300 }
301
302 let mut back_width = px(0.);
303 let mut back_start_ix = line.len();
304 for (ix, c) in line.char_indices().rev() {
305 let char_width = self.width_for_char(c);
306 if back_width + char_width > back_budget {
307 break;
308 }
309 back_width += char_width;
310 back_start_ix = ix;
311 }
312
313 if front_end_ix >= back_start_ix {
314 return Some((0, line.len()));
315 }
316
317 Some((front_end_ix, back_start_ix))
318 }
319
320 pub fn truncate_line<'a>(
322 &mut self,
323 line: SharedString,
324 truncate_width: Pixels,
325 truncation_affix: &str,
326 runs: &'a [TextRun],
327 truncate_from: TruncateFrom,
328 ) -> (SharedString, Cow<'a, [TextRun]>) {
329 if truncate_from == TruncateFrom::Middle {
330 if let Some((front_end_ix, back_start_ix)) =
331 self.should_truncate_line_middle(&line, truncate_width, truncation_affix)
332 {
333 let result = SharedString::from(format!(
334 "{}{truncation_affix}{}",
335 &line[..front_end_ix],
336 &line[back_start_ix..]
337 ));
338 let mut runs = runs.to_vec();
339 update_runs_after_middle_truncation(
340 truncation_affix,
341 &mut runs,
342 front_end_ix,
343 back_start_ix,
344 );
345 return (result, Cow::Owned(runs));
346 } else {
347 return (line, Cow::Borrowed(runs));
348 }
349 }
350
351 if let Some(truncate_ix) =
352 self.should_truncate_line(&line, truncate_width, truncation_affix, truncate_from)
353 {
354 let result = match truncate_from {
355 TruncateFrom::Start => SharedString::from(format!(
356 "{truncation_affix}{}",
357 &line[line.ceil_char_boundary(truncate_ix + 1)..]
358 )),
359 TruncateFrom::End => SharedString::from(format!(
360 "{}{truncation_affix}",
361 line[..truncate_ix]
362 .trim_end_matches(|c: char| c.is_whitespace() || c.is_ascii_punctuation())
363 )),
364 TruncateFrom::Middle => unreachable!("Middle truncation is handled above"),
365 };
366 let mut runs = runs.to_vec();
367 update_runs_after_truncation(&result, truncation_affix, &mut runs, truncate_from);
368 (result, Cow::Owned(runs))
369 } else {
370 (line, Cow::Borrowed(runs))
371 }
372 }
373
374 pub fn truncate_wrapped_line<'a>(
384 &mut self,
385 text: SharedString,
386 wrap_width: Pixels,
387 max_lines: usize,
388 truncation_affix: &str,
389 runs: &'a [TextRun],
390 truncate_from: TruncateFrom,
391 ) -> (SharedString, Cow<'a, [TextRun]>) {
392 if max_lines <= 1 || truncate_from == TruncateFrom::Start {
393 return self.truncate_line(
394 text,
395 wrap_width * max_lines,
396 truncation_affix,
397 runs,
398 truncate_from,
399 );
400 }
401 if truncate_from == TruncateFrom::Middle {
402 return self.truncate_line(text, wrap_width, truncation_affix, runs, truncate_from);
403 }
404
405 let affix_width: Pixels = truncation_affix
406 .chars()
407 .map(|c| self.width_for_char(c))
408 .sum();
409
410 let mut width = px(0.);
411 let mut line = 0usize;
412 let mut first_non_whitespace_ix = None;
413 let mut last_candidate_ix = 0usize;
414 let mut last_candidate_width = px(0.);
415 let mut last_wrap_ix = 0usize;
416 let mut prev_c = '\0';
417 let mut indent: Option<u32> = None;
418 let mut truncate_ix = 0usize;
419
420 for (ix, c) in text.char_indices() {
421 if c == '\n' {
422 if line >= max_lines - 1 && !text[ix + 1..].trim().is_empty() {
423 let truncated = text[..truncate_ix]
426 .trim_end_matches(|c: char| c.is_whitespace() || c.is_ascii_punctuation());
427 let result = SharedString::from(format!("{truncated}{truncation_affix}"));
428 let mut runs = runs.to_vec();
429 update_runs_after_truncation(
430 &result,
431 truncation_affix,
432 &mut runs,
433 TruncateFrom::End,
434 );
435 return (result, Cow::Owned(runs));
436 }
437
438 line += 1;
440 width = px(0.);
441 first_non_whitespace_ix = None;
442 last_candidate_ix = 0;
443 last_candidate_width = px(0.);
444 last_wrap_ix = ix + 1;
445 prev_c = '\0';
446 indent = None;
447 truncate_ix = ix + 1;
448 continue;
449 }
450
451 let char_width = self.width_for_char(c);
452
453 if Self::is_word_char(c) {
454 if prev_c == ' ' && first_non_whitespace_ix.is_some() {
455 last_candidate_ix = ix;
456 last_candidate_width = width;
457 }
458 } else if c != ' ' && first_non_whitespace_ix.is_some() {
459 last_candidate_ix = ix;
460 last_candidate_width = width;
461 }
462
463 if c != ' ' && first_non_whitespace_ix.is_none() {
464 first_non_whitespace_ix = Some(ix);
465 }
466
467 width += char_width;
468
469 if line < max_lines - 1 {
470 if width > wrap_width && ix > last_wrap_ix {
472 if let (None, Some(first_nw)) = (indent, first_non_whitespace_ix) {
473 indent = Some(Self::MAX_INDENT.min((first_nw - last_wrap_ix) as u32));
474 }
475
476 if last_candidate_ix > last_wrap_ix {
477 last_wrap_ix = last_candidate_ix;
478 width -= last_candidate_width;
479 last_candidate_ix = 0;
480 } else {
481 last_wrap_ix = ix;
482 width = char_width;
483 }
484
485 if let Some(ind) = indent {
486 width += self.width_for_char(' ') * ind as f32;
487 }
488
489 line += 1;
490 truncate_ix = last_wrap_ix;
491 }
492 } else {
493 if width + affix_width <= wrap_width {
496 truncate_ix = ix + c.len_utf8();
497 }
498
499 if width > wrap_width {
500 let truncated = text[..truncate_ix]
501 .trim_end_matches(|c: char| c.is_whitespace() || c.is_ascii_punctuation());
502 let result = SharedString::from(format!("{truncated}{truncation_affix}"));
503 let mut runs = runs.to_vec();
504 update_runs_after_truncation(
505 &result,
506 truncation_affix,
507 &mut runs,
508 TruncateFrom::End,
509 );
510 return (result, Cow::Owned(runs));
511 }
512 }
513
514 prev_c = c;
515 }
516
517 (text, Cow::Borrowed(runs))
519 }
520
521 pub(crate) fn is_word_char(c: char) -> bool {
524 c.is_ascii_alphanumeric() ||
526 matches!(c, '\u{00C0}'..='\u{00FF}') ||
530 matches!(c, '\u{0100}'..='\u{017F}') ||
533 matches!(c, '\u{0180}'..='\u{024F}') ||
536 matches!(c, '\u{0400}'..='\u{04FF}') ||
539
540 matches!(c, '\u{1E00}'..='\u{1EFF}') || matches!(c, '\u{0300}'..='\u{036F}') || matches!(c, '\u{0980}'..='\u{09FF}') ||
546
547 matches!(c, '-' | '_' | '.' | '\'' | '’' | '‘' | '$' | '%' | '@' | '#' | '^' | '~' | ',' | '=' | ':' | ';') ||
552 matches!(c, '!' | ')' | ']' | '}' | '"' | '”' | '»' | '…') ||
558 matches!(c, '⋯') ||
560
561 matches!(c, '\u{202F}' | '\u{00A0}' | '\u{2011}')
563 }
564
565 #[inline(always)]
566 fn width_for_char(&mut self, c: char) -> Pixels {
567 if (c as u32) < 128 {
568 if let Some(cached_width) = self.char_widths.ascii[c as usize] {
569 cached_width
570 } else {
571 let width = self
572 .text_system
573 .layout_width(self.font_id, self.font_size, c);
574 self.char_widths.ascii[c as usize] = Some(width);
575 width
576 }
577 } else if let Some(cached_width) = self.char_widths.other.get(&c) {
578 *cached_width
579 } else {
580 let width = self
581 .text_system
582 .layout_width(self.font_id, self.font_size, c);
583 self.char_widths.other.insert(c, width);
584 width
585 }
586 }
587}
588
589fn update_runs_after_truncation(
590 result: &str,
591 ellipsis: &str,
592 runs: &mut Vec<TextRun>,
593 truncate_from: TruncateFrom,
594) {
595 let mut truncate_at = result.len() - ellipsis.len();
596 match truncate_from {
597 TruncateFrom::Start => {
598 for (run_index, run) in runs.iter_mut().enumerate().rev() {
599 if run.len <= truncate_at {
600 truncate_at -= run.len;
601 } else {
602 run.len = truncate_at + ellipsis.len();
603 runs.splice(..run_index, std::iter::empty());
604 break;
605 }
606 }
607 }
608 TruncateFrom::End => {
609 for (run_index, run) in runs.iter_mut().enumerate() {
610 if run.len <= truncate_at {
611 truncate_at -= run.len;
612 } else {
613 run.len = truncate_at + ellipsis.len();
614 runs.truncate(run_index + 1);
615 break;
616 }
617 }
618 }
619 TruncateFrom::Middle => {
620 unreachable!("Middle truncation calls this function with TruncateFrom::End directly")
621 }
622 }
623}
624
625fn update_runs_after_middle_truncation(
626 ellipsis: &str,
627 runs: &mut Vec<TextRun>,
628 front_end_ix: usize,
629 back_start_ix: usize,
630) {
631 let original_runs = std::mem::take(runs);
632 let mut result_runs: Vec<TextRun> = Vec::with_capacity(original_runs.len());
633
634 let mut front_remaining = front_end_ix;
638 let mut front_done = false;
639 for run in &original_runs {
640 if front_done {
641 break;
642 }
643 if run.len <= front_remaining {
644 result_runs.push(run.clone());
645 front_remaining -= run.len;
646 } else {
647 let mut partial = run.clone();
648 partial.len = front_remaining + ellipsis.len();
649 result_runs.push(partial);
650 front_done = true;
651 }
652 }
653 if !front_done {
654 if let Some(last) = result_runs.last_mut() {
657 last.len += ellipsis.len();
658 } else if let Some(first) = original_runs.first() {
659 let mut affix_run = first.clone();
660 affix_run.len = ellipsis.len();
661 result_runs.push(affix_run);
662 }
663 }
664
665 let mut byte_pos = 0usize;
668 for run in &original_runs {
669 let run_end = byte_pos + run.len;
670 if run_end > back_start_ix {
671 if byte_pos < back_start_ix {
672 let mut partial = run.clone();
674 partial.len = run_end - back_start_ix;
675 result_runs.push(partial);
676 } else {
677 result_runs.push(run.clone());
678 }
679 }
680 byte_pos = run_end;
681 }
682
683 *runs = result_runs;
684}
685
686pub enum LineFragment<'a> {
688 Text {
690 text: &'a str,
692 },
693 Element {
695 width: Pixels,
697 len_utf8: usize,
699 },
700}
701
702impl<'a> LineFragment<'a> {
703 pub fn text(text: &'a str) -> Self {
705 LineFragment::Text { text }
706 }
707
708 pub fn element(width: Pixels, len_utf8: usize) -> Self {
710 LineFragment::Element { width, len_utf8 }
711 }
712
713 fn wrap_boundary_candidates(&self) -> impl Iterator<Item = WrapBoundaryCandidate> {
714 let text = match self {
715 LineFragment::Text { text } => text,
716 LineFragment::Element { .. } => "\0",
717 };
718 text.chars().map(move |character| {
719 if let LineFragment::Element { width, len_utf8 } = self {
720 WrapBoundaryCandidate::Element {
721 width: *width,
722 len_utf8: *len_utf8,
723 }
724 } else {
725 WrapBoundaryCandidate::Char { character }
726 }
727 })
728 }
729}
730
731enum WrapBoundaryCandidate {
732 Char { character: char },
733 Element { width: Pixels, len_utf8: usize },
734}
735
736impl WrapBoundaryCandidate {
737 pub fn len_utf8(&self) -> usize {
738 match self {
739 WrapBoundaryCandidate::Char { character } => character.len_utf8(),
740 WrapBoundaryCandidate::Element { len_utf8: len, .. } => *len,
741 }
742 }
743}
744
745#[derive(Copy, Clone, Debug, PartialEq, Eq)]
747pub struct Boundary {
748 pub ix: usize,
750 pub next_indent: u32,
752}
753
754impl Boundary {
755 fn new(ix: usize, next_indent: u32) -> Self {
756 Self { ix, next_indent }
757 }
758}
759
760#[cfg(test)]
761mod tests {
762 use super::*;
763 use crate::{Font, FontFeatures, FontStyle, FontWeight, TestAppContext, TestDispatcher, font};
764 #[cfg(target_os = "macos")]
765 use crate::{TextRun, WindowTextSystem, WrapBoundary};
766
767 fn build_wrapper() -> LineWrapper {
768 let dispatcher = TestDispatcher::new(0);
769 let cx = TestAppContext::build(dispatcher, None);
770 let id = cx.text_system().resolve_font(&font(".ZedMono"));
771 LineWrapper::new(
772 id,
773 px(16.),
774 cx.text_system().clone(),
775 CharWidthCache::default(),
776 )
777 }
778
779 fn generate_test_runs(input_run_len: &[usize]) -> Vec<TextRun> {
780 input_run_len
781 .iter()
782 .map(|run_len| TextRun {
783 len: *run_len,
784 font: Font {
785 family: "Dummy".into(),
786 features: FontFeatures::default(),
787 variations: Default::default(),
788 fallbacks: None,
789 weight: FontWeight::default(),
790 style: FontStyle::Normal,
791 },
792 ..Default::default()
793 })
794 .collect()
795 }
796
797 #[test]
798 fn test_wrap_line() {
799 let mut wrapper = build_wrapper();
800
801 assert_eq!(
802 wrapper
803 .wrap_line(
804 &[LineFragment::text("aa bbb cccc ddddd eeee")],
805 px(72.),
806 IndentAdjustment::default()
807 )
808 .collect::<Vec<_>>(),
809 &[
810 Boundary::new(7, 0),
811 Boundary::new(12, 0),
812 Boundary::new(18, 0)
813 ],
814 );
815 assert_eq!(
816 wrapper
817 .wrap_line(
818 &[LineFragment::text("aaa aaaaaaaaaaaaaaaaaa")],
819 px(72.0),
820 IndentAdjustment::default()
821 )
822 .collect::<Vec<_>>(),
823 &[
824 Boundary::new(4, 0),
825 Boundary::new(11, 0),
826 Boundary::new(18, 0)
827 ],
828 );
829 assert_eq!(
830 wrapper
831 .wrap_line(
832 &[LineFragment::text(" aaaaaaa")],
833 px(72.),
834 IndentAdjustment::default()
835 )
836 .collect::<Vec<_>>(),
837 &[
838 Boundary::new(7, 5),
839 Boundary::new(9, 5),
840 Boundary::new(11, 5),
841 ]
842 );
843 assert_eq!(
844 wrapper
845 .wrap_line(
846 &[LineFragment::text(" ")],
847 px(72.),
848 IndentAdjustment::default()
849 )
850 .collect::<Vec<_>>(),
851 &[
852 Boundary::new(7, 0),
853 Boundary::new(14, 0),
854 Boundary::new(21, 0)
855 ]
856 );
857 assert_eq!(
858 wrapper
859 .wrap_line(
860 &[LineFragment::text(" aaaaaaaaaaaaaa")],
861 px(72.),
862 IndentAdjustment::default()
863 )
864 .collect::<Vec<_>>(),
865 &[
866 Boundary::new(7, 0),
867 Boundary::new(14, 3),
868 Boundary::new(18, 3),
869 Boundary::new(22, 3),
870 ]
871 );
872
873 assert_eq!(
875 wrapper
876 .wrap_line(
877 &[
878 LineFragment::text("aa bbb "),
879 LineFragment::text("cccc ddddd eeee")
880 ],
881 px(72.),
882 IndentAdjustment::default()
883 )
884 .collect::<Vec<_>>(),
885 &[
886 Boundary::new(7, 0),
887 Boundary::new(12, 0),
888 Boundary::new(18, 0)
889 ],
890 );
891
892 assert_eq!(
894 wrapper
895 .wrap_line(
896 &[
897 LineFragment::text("aa "),
898 LineFragment::element(px(20.), 1),
899 LineFragment::text(" bbb "),
900 LineFragment::element(px(30.), 1),
901 LineFragment::text(" cccc")
902 ],
903 px(72.),
904 IndentAdjustment::default()
905 )
906 .collect::<Vec<_>>(),
907 &[
908 Boundary::new(5, 0),
909 Boundary::new(9, 0),
910 Boundary::new(11, 0)
911 ],
912 );
913
914 assert_eq!(
916 wrapper
917 .wrap_line(
918 &[
919 LineFragment::element(px(50.), 1),
920 LineFragment::text(" aaaa bbbb cccc dddd")
921 ],
922 px(72.),
923 IndentAdjustment::default()
924 )
925 .collect::<Vec<_>>(),
926 &[
927 Boundary::new(2, 0),
928 Boundary::new(7, 0),
929 Boundary::new(12, 0),
930 Boundary::new(17, 0)
931 ],
932 );
933
934 assert_eq!(
936 wrapper
937 .wrap_line(
938 &[
939 LineFragment::text("short text "),
940 LineFragment::element(px(100.), 1),
941 LineFragment::text(" more text")
942 ],
943 px(72.),
944 IndentAdjustment::default()
945 )
946 .collect::<Vec<_>>(),
947 &[
948 Boundary::new(6, 0),
949 Boundary::new(11, 0),
950 Boundary::new(12, 0),
951 Boundary::new(18, 0)
952 ],
953 );
954
955 assert_eq!(
957 wrapper
958 .wrap_line(
959 &[LineFragment::text("a\u{202F}b\u{00A0}c\u{2011}d e")],
960 px(72.0),
961 IndentAdjustment::default()
962 )
963 .collect::<Vec<_>>(),
964 &[Boundary::new(12, 0),], );
966 }
967
968 #[test]
969 fn test_truncate_line_end() {
970 let mut wrapper = build_wrapper();
971
972 fn perform_test(
973 wrapper: &mut LineWrapper,
974 text: &'static str,
975 expected: &'static str,
976 ellipsis: &str,
977 ) {
978 let dummy_run_lens = vec![text.len()];
979 let dummy_runs = generate_test_runs(&dummy_run_lens);
980 let (result, dummy_runs) = wrapper.truncate_line(
981 text.into(),
982 px(220.),
983 ellipsis,
984 &dummy_runs,
985 TruncateFrom::End,
986 );
987 assert_eq!(result, expected);
988 assert_eq!(dummy_runs.first().unwrap().len, result.len());
989 }
990
991 perform_test(
992 &mut wrapper,
993 "aa bbb cccc ddddd eeee ffff gggg",
994 "aa bbb cccc ddddd eeee",
995 "",
996 );
997 perform_test(
998 &mut wrapper,
999 "aa bbb cccc ddddd eeee ffff gggg",
1000 "aa bbb cccc ddddd eee…",
1001 "…",
1002 );
1003 perform_test(
1004 &mut wrapper,
1005 "aa bbb cccc ddddd eeee ffff gggg",
1006 "aa bbb cccc dddd......",
1007 "......",
1008 );
1009 perform_test(
1010 &mut wrapper,
1011 "aa bbb cccc 🦀🦀🦀🦀🦀 eeee ffff gggg",
1012 "aa bbb cccc 🦀🦀🦀🦀…",
1013 "…",
1014 );
1015 }
1016
1017 #[test]
1018 fn test_truncate_line_start() {
1019 let mut wrapper = build_wrapper();
1020
1021 #[track_caller]
1022 fn perform_test(
1023 wrapper: &mut LineWrapper,
1024 text: &'static str,
1025 expected: &'static str,
1026 ellipsis: &str,
1027 ) {
1028 let dummy_run_lens = vec![text.len()];
1029 let dummy_runs = generate_test_runs(&dummy_run_lens);
1030 let (result, dummy_runs) = wrapper.truncate_line(
1031 text.into(),
1032 px(220.),
1033 ellipsis,
1034 &dummy_runs,
1035 TruncateFrom::Start,
1036 );
1037 assert_eq!(result, expected);
1038 assert_eq!(dummy_runs.first().unwrap().len, result.len());
1039 }
1040
1041 perform_test(
1042 &mut wrapper,
1043 "aaaa bbbb cccc ddddd eeee fff gg",
1044 "cccc ddddd eeee fff gg",
1045 "",
1046 );
1047 perform_test(
1048 &mut wrapper,
1049 "aaaa bbbb cccc ddddd eeee fff gg",
1050 "…ccc ddddd eeee fff gg",
1051 "…",
1052 );
1053 perform_test(
1054 &mut wrapper,
1055 "aaaa bbbb cccc ddddd eeee fff gg",
1056 "......dddd eeee fff gg",
1057 "......",
1058 );
1059 perform_test(
1060 &mut wrapper,
1061 "aaaa bbbb cccc 🦀🦀🦀🦀🦀 eeee fff gg",
1062 "…🦀🦀🦀🦀 eeee fff gg",
1063 "…",
1064 );
1065 }
1066
1067 #[test]
1068 fn test_truncate_multiple_runs_end() {
1069 let mut wrapper = build_wrapper();
1070
1071 fn perform_test(
1072 wrapper: &mut LineWrapper,
1073 text: &'static str,
1074 expected: &str,
1075 run_lens: &[usize],
1076 result_run_len: &[usize],
1077 line_width: Pixels,
1078 ) {
1079 let dummy_runs = generate_test_runs(run_lens);
1080 let (result, dummy_runs) =
1081 wrapper.truncate_line(text.into(), line_width, "…", &dummy_runs, TruncateFrom::End);
1082 assert_eq!(result, expected);
1083 for (run, result_len) in dummy_runs.iter().zip(result_run_len) {
1084 assert_eq!(run.len, *result_len);
1085 }
1086 }
1087 perform_test(&mut wrapper, "abcdefghijkl", "abcd…", &[12], &[7], px(50.));
1094 perform_test(
1102 &mut wrapper,
1103 "abcdefghijkl",
1104 "abcdef…",
1105 &[4, 4, 4],
1106 &[4, 5],
1107 px(70.),
1108 );
1109 perform_test(
1117 &mut wrapper,
1118 "abcdefghijkl",
1119 "abcdefgh…",
1120 &[4, 4, 4],
1121 &[4, 4, 3],
1122 px(90.),
1123 );
1124 }
1125
1126 #[test]
1127 fn test_truncate_multiple_runs_start() {
1128 let mut wrapper = build_wrapper();
1129
1130 #[track_caller]
1131 fn perform_test(
1132 wrapper: &mut LineWrapper,
1133 text: &'static str,
1134 expected: &str,
1135 run_lens: &[usize],
1136 result_run_len: &[usize],
1137 line_width: Pixels,
1138 ) {
1139 let dummy_runs = generate_test_runs(run_lens);
1140 let (result, dummy_runs) = wrapper.truncate_line(
1141 text.into(),
1142 line_width,
1143 "…",
1144 &dummy_runs,
1145 TruncateFrom::Start,
1146 );
1147 assert_eq!(result, expected);
1148 for (run, result_len) in dummy_runs.iter().zip(result_run_len) {
1149 assert_eq!(run.len, *result_len);
1150 }
1151 }
1152 perform_test(&mut wrapper, "abcdefghijkl", "…ijkl", &[12], &[7], px(50.));
1159 perform_test(
1167 &mut wrapper,
1168 "abcdefghijkl",
1169 "…ghijkl",
1170 &[4, 4, 4],
1171 &[5, 4],
1172 px(70.),
1173 );
1174 perform_test(
1182 &mut wrapper,
1183 "abcdefghijkl",
1184 "…efghijkl",
1185 &[4, 4, 4],
1186 &[3, 4, 4],
1187 px(90.),
1188 );
1189 }
1190
1191 #[test]
1192 fn test_update_run_after_truncation_end() {
1193 fn perform_test(result: &str, run_lens: &[usize], result_run_lens: &[usize]) {
1194 let mut dummy_runs = generate_test_runs(run_lens);
1195 update_runs_after_truncation(result, "…", &mut dummy_runs, TruncateFrom::End);
1196 for (run, result_len) in dummy_runs.iter().zip(result_run_lens) {
1197 assert_eq!(run.len, *result_len);
1198 }
1199 }
1200 perform_test("abcd…", &[12], &[7]);
1207 perform_test("abcdef…", &[4, 4, 4], &[4, 5]);
1215 perform_test("abcdefgh…", &[4, 4, 4], &[4, 4, 3]);
1223 }
1224
1225 #[test]
1226 fn test_is_word_char() {
1227 #[track_caller]
1228 fn assert_word(word: &str) {
1229 for c in word.chars() {
1230 assert!(
1231 LineWrapper::is_word_char(c),
1232 "assertion failed for '{}' (unicode 0x{:x})",
1233 c,
1234 c as u32
1235 );
1236 }
1237 }
1238
1239 #[track_caller]
1240 fn assert_not_word(word: &str) {
1241 let found = word.chars().any(|c| !LineWrapper::is_word_char(c));
1242 assert!(found, "assertion failed for '{}'", word);
1243 }
1244
1245 assert_word("Hello123");
1246 assert_word("non-English");
1247 assert_word("var_name");
1248 assert_word("123456");
1249 assert_word("3.1415");
1250 assert_word("10^2");
1251 assert_word("1~2");
1252 assert_word("100%");
1253 assert_word("@mention");
1254 assert_word("#hashtag");
1255 assert_word("$variable");
1256 assert_word("a=1");
1257 assert_word("Self::is_word_char");
1258 assert_word("on;");
1259 assert_word("more⋯");
1260 assert_word("won’t");
1261 assert_word("‘twas");
1262 assert_word("plz!");
1263 assert_word("see)");
1264 assert_word("quoted”");
1265 assert_word("well…");
1266
1267 assert_not_word("foo bar");
1269
1270 assert_word("github.com");
1272 assert_not_word("zed-industries/zed");
1273 assert_not_word("zed-industries\\zed");
1274 assert_not_word("a=1&b=2");
1275 assert_not_word("foo?b=2");
1276
1277 assert_word("ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏ");
1279 assert_word("ĀāĂ㥹ĆćĈĉĊċČčĎď");
1281 assert_word("ƀƁƂƃƄƅƆƇƈƉƊƋƌƍƎƏ");
1283 assert_word("АБВГДЕЖЗИЙКЛМНОП");
1285 assert_word("ThậmchíđếnkhithuachạychúngcònnhẫntâmgiếtnốtsốđôngtùchínhtrịởYênBáivàCaoBằng");
1287 assert_word("গিয়েছিলেন");
1289 assert_word("ছেলে");
1290 assert_word("হচ্ছিল");
1291
1292 assert_not_word("你好");
1294 assert_not_word("안녕하세요");
1295 assert_not_word("こんにちは");
1296 assert_not_word("😀😁😂");
1297 assert_not_word("()[]{}<>");
1298
1299 assert_word("\u{202F}"); assert_word("\u{00A0}"); assert_word("\u{2011}"); }
1305
1306 #[cfg(target_os = "macos")]
1308 use crate as gpui;
1309
1310 #[cfg(target_os = "macos")]
1312 #[crate::test]
1313 fn test_wrap_shaped_line(cx: &mut TestAppContext) {
1314 cx.update(|cx| {
1315 let text_system = WindowTextSystem::new(cx.text_system().clone());
1316
1317 let normal = TextRun {
1318 len: 0,
1319 font: font("Helvetica"),
1320 color: Default::default(),
1321 underline: Default::default(),
1322 ..Default::default()
1323 };
1324 let bold = TextRun {
1325 len: 0,
1326 font: font("Helvetica").bold(),
1327 ..Default::default()
1328 };
1329
1330 let text = "aa bbb cccc ddddd eeee".into();
1331 let lines = text_system
1332 .shape_text(
1333 text,
1334 px(16.),
1335 &[
1336 normal.with_len(4),
1337 bold.with_len(5),
1338 normal.with_len(6),
1339 bold.with_len(1),
1340 normal.with_len(7),
1341 ],
1342 Some(px(72.)),
1343 None,
1344 )
1345 .unwrap();
1346
1347 assert_eq!(
1348 lines[0].layout.wrap_boundaries(),
1349 &[
1350 WrapBoundary {
1351 run_ix: 0,
1352 glyph_ix: 7
1353 },
1354 WrapBoundary {
1355 run_ix: 0,
1356 glyph_ix: 12
1357 },
1358 WrapBoundary {
1359 run_ix: 0,
1360 glyph_ix: 18
1361 }
1362 ],
1363 );
1364 });
1365 }
1366
1367 #[test]
1368 fn test_multiline_truncation_fits_within_wrapped_lines() {
1369 let mut wrapper = build_wrapper();
1370
1371 let text: &str = "aa bbbbbb cccccc dddddd eeee ffff";
1383 let wrap_width = px(72.);
1384 let max_lines: usize = 2;
1385
1386 let runs = generate_test_runs(&[text.len()]);
1387 let (truncated, _) = wrapper.truncate_wrapped_line(
1388 text.into(),
1389 wrap_width,
1390 max_lines,
1391 "\u{2026}",
1392 &runs,
1393 TruncateFrom::End,
1394 );
1395
1396 let wrap_count = wrapper
1398 .wrap_line(
1399 &[LineFragment::text(&truncated)],
1400 wrap_width,
1401 IndentAdjustment::default(),
1402 )
1403 .count();
1404
1405 assert!(
1406 wrap_count < max_lines,
1407 "Truncated text '{}' wraps into {} visual lines, expected at most {}",
1408 truncated,
1409 wrap_count + 1,
1410 max_lines
1411 );
1412
1413 assert!(
1415 truncated.ends_with('\u{2026}'),
1416 "Truncated text '{}' should end with ellipsis",
1417 truncated
1418 );
1419 }
1420
1421 #[test]
1422 fn test_multiline_truncation_no_truncation_needed() {
1423 let mut wrapper = build_wrapper();
1424
1425 let text: &str = "aa bbb cccccc";
1428 let wrap_width = px(72.);
1429 let max_lines: usize = 2;
1430
1431 let runs = generate_test_runs(&[text.len()]);
1432 let (result, _) = wrapper.truncate_wrapped_line(
1433 text.into(),
1434 wrap_width,
1435 max_lines,
1436 "\u{2026}",
1437 &runs,
1438 TruncateFrom::End,
1439 );
1440
1441 assert_eq!(
1442 result.as_ref(),
1443 text,
1444 "Text that fits should not be modified"
1445 );
1446 }
1447
1448 #[test]
1449 fn test_multiline_truncation_three_lines() {
1450 let mut wrapper = build_wrapper();
1451
1452 let text: &str = "aa bbb cccc ddddd eeee ffff gggg hhhh iiii jjjj";
1453 let wrap_width = px(72.);
1454 let max_lines: usize = 3;
1455
1456 let runs = generate_test_runs(&[text.len()]);
1457 let (truncated, _) = wrapper.truncate_wrapped_line(
1458 text.into(),
1459 wrap_width,
1460 max_lines,
1461 "\u{2026}",
1462 &runs,
1463 TruncateFrom::End,
1464 );
1465
1466 let wrap_count = wrapper
1467 .wrap_line(
1468 &[LineFragment::text(&truncated)],
1469 wrap_width,
1470 IndentAdjustment::default(),
1471 )
1472 .count();
1473
1474 assert!(
1475 wrap_count < max_lines,
1476 "Truncated text '{}' wraps into {} visual lines, expected at most {}",
1477 truncated,
1478 wrap_count + 1,
1479 max_lines
1480 );
1481
1482 assert!(
1483 truncated.ends_with('\u{2026}'),
1484 "Truncated text '{}' should end with ellipsis",
1485 truncated
1486 );
1487 }
1488
1489 #[test]
1490 fn test_multiline_truncation_with_newlines() {
1491 let mut wrapper = build_wrapper();
1492
1493 let text: &str = "hello\nworld foo bar baz";
1498 let wrap_width = px(72.);
1499 let max_lines: usize = 2;
1500
1501 let runs = generate_test_runs(&[text.len()]);
1502 let (truncated, _) = wrapper.truncate_wrapped_line(
1503 text.into(),
1504 wrap_width,
1505 max_lines,
1506 "\u{2026}",
1507 &runs,
1508 TruncateFrom::End,
1509 );
1510
1511 let parts: Vec<&str> = truncated.splitn(2, '\n').collect();
1513 assert_eq!(
1514 parts.len(),
1515 2,
1516 "Newline should be preserved: '{}'",
1517 truncated
1518 );
1519 assert_eq!(parts[0], "hello");
1520
1521 let second_line_width: Pixels = parts[1].chars().map(|c| wrapper.width_for_char(c)).sum();
1523 assert!(
1524 second_line_width <= wrap_width,
1525 "Second line '{}' ({}px) exceeds wrap_width ({}px)",
1526 parts[1],
1527 second_line_width,
1528 wrap_width
1529 );
1530 assert!(
1531 truncated.ends_with('\u{2026}'),
1532 "Should end with ellipsis: '{}'",
1533 truncated
1534 );
1535 }
1536
1537 #[test]
1538 fn test_multiline_truncation_newline_on_last_line() {
1539 let mut wrapper = build_wrapper();
1540
1541 let text: &str = "hello\nworld\nmore";
1545 let wrap_width = px(72.);
1546 let max_lines: usize = 2;
1547
1548 let runs = generate_test_runs(&[text.len()]);
1549 let (truncated, _) = wrapper.truncate_wrapped_line(
1550 text.into(),
1551 wrap_width,
1552 max_lines,
1553 "\u{2026}",
1554 &runs,
1555 TruncateFrom::End,
1556 );
1557
1558 let parts: Vec<&str> = truncated.splitn(2, '\n').collect();
1559 assert_eq!(parts[0], "hello");
1560 assert!(
1561 truncated.ends_with('\u{2026}'),
1562 "Should end with ellipsis since there's more content: '{}'",
1563 truncated
1564 );
1565 }
1566
1567 #[test]
1568 fn test_truncate_line_middle() {
1569 let mut wrapper = build_wrapper();
1570
1571 let short_text = "hello world";
1573 let runs = generate_test_runs(&[short_text.len()]);
1574 let (result, result_runs) = wrapper.truncate_line(
1575 short_text.into(),
1576 px(10000.),
1577 "…",
1578 &runs,
1579 TruncateFrom::Middle,
1580 );
1581 assert_eq!(result.as_ref(), short_text);
1582 assert_eq!(result_runs.len(), 1);
1583 assert_eq!(result_runs[0].len, short_text.len());
1584
1585 let long_text = "abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz";
1587 let runs = generate_test_runs(&[long_text.len()]);
1588 let (result, _result_runs) =
1589 wrapper.truncate_line(long_text.into(), px(100.), "…", &runs, TruncateFrom::Middle);
1590 assert!(
1591 result.contains('…'),
1592 "Middle-truncated result should contain '…', got: '{}'",
1593 result
1594 );
1595 assert!(
1596 result.chars().count() < long_text.chars().count(),
1597 "Middle-truncated result should be shorter than original"
1598 );
1599 assert_eq!(
1600 result.chars().next(),
1601 long_text.chars().next(),
1602 "Result should start with the same first character as original"
1603 );
1604 assert_eq!(
1605 result.chars().last(),
1606 long_text.chars().last(),
1607 "Result should end with the same last character as original"
1608 );
1609
1610 let text = "abcdef";
1613 let runs = generate_test_runs(&[text.len()]);
1614 let (result, result_runs) =
1615 wrapper.truncate_line(text.into(), px(1.), "…", &runs, TruncateFrom::Middle);
1616 assert_eq!(result.as_ref(), "…");
1617 assert_eq!(result_runs.len(), 1);
1618 assert_eq!(result_runs[0].len, "…".len());
1619
1620 let multi_run_text = "abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz";
1623 let run_lens = [20, 20, multi_run_text.len() - 40];
1624 let runs = generate_test_runs(&run_lens);
1625 let (result, result_runs) = wrapper.truncate_line(
1626 multi_run_text.into(),
1627 px(100.),
1628 "…",
1629 &runs,
1630 TruncateFrom::Middle,
1631 );
1632 let total_run_len: usize = result_runs.iter().map(|r| r.len).sum();
1633 assert_eq!(
1634 total_run_len,
1635 result.len(),
1636 "Sum of run lengths ({}) should equal result byte length ({})",
1637 total_run_len,
1638 result.len()
1639 );
1640 }
1641
1642 #[test]
1643 fn test_multiline_truncation_trailing_newline() {
1644 let mut wrapper = build_wrapper();
1645
1646 let text: &str = "hello\nworld\n";
1649 let wrap_width = px(72.);
1650 let max_lines: usize = 2;
1651
1652 let runs = generate_test_runs(&[text.len()]);
1653 let (result, _) = wrapper.truncate_wrapped_line(
1654 text.into(),
1655 wrap_width,
1656 max_lines,
1657 "\u{2026}",
1658 &runs,
1659 TruncateFrom::End,
1660 );
1661
1662 assert!(
1663 !result.ends_with('\u{2026}'),
1664 "Trailing newline with no content should not add ellipsis: '{}'",
1665 result
1666 );
1667 }
1668
1669 #[test]
1670 fn test_multiline_truncation_newline_fits_exactly() {
1671 let mut wrapper = build_wrapper();
1672
1673 let text: &str = "hello\nworld";
1676 let wrap_width = px(72.);
1677 let max_lines: usize = 2;
1678
1679 let runs = generate_test_runs(&[text.len()]);
1680 let (result, _) = wrapper.truncate_wrapped_line(
1681 text.into(),
1682 wrap_width,
1683 max_lines,
1684 "\u{2026}",
1685 &runs,
1686 TruncateFrom::End,
1687 );
1688
1689 assert_eq!(
1690 result.as_ref(),
1691 text,
1692 "Text that fits exactly should not be modified: '{}'",
1693 result
1694 );
1695 }
1696
1697 #[test]
1698 fn test_extra_columns_overflow_guard() {
1699 let mut wrapper = build_wrapper();
1700 let space_width = wrapper.width_for_char(' ');
1701
1702 let text = " ab cd ef gh";
1704 let wrap_width = space_width * 10.0;
1705
1706 assert_eq!(
1713 wrapper
1714 .wrap_line(
1715 &[LineFragment::text(text)],
1716 wrap_width,
1717 IndentAdjustment::ExtraColumns(8),
1718 )
1719 .collect::<Vec<_>>(),
1720 &[Boundary::new(9, 0)]
1721 );
1722
1723 assert_eq!(
1732 wrapper
1733 .wrap_line(
1734 &[LineFragment::text(text)],
1735 wrap_width,
1736 IndentAdjustment::ExtraColumns(2),
1737 )
1738 .collect::<Vec<_>>(),
1739 &[
1740 Boundary::new(9, 8),
1741 Boundary::new(11, 8),
1742 Boundary::new(13, 8),
1743 Boundary::new(15, 8),
1744 ]
1745 );
1746
1747 let full_width_text = " 🦀🦀🦀🦀🦀🦀🦀🦀";
1760 assert_eq!(
1761 wrapper
1762 .wrap_line(
1763 &[LineFragment::text(full_width_text)],
1764 wrap_width,
1765 IndentAdjustment::ExtraColumns(8),
1766 )
1767 .collect::<Vec<_>>(),
1768 &[Boundary::new(17, 0)]
1769 );
1770
1771 assert_eq!(
1778 wrapper
1779 .wrap_line(
1780 &[LineFragment::text("a abcdefghi")],
1781 wrap_width,
1782 IndentAdjustment::ExtraColumns(1),
1783 )
1784 .collect::<Vec<_>>(),
1785 &[Boundary::new(2, 1)]
1786 );
1787
1788 assert_eq!(
1795 wrapper
1796 .wrap_line(
1797 &[LineFragment::text("a abcdefghi")],
1798 wrap_width,
1799 IndentAdjustment::ExtraColumns(2),
1800 )
1801 .collect::<Vec<_>>(),
1802 &[Boundary::new(2, 0)]
1803 );
1804
1805 let carried_text = "a abcdefghij";
1817 assert_eq!(
1818 wrapper
1819 .wrap_line(
1820 &[LineFragment::text(carried_text)],
1821 wrap_width,
1822 IndentAdjustment::ExtraColumns(8),
1823 )
1824 .collect::<Vec<_>>(),
1825 &[Boundary::new(2, 0)]
1826 );
1827
1828 let element_fragments = [
1846 LineFragment::text("abcdefghijk "),
1847 LineFragment::element(space_width * 8.0, 1),
1848 LineFragment::text("z 12"),
1849 ];
1850 assert_eq!(
1851 wrapper
1852 .wrap_line(
1853 &element_fragments,
1854 wrap_width,
1855 IndentAdjustment::ExtraColumns(8),
1856 )
1857 .collect::<Vec<_>>(),
1858 &[
1859 Boundary::new(10, 8),
1860 Boundary::new(12, 0),
1861 Boundary::new(15, 8),
1862 ]
1863 );
1864
1865 let spaces = " "; assert_eq!(
1869 wrapper
1870 .wrap_line(
1871 &[LineFragment::text(spaces)],
1872 wrap_width,
1873 IndentAdjustment::ExtraColumns(8),
1874 )
1875 .collect::<Vec<_>>(),
1876 &[Boundary::new(10, 0)]
1877 );
1878
1879 let multiline_indent_text = " ab cd ef gh";
1888 assert_eq!(
1889 wrapper
1890 .wrap_line(
1891 &[LineFragment::text(multiline_indent_text)],
1892 wrap_width,
1893 IndentAdjustment::ExtraColumns(2),
1894 )
1895 .collect::<Vec<_>>(),
1896 &[
1897 Boundary::new(10, 0),
1898 Boundary::new(20, 6),
1899 Boundary::new(23, 6),
1900 ]
1901 );
1902
1903 let oversized_suffix_fragments = [
1920 LineFragment::text("aaaaaaaaaaaaaaaaaaa a"),
1921 LineFragment::element(space_width * 10.0, 1),
1922 ];
1923 assert_eq!(
1924 wrapper
1925 .wrap_line(
1926 &oversized_suffix_fragments,
1927 wrap_width,
1928 IndentAdjustment::ExtraColumns(2),
1929 )
1930 .collect::<Vec<_>>(),
1931 &[
1932 Boundary::new(10, 2),
1933 Boundary::new(18, 2),
1934 Boundary::new(21, 0),
1935 ]
1936 );
1937
1938 assert_eq!(
1939 wrapper
1940 .wrap_line(
1941 &[LineFragment::text(" aaaaaaaa")],
1942 space_width * 4.0,
1943 IndentAdjustment::ExtraColumns(u32::MAX),
1944 )
1945 .collect::<Vec<_>>(),
1946 &[Boundary::new(4, 0), Boundary::new(8, 0)]
1947 );
1948 }
1949}