rab/tui/
visual_truncate.rs1use crate::tui::util::visible_width;
8
9pub struct VisualTruncateResult<'a> {
11 pub lines: Vec<&'a str>,
13 pub skipped: usize,
15}
16
17pub fn visual_line_count(line: &str, width: usize) -> usize {
20 if width == 0 {
21 return 1;
22 }
23 let vis = visible_width(line);
24 if vis == 0 {
25 return 1;
26 }
27 vis.div_ceil(width)
28}
29
30pub fn truncate_to_visual_lines<'a>(
39 lines: &'a [&'a str],
40 width: usize,
41 max_visual_lines: usize,
42) -> (Vec<&'a str>, usize) {
43 if lines.is_empty() || max_visual_lines == 0 {
44 return (vec![], 0);
45 }
46
47 let visual_counts: Vec<usize> = lines.iter().map(|l| visual_line_count(l, width)).collect();
49
50 let total_visual: usize = visual_counts.iter().sum();
51
52 if total_visual <= max_visual_lines {
54 return (lines.to_vec(), 0);
55 }
56
57 let mut budget = max_visual_lines;
59 let mut start = lines.len();
60
61 for (i, &vc) in visual_counts.iter().enumerate().rev() {
62 if vc > budget {
63 break;
66 }
67 budget -= vc;
68 start = i;
69 }
70
71 (lines[start..].to_vec(), start)
72}
73
74pub fn truncate_preview<'a>(
78 lines: &'a [&'a str],
79 width: usize,
80 max_visual_lines: usize,
81) -> (Vec<&'a str>, usize) {
82 truncate_to_visual_lines(lines, width, max_visual_lines)
83}
84
85pub fn format_hidden_hint(hidden: usize, expand_key: &str) -> String {
88 if expand_key.is_empty() {
89 format!("... {} earlier lines", hidden)
90 } else {
91 format!("... ({} earlier lines, {} to expand)", hidden, expand_key)
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 #[test]
100 fn test_visual_line_count_ascii() {
101 assert_eq!(visual_line_count("hello", 80), 1);
102 assert_eq!(visual_line_count("", 80), 1);
103 }
104
105 #[test]
106 fn test_visual_line_count_wrapping() {
107 assert_eq!(visual_line_count(&"a".repeat(100), 80), 2);
108 assert_eq!(visual_line_count(&"a".repeat(160), 80), 2);
109 assert_eq!(visual_line_count(&"a".repeat(161), 80), 3);
110 }
111
112 #[test]
113 fn test_visual_line_count_zero_width() {
114 assert_eq!(visual_line_count("hello", 0), 1);
115 }
116
117 #[test]
118 fn test_truncate_to_visual_lines_no_truncation() {
119 let lines = vec!["short", "also short"];
120 let (selected, hidden) = truncate_to_visual_lines(&lines, 80, 10);
121 assert_eq!(selected.len(), 2);
122 assert_eq!(hidden, 0);
123 }
124
125 #[test]
126 fn test_truncate_to_visual_lines_with_wrapping() {
127 let line1 = "a".repeat(100);
128 let line2 = "b".repeat(100);
129 let line3 = "c".repeat(100);
130 let lines = vec![line1.as_str(), line2.as_str(), line3.as_str()];
131
132 let (selected, hidden) = truncate_to_visual_lines(&lines, 80, 4);
134 assert_eq!(selected.len(), 2);
135 assert_eq!(hidden, 1);
136 assert_eq!(selected[0], line2.as_str());
137 assert_eq!(selected[1], line3.as_str());
138 }
139
140 #[test]
141 fn test_truncate_to_visual_lines_exact_fit() {
142 let line1 = "a".repeat(100);
143 let line2 = "b".repeat(100);
144 let lines = vec![line1.as_str(), line2.as_str()];
145 let (selected, hidden) = truncate_to_visual_lines(&lines, 80, 4);
146 assert_eq!(selected.len(), 2);
147 assert_eq!(hidden, 0);
148 }
149
150 #[test]
151 fn test_truncate_to_visual_lines_empty() {
152 let lines: Vec<&str> = vec![];
153 let (selected, hidden) = truncate_to_visual_lines(&lines, 80, 5);
154 assert!(selected.is_empty());
155 assert_eq!(hidden, 0);
156 }
157
158 #[test]
159 fn test_truncate_to_visual_lines_mixed_widths() {
160 let short1 = "short";
161 let long = "x".repeat(100);
162 let short2 = "also short";
163 let lines = vec![short1, long.as_str(), short2];
164
165 let (selected, hidden) = truncate_to_visual_lines(&lines, 80, 3);
166 assert_eq!(selected.len(), 2);
167 assert_eq!(hidden, 1);
168 assert_eq!(selected[0], long.as_str());
169 assert_eq!(selected[1], short2);
170 }
171
172 #[test]
173 fn test_format_hidden_hint() {
174 let hint = format_hidden_hint(12, "C-O");
175 assert!(hint.contains("12"));
176 assert!(hint.contains("C-O"));
177
178 let hint = format_hidden_hint(5, "");
179 assert!(hint.contains("5"));
180 assert!(!hint.contains("to expand"));
181 }
182}