Skip to main content

vtcode_tui/core_tui/session/utils/
line_truncation.rs

1use ratatui::prelude::*;
2use unicode_width::UnicodeWidthStr;
3
4/// Ellipsis character used to indicate truncated text.
5const ELLIPSIS: char = '…';
6
7/// Truncate a styled line to `max_width` and append an ellipsis on overflow.
8///
9/// This function preserves a fast no-overflow path (returns original line
10/// unchanged if it fits) and uses `truncate_line_to_width` for overflow cases,
11/// appending `…` (ellipsis character) when truncation occurs.
12///
13/// # Arguments
14///
15/// * `line` - The line to potentially truncate
16/// * `max_width` - The maximum width in display columns
17///
18/// # Returns
19///
20/// The original line if it fits, or a truncated line with ellipsis appended
21pub(crate) fn truncate_line_with_ellipsis_if_overflow(
22    line: Line<'static>,
23    max_width: usize,
24) -> Line<'static> {
25    let total_width: usize = line.spans.iter().map(|s| s.width()).sum();
26    if total_width <= max_width {
27        // Fast path: no truncation needed
28        return line;
29    }
30
31    // Reserve space for ellipsis (1 character width)
32    let available_width = max_width.saturating_sub(ELLIPSIS.len_utf8());
33    if available_width == 0 {
34        // Edge case: not enough room for even the ellipsis
35        return Line::from(Span::raw(ELLIPSIS.to_string()));
36    }
37
38    let mut truncated_line = truncate_line_to_width(line, available_width);
39    truncated_line.spans.push(Span::raw(ELLIPSIS.to_string()));
40    truncated_line
41}
42
43/// Truncate a `Line` to fit within `max_width` display columns.
44///
45/// Used for table lines where word-wrapping would break the box-drawing
46/// alignment. Spans are trimmed at the character boundary that exceeds the
47/// width; any remaining spans are dropped.
48fn truncate_line_to_width(line: Line<'static>, max_width: usize) -> Line<'static> {
49    let total: usize = line.spans.iter().map(|s| s.width()).sum();
50    if total <= max_width {
51        return line;
52    }
53
54    let mut remaining = max_width;
55    let mut truncated_spans: Vec<Span<'static>> = Vec::with_capacity(line.spans.len());
56    for span in line.spans {
57        let span_width = span.width();
58        if span_width <= remaining {
59            remaining -= span_width;
60            truncated_spans.push(span);
61        } else {
62            // Truncate within this span at a char boundary
63            let mut chars_width = 0usize;
64            let mut byte_end = 0usize;
65            for ch in span.content.chars() {
66                let cw = UnicodeWidthStr::width(ch.encode_utf8(&mut [0u8; 4]) as &str);
67                if chars_width + cw > remaining {
68                    break;
69                }
70                chars_width += cw;
71                byte_end += ch.len_utf8();
72            }
73            if byte_end > 0 {
74                let fragment: String = span.content[..byte_end].to_string();
75                truncated_spans.push(Span::styled(fragment, span.style));
76            }
77            break;
78        }
79    }
80    Line::from(truncated_spans)
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn test_no_truncation_when_fits() {
89        let line = Line::from("Hello");
90        let result = truncate_line_with_ellipsis_if_overflow(line.clone(), 10);
91        assert_eq!(result.spans.len(), 1);
92        assert_eq!(result.spans[0].content, "Hello");
93    }
94
95    #[test]
96    fn test_truncation_with_ellipsis() {
97        let line = Line::from("Hello World");
98        let result = truncate_line_with_ellipsis_if_overflow(line, 8);
99        let result_text: String = result.spans.iter().map(|s| s.content.as_ref()).collect();
100        assert!(result_text.ends_with(ELLIPSIS));
101        assert!(result_text.len() < "Hello World".len());
102    }
103
104    #[test]
105    fn test_ellipsis_only_when_very_narrow() {
106        let line = Line::from("Hello");
107        let result = truncate_line_with_ellipsis_if_overflow(line, 1);
108        assert_eq!(result.spans[0].content, ELLIPSIS.to_string());
109    }
110}