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 cached_ascii_char_widths: [Option<Pixels>; 128],
34 cached_other_char_widths: HashMap<char, Pixels>,
35}
36
37impl LineWrapper {
38 pub const MAX_INDENT: u32 = 256;
40
41 pub(crate) fn new(font_id: FontId, font_size: Pixels, text_system: Arc<TextSystem>) -> Self {
42 Self {
43 text_system,
44 font_id,
45 font_size,
46 cached_ascii_char_widths: [None; 128],
47 cached_other_char_widths: HashMap::default(),
48 }
49 }
50
51 pub fn wrap_line<'a>(
53 &'a mut self,
54 fragments: &'a [LineFragment],
55 wrap_width: Pixels,
56 indent_adjustment: IndentAdjustment,
57 ) -> impl Iterator<Item = Boundary> + 'a {
58 let mut width = px(0.);
59 let mut first_non_whitespace_ix = None;
60 let mut base_indent = None;
61 let mut last_candidate_ix = 0;
62 let mut last_candidate_width = px(0.);
63 let mut last_wrap_ix = 0;
64 let mut prev_c = '\0';
65 let mut index = 0;
66 let mut candidates = fragments
67 .iter()
68 .flat_map(move |fragment| fragment.wrap_boundary_candidates())
69 .peekable();
70 iter::from_fn(move || {
71 for candidate in candidates.by_ref() {
72 let ix = index;
73 index += candidate.len_utf8();
74 let mut new_prev_c = prev_c;
75 let item_width = match candidate {
76 WrapBoundaryCandidate::Char { character: c } => {
77 if c == '\n' {
78 continue;
79 }
80
81 if Self::is_word_char(c) {
82 if prev_c == ' ' && c != ' ' && first_non_whitespace_ix.is_some() {
83 last_candidate_ix = ix;
84 last_candidate_width = width;
85 }
86 } else {
87 if c != ' ' && first_non_whitespace_ix.is_some() {
89 last_candidate_ix = ix;
90 last_candidate_width = width;
91 }
92 }
93
94 if c != ' ' && first_non_whitespace_ix.is_none() {
95 first_non_whitespace_ix = Some(ix);
96 }
97
98 new_prev_c = c;
99
100 self.width_for_char(c)
101 }
102 WrapBoundaryCandidate::Element {
103 width: element_width,
104 ..
105 } => {
106 if prev_c == ' ' && first_non_whitespace_ix.is_some() {
107 last_candidate_ix = ix;
108 last_candidate_width = width;
109 }
110
111 if first_non_whitespace_ix.is_none() {
112 first_non_whitespace_ix = Some(ix);
113 }
114
115 element_width
116 }
117 };
118
119 width += item_width;
120 if width > wrap_width && ix > last_wrap_ix {
121 let wrap_at_candidate =
122 last_candidate_ix > 0 && width - last_candidate_width <= wrap_width;
123
124 let carried_width = if wrap_at_candidate {
125 width - last_candidate_width
126 } else {
127 item_width
128 };
129
130 if base_indent.is_none() {
136 if let Some(first_non_whitespace_ix) = first_non_whitespace_ix {
137 base_indent =
138 Some(Self::MAX_INDENT.min(
139 first_non_whitespace_ix.saturating_sub(last_wrap_ix) as u32,
140 ));
141 }
142 }
143
144 let next_indent = match indent_adjustment {
145 IndentAdjustment::NoIndent => 0,
146 IndentAdjustment::SameIndent => base_indent.unwrap_or(0),
147 IndentAdjustment::ExtraColumns(extra) => {
148 if let Some(base_indent) = base_indent {
149 let candidate = base_indent.saturating_add(extra);
150 let candidate_indent_width =
151 self.width_for_char(' ') * candidate as f32;
152 let min_headroom =
156 carried_width.max(self.width_for_char(' ') * 2.0);
157 if candidate_indent_width + min_headroom > wrap_width {
158 0
159 } else {
160 Self::MAX_INDENT.min(candidate)
161 }
162 } else {
163 0
164 }
165 }
166 };
167
168 if wrap_at_candidate {
169 last_wrap_ix = last_candidate_ix;
170 width -= last_candidate_width;
171 } else {
172 last_wrap_ix = ix;
173 width = item_width;
174 }
175 last_candidate_ix = 0;
176
177 width += self.width_for_char(' ') * next_indent as f32;
178
179 return Some(Boundary::new(last_wrap_ix, next_indent));
180 }
181
182 prev_c = new_prev_c;
183 }
184
185 None
186 })
187 }
188
189 pub fn should_truncate_line(
193 &mut self,
194 line: &str,
195 truncate_width: Pixels,
196 truncation_affix: &str,
197 truncate_from: TruncateFrom,
198 ) -> Option<usize> {
199 let mut width = px(0.);
200 let suffix_width = truncation_affix
201 .chars()
202 .map(|c| self.width_for_char(c))
203 .fold(px(0.0), |a, x| a + x);
204 let mut truncate_ix = 0;
205
206 match truncate_from {
207 TruncateFrom::Start => {
208 for (ix, c) in line.char_indices().rev() {
209 if width + suffix_width < truncate_width {
210 truncate_ix = ix;
211 }
212
213 let char_width = self.width_for_char(c);
214 width += char_width;
215
216 if width.floor() > truncate_width {
217 return Some(truncate_ix);
218 }
219 }
220 }
221 TruncateFrom::End => {
222 for (ix, c) in line.char_indices() {
223 if width + suffix_width < truncate_width {
224 truncate_ix = ix;
225 }
226
227 let char_width = self.width_for_char(c);
228 width += char_width;
229
230 if width.floor() > truncate_width {
231 return Some(truncate_ix);
232 }
233 }
234 }
235 TruncateFrom::Middle => {}
236 }
237
238 None
239 }
240
241 fn should_truncate_line_middle(
242 &mut self,
243 line: &str,
244 truncate_width: Pixels,
245 truncation_affix: &str,
246 ) -> Option<(usize, usize)> {
247 let suffix_width = truncation_affix
248 .chars()
249 .map(|c| self.width_for_char(c))
250 .fold(px(0.0), |a, x| a + x);
251
252 let total_width: Pixels = line
253 .chars()
254 .map(|c| self.width_for_char(c))
255 .fold(px(0.0), |a, x| a + x);
256
257 if total_width <= truncate_width {
258 return None;
259 }
260
261 let content_budget = truncate_width - suffix_width;
262 if content_budget <= px(0.) {
263 return Some((0, line.len()));
264 }
265
266 let front_budget = content_budget * (2.0 / 3.0);
267 let back_budget = content_budget - front_budget;
268
269 let mut front_width = px(0.);
270 let mut front_end_ix = 0usize;
271 for (ix, c) in line.char_indices() {
272 let char_width = self.width_for_char(c);
273 if front_width + char_width > front_budget {
274 break;
275 }
276 front_width += char_width;
277 front_end_ix = ix + c.len_utf8();
278 }
279
280 let mut back_width = px(0.);
281 let mut back_start_ix = line.len();
282 for (ix, c) in line.char_indices().rev() {
283 let char_width = self.width_for_char(c);
284 if back_width + char_width > back_budget {
285 break;
286 }
287 back_width += char_width;
288 back_start_ix = ix;
289 }
290
291 if front_end_ix >= back_start_ix {
292 return Some((0, line.len()));
293 }
294
295 Some((front_end_ix, back_start_ix))
296 }
297
298 pub fn truncate_line<'a>(
300 &mut self,
301 line: SharedString,
302 truncate_width: Pixels,
303 truncation_affix: &str,
304 runs: &'a [TextRun],
305 truncate_from: TruncateFrom,
306 ) -> (SharedString, Cow<'a, [TextRun]>) {
307 if truncate_from == TruncateFrom::Middle {
308 if let Some((front_end_ix, back_start_ix)) =
309 self.should_truncate_line_middle(&line, truncate_width, truncation_affix)
310 {
311 let result = SharedString::from(format!(
312 "{}{truncation_affix}{}",
313 &line[..front_end_ix],
314 &line[back_start_ix..]
315 ));
316 let mut runs = runs.to_vec();
317 update_runs_after_middle_truncation(
318 truncation_affix,
319 &mut runs,
320 front_end_ix,
321 back_start_ix,
322 );
323 return (result, Cow::Owned(runs));
324 } else {
325 return (line, Cow::Borrowed(runs));
326 }
327 }
328
329 if let Some(truncate_ix) =
330 self.should_truncate_line(&line, truncate_width, truncation_affix, truncate_from)
331 {
332 let result = match truncate_from {
333 TruncateFrom::Start => SharedString::from(format!(
334 "{truncation_affix}{}",
335 &line[line.ceil_char_boundary(truncate_ix + 1)..]
336 )),
337 TruncateFrom::End => SharedString::from(format!(
338 "{}{truncation_affix}",
339 line[..truncate_ix]
340 .trim_end_matches(|c: char| c.is_whitespace() || c.is_ascii_punctuation())
341 )),
342 TruncateFrom::Middle => unreachable!("Middle truncation is handled above"),
343 };
344 let mut runs = runs.to_vec();
345 update_runs_after_truncation(&result, truncation_affix, &mut runs, truncate_from);
346 (result, Cow::Owned(runs))
347 } else {
348 (line, Cow::Borrowed(runs))
349 }
350 }
351
352 pub fn truncate_wrapped_line<'a>(
362 &mut self,
363 text: SharedString,
364 wrap_width: Pixels,
365 max_lines: usize,
366 truncation_affix: &str,
367 runs: &'a [TextRun],
368 truncate_from: TruncateFrom,
369 ) -> (SharedString, Cow<'a, [TextRun]>) {
370 if max_lines <= 1 || truncate_from == TruncateFrom::Start {
371 return self.truncate_line(
372 text,
373 wrap_width * max_lines,
374 truncation_affix,
375 runs,
376 truncate_from,
377 );
378 }
379 if truncate_from == TruncateFrom::Middle {
380 return self.truncate_line(text, wrap_width, truncation_affix, runs, truncate_from);
381 }
382
383 let affix_width: Pixels = truncation_affix
384 .chars()
385 .map(|c| self.width_for_char(c))
386 .sum();
387
388 let mut width = px(0.);
389 let mut line = 0usize;
390 let mut first_non_whitespace_ix = None;
391 let mut last_candidate_ix = 0usize;
392 let mut last_candidate_width = px(0.);
393 let mut last_wrap_ix = 0usize;
394 let mut prev_c = '\0';
395 let mut indent: Option<u32> = None;
396 let mut truncate_ix = 0usize;
397
398 for (ix, c) in text.char_indices() {
399 if c == '\n' {
400 if line >= max_lines - 1 && !text[ix + 1..].trim().is_empty() {
401 let truncated = text[..truncate_ix]
404 .trim_end_matches(|c: char| c.is_whitespace() || c.is_ascii_punctuation());
405 let result = SharedString::from(format!("{truncated}{truncation_affix}"));
406 let mut runs = runs.to_vec();
407 update_runs_after_truncation(
408 &result,
409 truncation_affix,
410 &mut runs,
411 TruncateFrom::End,
412 );
413 return (result, Cow::Owned(runs));
414 }
415
416 line += 1;
418 width = px(0.);
419 first_non_whitespace_ix = None;
420 last_candidate_ix = 0;
421 last_candidate_width = px(0.);
422 last_wrap_ix = ix + 1;
423 prev_c = '\0';
424 indent = None;
425 truncate_ix = ix + 1;
426 continue;
427 }
428
429 let char_width = self.width_for_char(c);
430
431 if Self::is_word_char(c) {
432 if prev_c == ' ' && first_non_whitespace_ix.is_some() {
433 last_candidate_ix = ix;
434 last_candidate_width = width;
435 }
436 } else if c != ' ' && first_non_whitespace_ix.is_some() {
437 last_candidate_ix = ix;
438 last_candidate_width = width;
439 }
440
441 if c != ' ' && first_non_whitespace_ix.is_none() {
442 first_non_whitespace_ix = Some(ix);
443 }
444
445 width += char_width;
446
447 if line < max_lines - 1 {
448 if width > wrap_width && ix > last_wrap_ix {
450 if let (None, Some(first_nw)) = (indent, first_non_whitespace_ix) {
451 indent = Some(Self::MAX_INDENT.min((first_nw - last_wrap_ix) as u32));
452 }
453
454 if last_candidate_ix > last_wrap_ix {
455 last_wrap_ix = last_candidate_ix;
456 width -= last_candidate_width;
457 last_candidate_ix = 0;
458 } else {
459 last_wrap_ix = ix;
460 width = char_width;
461 }
462
463 if let Some(ind) = indent {
464 width += self.width_for_char(' ') * ind as f32;
465 }
466
467 line += 1;
468 truncate_ix = last_wrap_ix;
469 }
470 } else {
471 if width + affix_width <= wrap_width {
474 truncate_ix = ix + c.len_utf8();
475 }
476
477 if width > wrap_width {
478 let truncated = text[..truncate_ix]
479 .trim_end_matches(|c: char| c.is_whitespace() || c.is_ascii_punctuation());
480 let result = SharedString::from(format!("{truncated}{truncation_affix}"));
481 let mut runs = runs.to_vec();
482 update_runs_after_truncation(
483 &result,
484 truncation_affix,
485 &mut runs,
486 TruncateFrom::End,
487 );
488 return (result, Cow::Owned(runs));
489 }
490 }
491
492 prev_c = c;
493 }
494
495 (text, Cow::Borrowed(runs))
497 }
498
499 pub(crate) fn is_word_char(c: char) -> bool {
502 c.is_ascii_alphanumeric() ||
504 matches!(c, '\u{00C0}'..='\u{00FF}') ||
508 matches!(c, '\u{0100}'..='\u{017F}') ||
511 matches!(c, '\u{0180}'..='\u{024F}') ||
514 matches!(c, '\u{0400}'..='\u{04FF}') ||
517
518 matches!(c, '\u{1E00}'..='\u{1EFF}') || matches!(c, '\u{0300}'..='\u{036F}') || matches!(c, '\u{0980}'..='\u{09FF}') ||
524
525 matches!(c, '-' | '_' | '.' | '\'' | '’' | '‘' | '$' | '%' | '@' | '#' | '^' | '~' | ',' | '=' | ':' | ';') ||
530 matches!(c, '!' | ')' | ']' | '}' | '"' | '”' | '»' | '…') ||
536 matches!(c, '⋯') ||
538
539 matches!(c, '\u{202F}' | '\u{00A0}' | '\u{2011}')
541 }
542
543 #[inline(always)]
544 fn width_for_char(&mut self, c: char) -> Pixels {
545 if (c as u32) < 128 {
546 if let Some(cached_width) = self.cached_ascii_char_widths[c as usize] {
547 cached_width
548 } else {
549 let width = self
550 .text_system
551 .layout_width(self.font_id, self.font_size, c);
552 self.cached_ascii_char_widths[c as usize] = Some(width);
553 width
554 }
555 } else if let Some(cached_width) = self.cached_other_char_widths.get(&c) {
556 *cached_width
557 } else {
558 let width = self
559 .text_system
560 .layout_width(self.font_id, self.font_size, c);
561 self.cached_other_char_widths.insert(c, width);
562 width
563 }
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::{Font, FontFeatures, FontStyle, FontWeight, TestAppContext, TestDispatcher, font};
742 #[cfg(target_os = "macos")]
743 use crate::{TextRun, WindowTextSystem, WrapBoundary};
744
745 fn build_wrapper() -> LineWrapper {
746 let dispatcher = TestDispatcher::new(0);
747 let cx = TestAppContext::build(dispatcher, None);
748 let id = cx.text_system().resolve_font(&font(".ZedMono"));
749 LineWrapper::new(id, px(16.), cx.text_system().clone())
750 }
751
752 fn generate_test_runs(input_run_len: &[usize]) -> Vec<TextRun> {
753 input_run_len
754 .iter()
755 .map(|run_len| TextRun {
756 len: *run_len,
757 font: Font {
758 family: "Dummy".into(),
759 features: FontFeatures::default(),
760 fallbacks: None,
761 weight: FontWeight::default(),
762 style: FontStyle::Normal,
763 },
764 ..Default::default()
765 })
766 .collect()
767 }
768
769 #[test]
770 fn test_wrap_line() {
771 let mut wrapper = build_wrapper();
772
773 assert_eq!(
774 wrapper
775 .wrap_line(
776 &[LineFragment::text("aa bbb cccc ddddd eeee")],
777 px(72.),
778 IndentAdjustment::default()
779 )
780 .collect::<Vec<_>>(),
781 &[
782 Boundary::new(7, 0),
783 Boundary::new(12, 0),
784 Boundary::new(18, 0)
785 ],
786 );
787 assert_eq!(
788 wrapper
789 .wrap_line(
790 &[LineFragment::text("aaa aaaaaaaaaaaaaaaaaa")],
791 px(72.0),
792 IndentAdjustment::default()
793 )
794 .collect::<Vec<_>>(),
795 &[
796 Boundary::new(4, 0),
797 Boundary::new(11, 0),
798 Boundary::new(18, 0)
799 ],
800 );
801 assert_eq!(
802 wrapper
803 .wrap_line(
804 &[LineFragment::text(" aaaaaaa")],
805 px(72.),
806 IndentAdjustment::default()
807 )
808 .collect::<Vec<_>>(),
809 &[
810 Boundary::new(7, 5),
811 Boundary::new(9, 5),
812 Boundary::new(11, 5),
813 ]
814 );
815 assert_eq!(
816 wrapper
817 .wrap_line(
818 &[LineFragment::text(" ")],
819 px(72.),
820 IndentAdjustment::default()
821 )
822 .collect::<Vec<_>>(),
823 &[
824 Boundary::new(7, 0),
825 Boundary::new(14, 0),
826 Boundary::new(21, 0)
827 ]
828 );
829 assert_eq!(
830 wrapper
831 .wrap_line(
832 &[LineFragment::text(" aaaaaaaaaaaaaa")],
833 px(72.),
834 IndentAdjustment::default()
835 )
836 .collect::<Vec<_>>(),
837 &[
838 Boundary::new(7, 0),
839 Boundary::new(14, 3),
840 Boundary::new(18, 3),
841 Boundary::new(22, 3),
842 ]
843 );
844
845 assert_eq!(
847 wrapper
848 .wrap_line(
849 &[
850 LineFragment::text("aa bbb "),
851 LineFragment::text("cccc ddddd eeee")
852 ],
853 px(72.),
854 IndentAdjustment::default()
855 )
856 .collect::<Vec<_>>(),
857 &[
858 Boundary::new(7, 0),
859 Boundary::new(12, 0),
860 Boundary::new(18, 0)
861 ],
862 );
863
864 assert_eq!(
866 wrapper
867 .wrap_line(
868 &[
869 LineFragment::text("aa "),
870 LineFragment::element(px(20.), 1),
871 LineFragment::text(" bbb "),
872 LineFragment::element(px(30.), 1),
873 LineFragment::text(" cccc")
874 ],
875 px(72.),
876 IndentAdjustment::default()
877 )
878 .collect::<Vec<_>>(),
879 &[
880 Boundary::new(5, 0),
881 Boundary::new(9, 0),
882 Boundary::new(11, 0)
883 ],
884 );
885
886 assert_eq!(
888 wrapper
889 .wrap_line(
890 &[
891 LineFragment::element(px(50.), 1),
892 LineFragment::text(" aaaa bbbb cccc dddd")
893 ],
894 px(72.),
895 IndentAdjustment::default()
896 )
897 .collect::<Vec<_>>(),
898 &[
899 Boundary::new(2, 0),
900 Boundary::new(7, 0),
901 Boundary::new(12, 0),
902 Boundary::new(17, 0)
903 ],
904 );
905
906 assert_eq!(
908 wrapper
909 .wrap_line(
910 &[
911 LineFragment::text("short text "),
912 LineFragment::element(px(100.), 1),
913 LineFragment::text(" more text")
914 ],
915 px(72.),
916 IndentAdjustment::default()
917 )
918 .collect::<Vec<_>>(),
919 &[
920 Boundary::new(6, 0),
921 Boundary::new(11, 0),
922 Boundary::new(12, 0),
923 Boundary::new(18, 0)
924 ],
925 );
926
927 assert_eq!(
929 wrapper
930 .wrap_line(
931 &[LineFragment::text("a\u{202F}b\u{00A0}c\u{2011}d e")],
932 px(72.0),
933 IndentAdjustment::default()
934 )
935 .collect::<Vec<_>>(),
936 &[Boundary::new(12, 0),], );
938 }
939
940 #[test]
941 fn test_truncate_line_end() {
942 let mut wrapper = build_wrapper();
943
944 fn perform_test(
945 wrapper: &mut LineWrapper,
946 text: &'static str,
947 expected: &'static str,
948 ellipsis: &str,
949 ) {
950 let dummy_run_lens = vec![text.len()];
951 let dummy_runs = generate_test_runs(&dummy_run_lens);
952 let (result, dummy_runs) = wrapper.truncate_line(
953 text.into(),
954 px(220.),
955 ellipsis,
956 &dummy_runs,
957 TruncateFrom::End,
958 );
959 assert_eq!(result, expected);
960 assert_eq!(dummy_runs.first().unwrap().len, result.len());
961 }
962
963 perform_test(
964 &mut wrapper,
965 "aa bbb cccc ddddd eeee ffff gggg",
966 "aa bbb cccc ddddd eeee",
967 "",
968 );
969 perform_test(
970 &mut wrapper,
971 "aa bbb cccc ddddd eeee ffff gggg",
972 "aa bbb cccc ddddd eee…",
973 "…",
974 );
975 perform_test(
976 &mut wrapper,
977 "aa bbb cccc ddddd eeee ffff gggg",
978 "aa bbb cccc dddd......",
979 "......",
980 );
981 perform_test(
982 &mut wrapper,
983 "aa bbb cccc 🦀🦀🦀🦀🦀 eeee ffff gggg",
984 "aa bbb cccc 🦀🦀🦀🦀…",
985 "…",
986 );
987 }
988
989 #[test]
990 fn test_truncate_line_start() {
991 let mut wrapper = build_wrapper();
992
993 #[track_caller]
994 fn perform_test(
995 wrapper: &mut LineWrapper,
996 text: &'static str,
997 expected: &'static str,
998 ellipsis: &str,
999 ) {
1000 let dummy_run_lens = vec![text.len()];
1001 let dummy_runs = generate_test_runs(&dummy_run_lens);
1002 let (result, dummy_runs) = wrapper.truncate_line(
1003 text.into(),
1004 px(220.),
1005 ellipsis,
1006 &dummy_runs,
1007 TruncateFrom::Start,
1008 );
1009 assert_eq!(result, expected);
1010 assert_eq!(dummy_runs.first().unwrap().len, result.len());
1011 }
1012
1013 perform_test(
1014 &mut wrapper,
1015 "aaaa bbbb cccc ddddd eeee fff gg",
1016 "cccc ddddd eeee fff gg",
1017 "",
1018 );
1019 perform_test(
1020 &mut wrapper,
1021 "aaaa bbbb cccc ddddd eeee fff gg",
1022 "…ccc ddddd eeee fff gg",
1023 "…",
1024 );
1025 perform_test(
1026 &mut wrapper,
1027 "aaaa bbbb cccc ddddd eeee fff gg",
1028 "......dddd eeee fff gg",
1029 "......",
1030 );
1031 perform_test(
1032 &mut wrapper,
1033 "aaaa bbbb cccc 🦀🦀🦀🦀🦀 eeee fff gg",
1034 "…🦀🦀🦀🦀 eeee fff gg",
1035 "…",
1036 );
1037 }
1038
1039 #[test]
1040 fn test_truncate_multiple_runs_end() {
1041 let mut wrapper = build_wrapper();
1042
1043 fn perform_test(
1044 wrapper: &mut LineWrapper,
1045 text: &'static str,
1046 expected: &str,
1047 run_lens: &[usize],
1048 result_run_len: &[usize],
1049 line_width: Pixels,
1050 ) {
1051 let dummy_runs = generate_test_runs(run_lens);
1052 let (result, dummy_runs) =
1053 wrapper.truncate_line(text.into(), line_width, "…", &dummy_runs, TruncateFrom::End);
1054 assert_eq!(result, expected);
1055 for (run, result_len) in dummy_runs.iter().zip(result_run_len) {
1056 assert_eq!(run.len, *result_len);
1057 }
1058 }
1059 perform_test(&mut wrapper, "abcdefghijkl", "abcd…", &[12], &[7], px(50.));
1066 perform_test(
1074 &mut wrapper,
1075 "abcdefghijkl",
1076 "abcdef…",
1077 &[4, 4, 4],
1078 &[4, 5],
1079 px(70.),
1080 );
1081 perform_test(
1089 &mut wrapper,
1090 "abcdefghijkl",
1091 "abcdefgh…",
1092 &[4, 4, 4],
1093 &[4, 4, 3],
1094 px(90.),
1095 );
1096 }
1097
1098 #[test]
1099 fn test_truncate_multiple_runs_start() {
1100 let mut wrapper = build_wrapper();
1101
1102 #[track_caller]
1103 fn perform_test(
1104 wrapper: &mut LineWrapper,
1105 text: &'static str,
1106 expected: &str,
1107 run_lens: &[usize],
1108 result_run_len: &[usize],
1109 line_width: Pixels,
1110 ) {
1111 let dummy_runs = generate_test_runs(run_lens);
1112 let (result, dummy_runs) = wrapper.truncate_line(
1113 text.into(),
1114 line_width,
1115 "…",
1116 &dummy_runs,
1117 TruncateFrom::Start,
1118 );
1119 assert_eq!(result, expected);
1120 for (run, result_len) in dummy_runs.iter().zip(result_run_len) {
1121 assert_eq!(run.len, *result_len);
1122 }
1123 }
1124 perform_test(&mut wrapper, "abcdefghijkl", "…ijkl", &[12], &[7], px(50.));
1131 perform_test(
1139 &mut wrapper,
1140 "abcdefghijkl",
1141 "…ghijkl",
1142 &[4, 4, 4],
1143 &[5, 4],
1144 px(70.),
1145 );
1146 perform_test(
1154 &mut wrapper,
1155 "abcdefghijkl",
1156 "…efghijkl",
1157 &[4, 4, 4],
1158 &[3, 4, 4],
1159 px(90.),
1160 );
1161 }
1162
1163 #[test]
1164 fn test_update_run_after_truncation_end() {
1165 fn perform_test(result: &str, run_lens: &[usize], result_run_lens: &[usize]) {
1166 let mut dummy_runs = generate_test_runs(run_lens);
1167 update_runs_after_truncation(result, "…", &mut dummy_runs, TruncateFrom::End);
1168 for (run, result_len) in dummy_runs.iter().zip(result_run_lens) {
1169 assert_eq!(run.len, *result_len);
1170 }
1171 }
1172 perform_test("abcd…", &[12], &[7]);
1179 perform_test("abcdef…", &[4, 4, 4], &[4, 5]);
1187 perform_test("abcdefgh…", &[4, 4, 4], &[4, 4, 3]);
1195 }
1196
1197 #[test]
1198 fn test_is_word_char() {
1199 #[track_caller]
1200 fn assert_word(word: &str) {
1201 for c in word.chars() {
1202 assert!(
1203 LineWrapper::is_word_char(c),
1204 "assertion failed for '{}' (unicode 0x{:x})",
1205 c,
1206 c as u32
1207 );
1208 }
1209 }
1210
1211 #[track_caller]
1212 fn assert_not_word(word: &str) {
1213 let found = word.chars().any(|c| !LineWrapper::is_word_char(c));
1214 assert!(found, "assertion failed for '{}'", word);
1215 }
1216
1217 assert_word("Hello123");
1218 assert_word("non-English");
1219 assert_word("var_name");
1220 assert_word("123456");
1221 assert_word("3.1415");
1222 assert_word("10^2");
1223 assert_word("1~2");
1224 assert_word("100%");
1225 assert_word("@mention");
1226 assert_word("#hashtag");
1227 assert_word("$variable");
1228 assert_word("a=1");
1229 assert_word("Self::is_word_char");
1230 assert_word("on;");
1231 assert_word("more⋯");
1232 assert_word("won’t");
1233 assert_word("‘twas");
1234 assert_word("plz!");
1235 assert_word("see)");
1236 assert_word("quoted”");
1237 assert_word("well…");
1238
1239 assert_not_word("foo bar");
1241
1242 assert_word("github.com");
1244 assert_not_word("zed-industries/zed");
1245 assert_not_word("zed-industries\\zed");
1246 assert_not_word("a=1&b=2");
1247 assert_not_word("foo?b=2");
1248
1249 assert_word("ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏ");
1251 assert_word("ĀāĂ㥹ĆćĈĉĊċČčĎď");
1253 assert_word("ƀƁƂƃƄƅƆƇƈƉƊƋƌƍƎƏ");
1255 assert_word("АБВГДЕЖЗИЙКЛМНОП");
1257 assert_word("ThậmchíđếnkhithuachạychúngcònnhẫntâmgiếtnốtsốđôngtùchínhtrịởYênBáivàCaoBằng");
1259 assert_word("গিয়েছিলেন");
1261 assert_word("ছেলে");
1262 assert_word("হচ্ছিল");
1263
1264 assert_not_word("你好");
1266 assert_not_word("안녕하세요");
1267 assert_not_word("こんにちは");
1268 assert_not_word("😀😁😂");
1269 assert_not_word("()[]{}<>");
1270
1271 assert_word("\u{202F}"); assert_word("\u{00A0}"); assert_word("\u{2011}"); }
1277
1278 #[cfg(target_os = "macos")]
1280 use crate as gpui;
1281
1282 #[cfg(target_os = "macos")]
1284 #[crate::test]
1285 fn test_wrap_shaped_line(cx: &mut TestAppContext) {
1286 cx.update(|cx| {
1287 let text_system = WindowTextSystem::new(cx.text_system().clone());
1288
1289 let normal = TextRun {
1290 len: 0,
1291 font: font("Helvetica"),
1292 color: Default::default(),
1293 underline: Default::default(),
1294 ..Default::default()
1295 };
1296 let bold = TextRun {
1297 len: 0,
1298 font: font("Helvetica").bold(),
1299 ..Default::default()
1300 };
1301
1302 let text = "aa bbb cccc ddddd eeee".into();
1303 let lines = text_system
1304 .shape_text(
1305 text,
1306 px(16.),
1307 &[
1308 normal.with_len(4),
1309 bold.with_len(5),
1310 normal.with_len(6),
1311 bold.with_len(1),
1312 normal.with_len(7),
1313 ],
1314 Some(px(72.)),
1315 None,
1316 )
1317 .unwrap();
1318
1319 assert_eq!(
1320 lines[0].layout.wrap_boundaries(),
1321 &[
1322 WrapBoundary {
1323 run_ix: 0,
1324 glyph_ix: 7
1325 },
1326 WrapBoundary {
1327 run_ix: 0,
1328 glyph_ix: 12
1329 },
1330 WrapBoundary {
1331 run_ix: 0,
1332 glyph_ix: 18
1333 }
1334 ],
1335 );
1336 });
1337 }
1338
1339 #[test]
1340 fn test_multiline_truncation_fits_within_wrapped_lines() {
1341 let mut wrapper = build_wrapper();
1342
1343 let text: &str = "aa bbbbbb cccccc dddddd eeee ffff";
1355 let wrap_width = px(72.);
1356 let max_lines: usize = 2;
1357
1358 let runs = generate_test_runs(&[text.len()]);
1359 let (truncated, _) = wrapper.truncate_wrapped_line(
1360 text.into(),
1361 wrap_width,
1362 max_lines,
1363 "\u{2026}",
1364 &runs,
1365 TruncateFrom::End,
1366 );
1367
1368 let wrap_count = wrapper
1370 .wrap_line(
1371 &[LineFragment::text(&truncated)],
1372 wrap_width,
1373 IndentAdjustment::default(),
1374 )
1375 .count();
1376
1377 assert!(
1378 wrap_count < max_lines,
1379 "Truncated text '{}' wraps into {} visual lines, expected at most {}",
1380 truncated,
1381 wrap_count + 1,
1382 max_lines
1383 );
1384
1385 assert!(
1387 truncated.ends_with('\u{2026}'),
1388 "Truncated text '{}' should end with ellipsis",
1389 truncated
1390 );
1391 }
1392
1393 #[test]
1394 fn test_multiline_truncation_no_truncation_needed() {
1395 let mut wrapper = build_wrapper();
1396
1397 let text: &str = "aa bbb cccccc";
1400 let wrap_width = px(72.);
1401 let max_lines: usize = 2;
1402
1403 let runs = generate_test_runs(&[text.len()]);
1404 let (result, _) = wrapper.truncate_wrapped_line(
1405 text.into(),
1406 wrap_width,
1407 max_lines,
1408 "\u{2026}",
1409 &runs,
1410 TruncateFrom::End,
1411 );
1412
1413 assert_eq!(
1414 result.as_ref(),
1415 text,
1416 "Text that fits should not be modified"
1417 );
1418 }
1419
1420 #[test]
1421 fn test_multiline_truncation_three_lines() {
1422 let mut wrapper = build_wrapper();
1423
1424 let text: &str = "aa bbb cccc ddddd eeee ffff gggg hhhh iiii jjjj";
1425 let wrap_width = px(72.);
1426 let max_lines: usize = 3;
1427
1428 let runs = generate_test_runs(&[text.len()]);
1429 let (truncated, _) = wrapper.truncate_wrapped_line(
1430 text.into(),
1431 wrap_width,
1432 max_lines,
1433 "\u{2026}",
1434 &runs,
1435 TruncateFrom::End,
1436 );
1437
1438 let wrap_count = wrapper
1439 .wrap_line(
1440 &[LineFragment::text(&truncated)],
1441 wrap_width,
1442 IndentAdjustment::default(),
1443 )
1444 .count();
1445
1446 assert!(
1447 wrap_count < max_lines,
1448 "Truncated text '{}' wraps into {} visual lines, expected at most {}",
1449 truncated,
1450 wrap_count + 1,
1451 max_lines
1452 );
1453
1454 assert!(
1455 truncated.ends_with('\u{2026}'),
1456 "Truncated text '{}' should end with ellipsis",
1457 truncated
1458 );
1459 }
1460
1461 #[test]
1462 fn test_multiline_truncation_with_newlines() {
1463 let mut wrapper = build_wrapper();
1464
1465 let text: &str = "hello\nworld foo bar baz";
1470 let wrap_width = px(72.);
1471 let max_lines: usize = 2;
1472
1473 let runs = generate_test_runs(&[text.len()]);
1474 let (truncated, _) = wrapper.truncate_wrapped_line(
1475 text.into(),
1476 wrap_width,
1477 max_lines,
1478 "\u{2026}",
1479 &runs,
1480 TruncateFrom::End,
1481 );
1482
1483 let parts: Vec<&str> = truncated.splitn(2, '\n').collect();
1485 assert_eq!(
1486 parts.len(),
1487 2,
1488 "Newline should be preserved: '{}'",
1489 truncated
1490 );
1491 assert_eq!(parts[0], "hello");
1492
1493 let second_line_width: Pixels = parts[1].chars().map(|c| wrapper.width_for_char(c)).sum();
1495 assert!(
1496 second_line_width <= wrap_width,
1497 "Second line '{}' ({}px) exceeds wrap_width ({}px)",
1498 parts[1],
1499 second_line_width,
1500 wrap_width
1501 );
1502 assert!(
1503 truncated.ends_with('\u{2026}'),
1504 "Should end with ellipsis: '{}'",
1505 truncated
1506 );
1507 }
1508
1509 #[test]
1510 fn test_multiline_truncation_newline_on_last_line() {
1511 let mut wrapper = build_wrapper();
1512
1513 let text: &str = "hello\nworld\nmore";
1517 let wrap_width = px(72.);
1518 let max_lines: usize = 2;
1519
1520 let runs = generate_test_runs(&[text.len()]);
1521 let (truncated, _) = wrapper.truncate_wrapped_line(
1522 text.into(),
1523 wrap_width,
1524 max_lines,
1525 "\u{2026}",
1526 &runs,
1527 TruncateFrom::End,
1528 );
1529
1530 let parts: Vec<&str> = truncated.splitn(2, '\n').collect();
1531 assert_eq!(parts[0], "hello");
1532 assert!(
1533 truncated.ends_with('\u{2026}'),
1534 "Should end with ellipsis since there's more content: '{}'",
1535 truncated
1536 );
1537 }
1538
1539 #[test]
1540 fn test_truncate_line_middle() {
1541 let mut wrapper = build_wrapper();
1542
1543 let short_text = "hello world";
1545 let runs = generate_test_runs(&[short_text.len()]);
1546 let (result, result_runs) = wrapper.truncate_line(
1547 short_text.into(),
1548 px(10000.),
1549 "…",
1550 &runs,
1551 TruncateFrom::Middle,
1552 );
1553 assert_eq!(result.as_ref(), short_text);
1554 assert_eq!(result_runs.len(), 1);
1555 assert_eq!(result_runs[0].len, short_text.len());
1556
1557 let long_text = "abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz";
1559 let runs = generate_test_runs(&[long_text.len()]);
1560 let (result, _result_runs) =
1561 wrapper.truncate_line(long_text.into(), px(100.), "…", &runs, TruncateFrom::Middle);
1562 assert!(
1563 result.contains('…'),
1564 "Middle-truncated result should contain '…', got: '{}'",
1565 result
1566 );
1567 assert!(
1568 result.chars().count() < long_text.chars().count(),
1569 "Middle-truncated result should be shorter than original"
1570 );
1571 assert_eq!(
1572 result.chars().next(),
1573 long_text.chars().next(),
1574 "Result should start with the same first character as original"
1575 );
1576 assert_eq!(
1577 result.chars().last(),
1578 long_text.chars().last(),
1579 "Result should end with the same last character as original"
1580 );
1581
1582 let text = "abcdef";
1585 let runs = generate_test_runs(&[text.len()]);
1586 let (result, result_runs) =
1587 wrapper.truncate_line(text.into(), px(1.), "…", &runs, TruncateFrom::Middle);
1588 assert_eq!(result.as_ref(), "…");
1589 assert_eq!(result_runs.len(), 1);
1590 assert_eq!(result_runs[0].len, "…".len());
1591
1592 let multi_run_text = "abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz";
1595 let run_lens = [20, 20, multi_run_text.len() - 40];
1596 let runs = generate_test_runs(&run_lens);
1597 let (result, result_runs) = wrapper.truncate_line(
1598 multi_run_text.into(),
1599 px(100.),
1600 "…",
1601 &runs,
1602 TruncateFrom::Middle,
1603 );
1604 let total_run_len: usize = result_runs.iter().map(|r| r.len).sum();
1605 assert_eq!(
1606 total_run_len,
1607 result.len(),
1608 "Sum of run lengths ({}) should equal result byte length ({})",
1609 total_run_len,
1610 result.len()
1611 );
1612 }
1613
1614 #[test]
1615 fn test_multiline_truncation_trailing_newline() {
1616 let mut wrapper = build_wrapper();
1617
1618 let text: &str = "hello\nworld\n";
1621 let wrap_width = px(72.);
1622 let max_lines: usize = 2;
1623
1624 let runs = generate_test_runs(&[text.len()]);
1625 let (result, _) = wrapper.truncate_wrapped_line(
1626 text.into(),
1627 wrap_width,
1628 max_lines,
1629 "\u{2026}",
1630 &runs,
1631 TruncateFrom::End,
1632 );
1633
1634 assert!(
1635 !result.ends_with('\u{2026}'),
1636 "Trailing newline with no content should not add ellipsis: '{}'",
1637 result
1638 );
1639 }
1640
1641 #[test]
1642 fn test_multiline_truncation_newline_fits_exactly() {
1643 let mut wrapper = build_wrapper();
1644
1645 let text: &str = "hello\nworld";
1648 let wrap_width = px(72.);
1649 let max_lines: usize = 2;
1650
1651 let runs = generate_test_runs(&[text.len()]);
1652 let (result, _) = wrapper.truncate_wrapped_line(
1653 text.into(),
1654 wrap_width,
1655 max_lines,
1656 "\u{2026}",
1657 &runs,
1658 TruncateFrom::End,
1659 );
1660
1661 assert_eq!(
1662 result.as_ref(),
1663 text,
1664 "Text that fits exactly should not be modified: '{}'",
1665 result
1666 );
1667 }
1668
1669 #[test]
1670 fn test_extra_columns_overflow_guard() {
1671 let mut wrapper = build_wrapper();
1672 let space_width = wrapper.width_for_char(' ');
1673
1674 let text = " ab cd ef gh";
1676 let wrap_width = space_width * 10.0;
1677
1678 assert_eq!(
1685 wrapper
1686 .wrap_line(
1687 &[LineFragment::text(text)],
1688 wrap_width,
1689 IndentAdjustment::ExtraColumns(8),
1690 )
1691 .collect::<Vec<_>>(),
1692 &[Boundary::new(9, 0)]
1693 );
1694
1695 assert_eq!(
1704 wrapper
1705 .wrap_line(
1706 &[LineFragment::text(text)],
1707 wrap_width,
1708 IndentAdjustment::ExtraColumns(2),
1709 )
1710 .collect::<Vec<_>>(),
1711 &[
1712 Boundary::new(9, 8),
1713 Boundary::new(11, 8),
1714 Boundary::new(13, 8),
1715 Boundary::new(15, 8),
1716 ]
1717 );
1718
1719 let full_width_text = " 🦀🦀🦀🦀🦀🦀🦀🦀";
1732 assert_eq!(
1733 wrapper
1734 .wrap_line(
1735 &[LineFragment::text(full_width_text)],
1736 wrap_width,
1737 IndentAdjustment::ExtraColumns(8),
1738 )
1739 .collect::<Vec<_>>(),
1740 &[Boundary::new(17, 0)]
1741 );
1742
1743 assert_eq!(
1750 wrapper
1751 .wrap_line(
1752 &[LineFragment::text("a abcdefghi")],
1753 wrap_width,
1754 IndentAdjustment::ExtraColumns(1),
1755 )
1756 .collect::<Vec<_>>(),
1757 &[Boundary::new(2, 1)]
1758 );
1759
1760 assert_eq!(
1767 wrapper
1768 .wrap_line(
1769 &[LineFragment::text("a abcdefghi")],
1770 wrap_width,
1771 IndentAdjustment::ExtraColumns(2),
1772 )
1773 .collect::<Vec<_>>(),
1774 &[Boundary::new(2, 0)]
1775 );
1776
1777 let carried_text = "a abcdefghij";
1789 assert_eq!(
1790 wrapper
1791 .wrap_line(
1792 &[LineFragment::text(carried_text)],
1793 wrap_width,
1794 IndentAdjustment::ExtraColumns(8),
1795 )
1796 .collect::<Vec<_>>(),
1797 &[Boundary::new(2, 0)]
1798 );
1799
1800 let element_fragments = [
1818 LineFragment::text("abcdefghijk "),
1819 LineFragment::element(space_width * 8.0, 1),
1820 LineFragment::text("z 12"),
1821 ];
1822 assert_eq!(
1823 wrapper
1824 .wrap_line(
1825 &element_fragments,
1826 wrap_width,
1827 IndentAdjustment::ExtraColumns(8),
1828 )
1829 .collect::<Vec<_>>(),
1830 &[
1831 Boundary::new(10, 8),
1832 Boundary::new(12, 0),
1833 Boundary::new(15, 8),
1834 ]
1835 );
1836
1837 let spaces = " "; assert_eq!(
1841 wrapper
1842 .wrap_line(
1843 &[LineFragment::text(spaces)],
1844 wrap_width,
1845 IndentAdjustment::ExtraColumns(8),
1846 )
1847 .collect::<Vec<_>>(),
1848 &[Boundary::new(10, 0)]
1849 );
1850
1851 let multiline_indent_text = " ab cd ef gh";
1860 assert_eq!(
1861 wrapper
1862 .wrap_line(
1863 &[LineFragment::text(multiline_indent_text)],
1864 wrap_width,
1865 IndentAdjustment::ExtraColumns(2),
1866 )
1867 .collect::<Vec<_>>(),
1868 &[
1869 Boundary::new(10, 0),
1870 Boundary::new(20, 6),
1871 Boundary::new(23, 6),
1872 ]
1873 );
1874
1875 let oversized_suffix_fragments = [
1892 LineFragment::text("aaaaaaaaaaaaaaaaaaa a"),
1893 LineFragment::element(space_width * 10.0, 1),
1894 ];
1895 assert_eq!(
1896 wrapper
1897 .wrap_line(
1898 &oversized_suffix_fragments,
1899 wrap_width,
1900 IndentAdjustment::ExtraColumns(2),
1901 )
1902 .collect::<Vec<_>>(),
1903 &[
1904 Boundary::new(10, 2),
1905 Boundary::new(18, 2),
1906 Boundary::new(21, 0),
1907 ]
1908 );
1909
1910 assert_eq!(
1911 wrapper
1912 .wrap_line(
1913 &[LineFragment::text(" aaaaaaaa")],
1914 space_width * 4.0,
1915 IndentAdjustment::ExtraColumns(u32::MAX),
1916 )
1917 .collect::<Vec<_>>(),
1918 &[Boundary::new(4, 0), Boundary::new(8, 0)]
1919 );
1920 }
1921}