1use crate::{FontId, TextRun, TextSystem};
2use gpui_shared_string::SharedString;
3use gpui_types::{Pixels, px};
4use std::{
5 borrow::Cow,
6 collections::HashMap,
7 iter,
8 ops::{Deref, DerefMut},
9 sync::Arc,
10};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14pub enum TruncateFrom {
15 Start,
17 End,
19 Middle,
21}
22
23pub struct LineWrapper {
25 text_system: Arc<dyn TextSystem>,
26 pub(crate) font_id: FontId,
27 pub(crate) font_size: Pixels,
28 cached_ascii_char_widths: [Option<Pixels>; 128],
29 cached_other_char_widths: HashMap<char, Pixels>,
30}
31
32impl LineWrapper {
33 pub const MAX_INDENT: u32 = 256;
35
36 pub fn new(font_id: FontId, font_size: Pixels, text_system: Arc<dyn TextSystem>) -> Self {
38 Self {
39 text_system,
40 font_id,
41 font_size,
42 cached_ascii_char_widths: [None; 128],
43 cached_other_char_widths: HashMap::default(),
44 }
45 }
46
47 pub fn wrap_line<'a>(
49 &'a mut self,
50 fragments: &'a [LineFragment],
51 wrap_width: Pixels,
52 ) -> impl Iterator<Item = Boundary> + 'a {
53 let mut width = px(0.);
54 let mut first_non_whitespace_ix = None;
55 let mut indent = None;
56 let mut last_candidate_ix = 0;
57 let mut last_candidate_width = px(0.);
58 let mut last_wrap_ix = 0;
59 let mut prev_c = '\0';
60 let mut index = 0;
61 let mut candidates = fragments
62 .iter()
63 .flat_map(move |fragment| fragment.wrap_boundary_candidates())
64 .peekable();
65 iter::from_fn(move || {
66 for candidate in candidates.by_ref() {
67 let ix = index;
68 index += candidate.len_utf8();
69 let mut new_prev_c = prev_c;
70 let item_width = match candidate {
71 WrapBoundaryCandidate::Char { character: c } => {
72 if c == '\n' {
73 continue;
74 }
75
76 if Self::is_word_char(c) {
77 if prev_c == ' ' && c != ' ' && first_non_whitespace_ix.is_some() {
78 last_candidate_ix = ix;
79 last_candidate_width = width;
80 }
81 } else {
82 if c != ' ' && first_non_whitespace_ix.is_some() {
84 last_candidate_ix = ix;
85 last_candidate_width = width;
86 }
87 }
88
89 if c != ' ' && first_non_whitespace_ix.is_none() {
90 first_non_whitespace_ix = Some(ix);
91 }
92
93 new_prev_c = c;
94
95 self.width_for_char(c)
96 }
97 WrapBoundaryCandidate::Element {
98 width: element_width,
99 ..
100 } => {
101 if prev_c == ' ' && first_non_whitespace_ix.is_some() {
102 last_candidate_ix = ix;
103 last_candidate_width = width;
104 }
105
106 if first_non_whitespace_ix.is_none() {
107 first_non_whitespace_ix = Some(ix);
108 }
109
110 element_width
111 }
112 };
113
114 width += item_width;
115 if width > wrap_width && ix > last_wrap_ix {
116 if let (None, Some(first_non_whitespace_ix)) = (indent, first_non_whitespace_ix)
117 {
118 indent = Some(
119 Self::MAX_INDENT.min((first_non_whitespace_ix - last_wrap_ix) as u32),
120 );
121 }
122
123 if last_candidate_ix > 0 {
124 last_wrap_ix = last_candidate_ix;
125 width -= last_candidate_width;
126 last_candidate_ix = 0;
127 } else {
128 last_wrap_ix = ix;
129 width = item_width;
130 }
131
132 if let Some(indent) = indent {
133 width += self.width_for_char(' ') * indent as f32;
134 }
135
136 return Some(Boundary::new(last_wrap_ix, indent.unwrap_or(0)));
137 }
138
139 prev_c = new_prev_c;
140 }
141
142 None
143 })
144 }
145
146 pub fn should_truncate_line(
150 &mut self,
151 line: &str,
152 truncate_width: Pixels,
153 truncation_affix: &str,
154 truncate_from: TruncateFrom,
155 ) -> Option<usize> {
156 let mut width = px(0.);
157 let suffix_width = truncation_affix
158 .chars()
159 .map(|c| self.width_for_char(c))
160 .fold(px(0.0), |a, x| a + x);
161 let mut truncate_ix = 0;
162
163 match truncate_from {
164 TruncateFrom::Start => {
165 for (ix, c) in line.char_indices().rev() {
166 if width + suffix_width < truncate_width {
167 truncate_ix = ix;
168 }
169
170 let char_width = self.width_for_char(c);
171 width += char_width;
172
173 if width.floor() > truncate_width {
174 return Some(truncate_ix);
175 }
176 }
177 }
178 TruncateFrom::End => {
179 for (ix, c) in line.char_indices() {
180 if width + suffix_width < truncate_width {
181 truncate_ix = ix;
182 }
183
184 let char_width = self.width_for_char(c);
185 width += char_width;
186
187 if width.floor() > truncate_width {
188 return Some(truncate_ix);
189 }
190 }
191 }
192 TruncateFrom::Middle => {}
193 }
194
195 None
196 }
197
198 fn should_truncate_line_middle(
199 &mut self,
200 line: &str,
201 truncate_width: Pixels,
202 truncation_affix: &str,
203 ) -> Option<(usize, usize)> {
204 let suffix_width = truncation_affix
205 .chars()
206 .map(|c| self.width_for_char(c))
207 .fold(px(0.0), |a, x| a + x);
208
209 let total_width: Pixels = line
210 .chars()
211 .map(|c| self.width_for_char(c))
212 .fold(px(0.0), |a, x| a + x);
213
214 if total_width <= truncate_width {
215 return None;
216 }
217
218 let content_budget = truncate_width - suffix_width;
219 if content_budget <= px(0.) {
220 return Some((0, line.len()));
221 }
222
223 let front_budget = content_budget * (2.0 / 3.0);
224 let back_budget = content_budget - front_budget;
225
226 let mut front_width = px(0.);
227 let mut front_end_ix = 0usize;
228 for (ix, c) in line.char_indices() {
229 let char_width = self.width_for_char(c);
230 if front_width + char_width > front_budget {
231 break;
232 }
233 front_width += char_width;
234 front_end_ix = ix + c.len_utf8();
235 }
236
237 let mut back_width = px(0.);
238 let mut back_start_ix = line.len();
239 for (ix, c) in line.char_indices().rev() {
240 let char_width = self.width_for_char(c);
241 if back_width + char_width > back_budget {
242 break;
243 }
244 back_width += char_width;
245 back_start_ix = ix;
246 }
247
248 if front_end_ix >= back_start_ix {
249 return Some((0, line.len()));
250 }
251
252 Some((front_end_ix, back_start_ix))
253 }
254
255 pub fn truncate_line<'a>(
257 &mut self,
258 line: SharedString,
259 truncate_width: Pixels,
260 truncation_affix: &str,
261 runs: &'a [TextRun],
262 truncate_from: TruncateFrom,
263 ) -> (SharedString, Cow<'a, [TextRun]>) {
264 if truncate_from == TruncateFrom::Middle {
265 if let Some((front_end_ix, back_start_ix)) =
266 self.should_truncate_line_middle(&line, truncate_width, truncation_affix)
267 {
268 let result = SharedString::from(format!(
269 "{}{truncation_affix}{}",
270 &line[..front_end_ix],
271 &line[back_start_ix..]
272 ));
273 let mut runs = runs.to_vec();
274 update_runs_after_middle_truncation(
275 truncation_affix,
276 &mut runs,
277 front_end_ix,
278 back_start_ix,
279 );
280 return (result, Cow::Owned(runs));
281 } else {
282 return (line, Cow::Borrowed(runs));
283 }
284 }
285
286 if let Some(truncate_ix) =
287 self.should_truncate_line(&line, truncate_width, truncation_affix, truncate_from)
288 {
289 let result = match truncate_from {
290 TruncateFrom::Start => SharedString::from(format!(
291 "{truncation_affix}{}",
292 &line[line.ceil_char_boundary(truncate_ix + 1)..]
293 )),
294 TruncateFrom::End => SharedString::from(format!(
295 "{}{truncation_affix}",
296 line[..truncate_ix]
297 .trim_end_matches(|c: char| c.is_whitespace() || c.is_ascii_punctuation())
298 )),
299 TruncateFrom::Middle => unreachable!("Middle truncation is handled above"),
300 };
301 let mut runs = runs.to_vec();
302 update_runs_after_truncation(&result, truncation_affix, &mut runs, truncate_from);
303 (result, Cow::Owned(runs))
304 } else {
305 (line, Cow::Borrowed(runs))
306 }
307 }
308
309 pub fn truncate_wrapped_line<'a>(
319 &mut self,
320 text: SharedString,
321 wrap_width: Pixels,
322 max_lines: usize,
323 truncation_affix: &str,
324 runs: &'a [TextRun],
325 truncate_from: TruncateFrom,
326 ) -> (SharedString, Cow<'a, [TextRun]>) {
327 if max_lines <= 1 || truncate_from == TruncateFrom::Start {
328 return self.truncate_line(
329 text,
330 wrap_width * max_lines,
331 truncation_affix,
332 runs,
333 truncate_from,
334 );
335 }
336 if truncate_from == TruncateFrom::Middle {
337 return self.truncate_line(text, wrap_width, truncation_affix, runs, truncate_from);
338 }
339
340 let affix_width: Pixels = truncation_affix
341 .chars()
342 .map(|c| self.width_for_char(c))
343 .sum();
344
345 let mut width = px(0.);
346 let mut line = 0usize;
347 let mut first_non_whitespace_ix = None;
348 let mut last_candidate_ix = 0usize;
349 let mut last_candidate_width = px(0.);
350 let mut last_wrap_ix = 0usize;
351 let mut prev_c = '\0';
352 let mut indent: Option<u32> = None;
353 let mut truncate_ix = 0usize;
354
355 for (ix, c) in text.char_indices() {
356 if c == '\n' {
357 if line >= max_lines - 1 && !text[ix + 1..].trim().is_empty() {
358 let truncated = text[..truncate_ix]
361 .trim_end_matches(|c: char| c.is_whitespace() || c.is_ascii_punctuation());
362 let result = SharedString::from(format!("{truncated}{truncation_affix}"));
363 let mut runs = runs.to_vec();
364 update_runs_after_truncation(
365 &result,
366 truncation_affix,
367 &mut runs,
368 TruncateFrom::End,
369 );
370 return (result, Cow::Owned(runs));
371 }
372
373 line += 1;
375 width = px(0.);
376 first_non_whitespace_ix = None;
377 last_candidate_ix = 0;
378 last_candidate_width = px(0.);
379 last_wrap_ix = ix + 1;
380 prev_c = '\0';
381 indent = None;
382 truncate_ix = ix + 1;
383 continue;
384 }
385
386 let char_width = self.width_for_char(c);
387
388 if Self::is_word_char(c) {
389 if prev_c == ' ' && first_non_whitespace_ix.is_some() {
390 last_candidate_ix = ix;
391 last_candidate_width = width;
392 }
393 } else if c != ' ' && first_non_whitespace_ix.is_some() {
394 last_candidate_ix = ix;
395 last_candidate_width = width;
396 }
397
398 if c != ' ' && first_non_whitespace_ix.is_none() {
399 first_non_whitespace_ix = Some(ix);
400 }
401
402 width += char_width;
403
404 if line < max_lines - 1 {
405 if width > wrap_width && ix > last_wrap_ix {
407 if let (None, Some(first_nw)) = (indent, first_non_whitespace_ix) {
408 indent = Some(Self::MAX_INDENT.min((first_nw - last_wrap_ix) as u32));
409 }
410
411 if last_candidate_ix > last_wrap_ix {
412 last_wrap_ix = last_candidate_ix;
413 width -= last_candidate_width;
414 last_candidate_ix = 0;
415 } else {
416 last_wrap_ix = ix;
417 width = char_width;
418 }
419
420 if let Some(ind) = indent {
421 width += self.width_for_char(' ') * ind as f32;
422 }
423
424 line += 1;
425 truncate_ix = last_wrap_ix;
426 }
427 } else {
428 if width + affix_width <= wrap_width {
431 truncate_ix = ix + c.len_utf8();
432 }
433
434 if width > wrap_width {
435 let truncated = text[..truncate_ix]
436 .trim_end_matches(|c: char| c.is_whitespace() || c.is_ascii_punctuation());
437 let result = SharedString::from(format!("{truncated}{truncation_affix}"));
438 let mut runs = runs.to_vec();
439 update_runs_after_truncation(
440 &result,
441 truncation_affix,
442 &mut runs,
443 TruncateFrom::End,
444 );
445 return (result, Cow::Owned(runs));
446 }
447 }
448
449 prev_c = c;
450 }
451
452 (text, Cow::Borrowed(runs))
454 }
455
456 pub fn is_word_char(c: char) -> bool {
460 c.is_ascii_alphanumeric() ||
462 matches!(c, '\u{00C0}'..='\u{00FF}') ||
466 matches!(c, '\u{0100}'..='\u{017F}') ||
469 matches!(c, '\u{0180}'..='\u{024F}') ||
472 matches!(c, '\u{0400}'..='\u{04FF}') ||
475
476 matches!(c, '\u{1E00}'..='\u{1EFF}') || matches!(c, '\u{0300}'..='\u{036F}') || matches!(c, '\u{0980}'..='\u{09FF}') ||
482
483 matches!(c, '-' | '_' | '.' | '\'' | '’' | '‘' | '$' | '%' | '@' | '#' | '^' | '~' | ',' | '=' | ':' | ';') ||
488 matches!(c, '!' | ')' | ']' | '}' | '"' | '”' | '»' | '…') ||
494 matches!(c, '⋯') ||
496
497 matches!(c, '\u{202F}' | '\u{00A0}' | '\u{2011}')
499 }
500
501 #[inline(always)]
503 pub fn width_for_char(&mut self, c: char) -> Pixels {
504 if (c as u32) < 128 {
505 if let Some(cached_width) = self.cached_ascii_char_widths[c as usize] {
506 cached_width
507 } else {
508 let width = self
509 .text_system
510 .layout_width(self.font_id, self.font_size, c);
511 self.cached_ascii_char_widths[c as usize] = Some(width);
512 width
513 }
514 } else if let Some(cached_width) = self.cached_other_char_widths.get(&c) {
515 *cached_width
516 } else {
517 let width = self
518 .text_system
519 .layout_width(self.font_id, self.font_size, c);
520 self.cached_other_char_widths.insert(c, width);
521 width
522 }
523 }
524}
525
526pub fn update_runs_after_truncation(
529 result: &str,
530 ellipsis: &str,
531 runs: &mut Vec<TextRun>,
532 truncate_from: TruncateFrom,
533) {
534 let mut truncate_at = result.len() - ellipsis.len();
535 match truncate_from {
536 TruncateFrom::Start => {
537 for (run_index, run) in runs.iter_mut().enumerate().rev() {
538 if run.len <= truncate_at {
539 truncate_at -= run.len;
540 } else {
541 run.len = truncate_at + ellipsis.len();
542 runs.splice(..run_index, std::iter::empty());
543 break;
544 }
545 }
546 }
547 TruncateFrom::End => {
548 for (run_index, run) in runs.iter_mut().enumerate() {
549 if run.len <= truncate_at {
550 truncate_at -= run.len;
551 } else {
552 run.len = truncate_at + ellipsis.len();
553 runs.truncate(run_index + 1);
554 break;
555 }
556 }
557 }
558 TruncateFrom::Middle => {
559 unreachable!("Middle truncation calls this function with TruncateFrom::End directly")
560 }
561 }
562}
563
564fn update_runs_after_middle_truncation(
565 ellipsis: &str,
566 runs: &mut Vec<TextRun>,
567 front_end_ix: usize,
568 back_start_ix: usize,
569) {
570 let original_runs = std::mem::take(runs);
571 let mut result_runs: Vec<TextRun> = Vec::with_capacity(original_runs.len());
572
573 let mut front_remaining = front_end_ix;
577 let mut front_done = false;
578 for run in &original_runs {
579 if front_done {
580 break;
581 }
582 if run.len <= front_remaining {
583 result_runs.push(run.clone());
584 front_remaining -= run.len;
585 } else {
586 let mut partial = run.clone();
587 partial.len = front_remaining + ellipsis.len();
588 result_runs.push(partial);
589 front_done = true;
590 }
591 }
592 if !front_done {
593 if let Some(last) = result_runs.last_mut() {
596 last.len += ellipsis.len();
597 } else if let Some(first) = original_runs.first() {
598 let mut affix_run = first.clone();
599 affix_run.len = ellipsis.len();
600 result_runs.push(affix_run);
601 }
602 }
603
604 let mut byte_pos = 0usize;
607 for run in &original_runs {
608 let run_end = byte_pos + run.len;
609 if run_end > back_start_ix {
610 if byte_pos < back_start_ix {
611 let mut partial = run.clone();
613 partial.len = run_end - back_start_ix;
614 result_runs.push(partial);
615 } else {
616 result_runs.push(run.clone());
617 }
618 }
619 byte_pos = run_end;
620 }
621
622 *runs = result_runs;
623}
624
625pub enum LineFragment<'a> {
627 Text {
629 text: &'a str,
631 },
632 Element {
634 width: Pixels,
636 len_utf8: usize,
638 },
639}
640
641impl<'a> LineFragment<'a> {
642 pub fn text(text: &'a str) -> Self {
644 LineFragment::Text { text }
645 }
646
647 pub fn element(width: Pixels, len_utf8: usize) -> Self {
649 LineFragment::Element { width, len_utf8 }
650 }
651
652 fn wrap_boundary_candidates(&self) -> impl Iterator<Item = WrapBoundaryCandidate> {
653 let text = match self {
654 LineFragment::Text { text } => text,
655 LineFragment::Element { .. } => "\0",
656 };
657 text.chars().map(move |character| {
658 if let LineFragment::Element { width, len_utf8 } = self {
659 WrapBoundaryCandidate::Element {
660 width: *width,
661 len_utf8: *len_utf8,
662 }
663 } else {
664 WrapBoundaryCandidate::Char { character }
665 }
666 })
667 }
668}
669
670enum WrapBoundaryCandidate {
671 Char { character: char },
672 Element { width: Pixels, len_utf8: usize },
673}
674
675impl WrapBoundaryCandidate {
676 pub fn len_utf8(&self) -> usize {
677 match self {
678 WrapBoundaryCandidate::Char { character } => character.len_utf8(),
679 WrapBoundaryCandidate::Element { len_utf8: len, .. } => *len,
680 }
681 }
682}
683
684#[derive(Copy, Clone, Debug, PartialEq, Eq)]
686pub struct Boundary {
687 pub ix: usize,
689 pub next_indent: u32,
691}
692
693impl Boundary {
694 pub fn new(ix: usize, next_indent: u32) -> Self {
696 Self { ix, next_indent }
697 }
698}
699
700pub struct LineWrapperHandle {
702 wrapper: Option<LineWrapper>,
703 recycle: Option<Box<dyn FnOnce(LineWrapper) + Send + Sync>>,
704}
705
706impl LineWrapperHandle {
707 pub fn new(
709 wrapper: LineWrapper,
710 recycle: impl FnOnce(LineWrapper) + Send + Sync + 'static,
711 ) -> Self {
712 Self {
713 wrapper: Some(wrapper),
714 recycle: Some(Box::new(recycle)),
715 }
716 }
717}
718
719impl Deref for LineWrapperHandle {
720 type Target = LineWrapper;
721
722 fn deref(&self) -> &Self::Target {
723 self.wrapper.as_ref().expect("wrapper present until drop")
724 }
725}
726
727impl DerefMut for LineWrapperHandle {
728 fn deref_mut(&mut self) -> &mut Self::Target {
729 self.wrapper.as_mut().expect("wrapper present until drop")
730 }
731}
732
733impl Drop for LineWrapperHandle {
734 fn drop(&mut self) {
735 let wrapper = self.wrapper.take().expect("wrapper present until drop");
736 let recycle = self.recycle.take().expect("recycle present until drop");
737 recycle(wrapper);
738 }
739}