1pub const DEFAULT_MAX_LINES: usize = 2000;
13
14pub const DEFAULT_MAX_BYTES: usize = 50 * 1024;
17
18pub const GREP_MAX_LINE_LENGTH: usize = 500;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
26#[serde(rename_all = "lowercase")]
27pub enum TruncationLimit {
28 Lines,
29 Bytes,
30}
31
32#[derive(Debug, Clone, Copy, Default)]
34pub struct TruncationOptions {
35 pub max_lines: Option<usize>,
36 pub max_bytes: Option<usize>,
37}
38
39impl TruncationOptions {
40 fn resolve(self) -> (usize, usize) {
41 (
42 self.max_lines.unwrap_or(DEFAULT_MAX_LINES),
43 self.max_bytes.unwrap_or(DEFAULT_MAX_BYTES),
44 )
45 }
46}
47
48#[derive(Debug, Clone, serde::Serialize)]
53pub struct TruncationResult {
54 pub content: String,
55 pub truncated: bool,
56 pub truncated_by: Option<TruncationLimit>,
57 pub total_lines: usize,
58 pub total_bytes: usize,
59 pub output_lines: usize,
60 pub output_bytes: usize,
61 pub last_line_partial: bool,
64 pub first_line_exceeds_limit: bool,
67 pub max_lines: usize,
68 pub max_bytes: usize,
69}
70
71impl TruncationResult {
72 fn not_truncated(content: String, max_lines: usize, max_bytes: usize) -> Self {
73 let total_bytes = content.len();
74 let total_lines = split_lines_for_counting(&content).len();
75 Self {
76 output_bytes: total_bytes,
77 output_lines: total_lines,
78 content,
79 truncated: false,
80 truncated_by: None,
81 total_lines,
82 total_bytes,
83 last_line_partial: false,
84 first_line_exceeds_limit: false,
85 max_lines,
86 max_bytes,
87 }
88 }
89}
90
91pub fn split_lines_for_counting(content: &str) -> Vec<&str> {
94 if content.is_empty() {
95 return Vec::new();
96 }
97 let mut parts: Vec<&str> = content.split('\n').collect();
98 if content.ends_with('\n') {
99 if parts.last().map(|s| s.is_empty()).unwrap_or(false) {
101 parts.pop();
102 }
103 }
104 parts
105}
106
107pub fn format_size(bytes: usize) -> String {
109 if bytes < 1024 {
110 format!("{}B", bytes)
111 } else if bytes < 1024 * 1024 {
112 format!("({:.1})KB", bytes as f64 / 1024.0)
113 } else {
114 format!("({:.1})MB", bytes as f64 / (1024.0 * 1024.0))
115 }
116}
117
118pub fn truncate_head(content: &str, options: TruncationOptions) -> TruncationResult {
122 let (max_lines, max_bytes) = options.resolve();
123 let total_bytes = content.len();
124 let lines = split_lines_for_counting(content);
125 let total_lines = lines.len();
126
127 if total_lines <= max_lines && total_bytes <= max_bytes {
128 return TruncationResult::not_truncated(content.to_string(), max_lines, max_bytes);
129 }
130
131 let first_line_bytes = lines.first().map(|l| l.len()).unwrap_or(0);
133 if first_line_bytes > max_bytes {
134 return TruncationResult {
135 content: String::new(),
136 truncated: true,
137 truncated_by: Some(TruncationLimit::Bytes),
138 total_lines,
139 total_bytes,
140 output_lines: 0,
141 output_bytes: 0,
142 last_line_partial: false,
143 first_line_exceeds_limit: true,
144 max_lines,
145 max_bytes,
146 };
147 }
148
149 let mut output_lines_arr: Vec<&str> = Vec::new();
150 let mut output_bytes_count = 0usize;
151 let mut truncated_by: Option<TruncationLimit> = None;
152
153 let upper = std::cmp::min(lines.len(), max_lines);
154 for i in 0..upper {
155 let line_bytes = lines[i].len() + if i > 0 { 1 } else { 0 };
158 if output_bytes_count + line_bytes > max_bytes {
159 truncated_by = Some(TruncationLimit::Bytes);
160 break;
161 }
162 output_lines_arr.push(lines[i]);
163 output_bytes_count += line_bytes;
164 }
165
166 if output_lines_arr.len() >= max_lines && output_bytes_count <= max_bytes {
170 truncated_by = Some(TruncationLimit::Lines);
171 }
172
173 let output_content = output_lines_arr.join("\n");
174 let final_output_bytes = output_content.len();
175 TruncationResult {
176 content: output_content,
177 truncated: true,
178 truncated_by,
179 total_lines,
180 total_bytes,
181 output_lines: output_lines_arr.len(),
182 output_bytes: final_output_bytes,
183 last_line_partial: false,
184 first_line_exceeds_limit: false,
185 max_lines,
186 max_bytes,
187 }
188}
189
190pub fn truncate_tail(content: &str, options: TruncationOptions) -> TruncationResult {
194 let (max_lines, max_bytes) = options.resolve();
195 let total_bytes = content.len();
196 let lines = split_lines_for_counting(content);
197 let total_lines = lines.len();
198
199 if total_lines <= max_lines && total_bytes <= max_bytes {
200 return TruncationResult::not_truncated(content.to_string(), max_lines, max_bytes);
201 }
202
203 let mut output_lines_arr: Vec<&str> = Vec::new();
204 let mut output_bytes_count = 0usize;
205 let mut truncated_by: Option<TruncationLimit> = None;
206 let mut last_line_partial = false;
207
208 let mut i = lines.len() as isize - 1;
209 while i >= 0 && output_lines_arr.len() < max_lines {
210 let idx = i as usize;
211 let line_bytes = lines[idx].len() + if output_lines_arr.is_empty() { 0 } else { 1 };
214 if output_bytes_count + line_bytes > max_bytes {
215 truncated_by = Some(TruncationLimit::Bytes);
216 if output_lines_arr.is_empty() {
217 let partial = trim_to_last_utf8_bytes(lines[idx], max_bytes);
220 output_lines_arr.insert(0, partial);
221 output_bytes_count = partial.len();
222 last_line_partial = true;
223 }
224 break;
225 }
226 output_lines_arr.insert(0, lines[idx]);
227 output_bytes_count += line_bytes;
228 i -= 1;
229 }
230
231 if output_lines_arr.len() >= max_lines && output_bytes_count <= max_bytes {
232 truncated_by = Some(TruncationLimit::Lines);
233 }
234
235 let output_content: String = output_lines_arr
236 .iter()
237 .map(|s| s.to_string())
238 .collect::<Vec<_>>()
239 .join("\n");
240 let output_content = if last_line_partial {
243 let last = lines.last().copied().unwrap_or("");
247 let partial = trim_to_last_utf8_bytes(last, max_bytes);
248 partial.to_string()
249 } else {
250 output_content
251 };
252 let final_output_bytes = output_content.len();
253 let output_lines_len = if last_line_partial {
254 1
255 } else {
256 count_tail_output_lines(content, max_lines, max_bytes)
259 };
260
261 TruncationResult {
262 content: output_content,
263 truncated: true,
264 truncated_by,
265 total_lines,
266 total_bytes,
267 output_lines: output_lines_len,
268 output_bytes: final_output_bytes,
269 last_line_partial,
270 first_line_exceeds_limit: false,
271 max_lines,
272 max_bytes,
273 }
274}
275
276fn count_tail_output_lines(content: &str, max_lines: usize, _max_bytes: usize) -> usize {
278 let lines = split_lines_for_counting(content);
284 std::cmp::min(lines.len(), max_lines)
285}
286
287pub fn trim_to_last_utf8_bytes(s: &str, max_bytes: usize) -> &str {
291 if s.len() <= max_bytes || max_bytes == 0 {
292 if max_bytes == 0 {
293 return "";
294 }
295 return s;
296 }
297 let mut start = s.len() - max_bytes;
298 let bytes = s.as_bytes();
299 while start < bytes.len() && (bytes[start] & 0xc0) == 0x80 {
302 start += 1;
303 }
304 if start >= bytes.len() {
305 return "";
306 }
307 let mut start = start;
310 while !s.is_char_boundary(start) {
311 if start == 0 {
312 return "";
313 }
314 start -= 1;
315 }
316 &s[start..]
317}
318
319#[derive(Debug, Clone)]
321pub struct TruncateLineOutput {
322 pub text: String,
323 pub was_truncated: bool,
324}
325
326pub fn truncate_line(line: &str, max_chars: usize) -> TruncateLineOutput {
331 let chars: Vec<char> = line.chars().collect();
332 if chars.len() <= max_chars {
333 return TruncateLineOutput {
334 text: line.to_string(),
335 was_truncated: false,
336 };
337 }
338 let head: String = chars.iter().take(max_chars).collect();
339 TruncateLineOutput {
340 text: format!("{}... [truncated]", head),
341 was_truncated: true,
342 }
343}
344
345pub fn truncate_line_default(line: &str) -> TruncateLineOutput {
346 truncate_line(line, GREP_MAX_LINE_LENGTH)
347}
348
349#[cfg(test)]
350mod tests {
351 use super::*;
352
353 #[test]
354 fn split_lines_drops_trailing_empty() {
355 assert_eq!(split_lines_for_counting(""), Vec::<&str>::new());
356 assert_eq!(split_lines_for_counting("a"), vec!["a"]);
357 assert_eq!(split_lines_for_counting("a\nb"), vec!["a", "b"]);
358 assert_eq!(split_lines_for_counting("a\nb\n"), vec!["a", "b"]);
359 assert_eq!(split_lines_for_counting("a\n\n"), vec!["a", ""]);
360 }
361
362 #[test]
363 fn format_size_tiers() {
364 assert_eq!(format_size(0), "0B");
365 assert_eq!(format_size(512), "512B");
366 assert_eq!(format_size(1024), "(1.0)KB");
367 assert_eq!(format_size(1536), "(1.5)KB");
368 assert_eq!(format_size(1024 * 1024), "(1.0)MB");
369 }
370
371 #[test]
372 fn truncate_head_under_limits_is_noop() {
373 let r = truncate_head("a\nb\n", TruncationOptions::default());
374 assert!(!r.truncated);
375 assert_eq!(r.content, "a\nb\n");
376 assert_eq!(r.output_lines, 2);
377 }
378
379 #[test]
380 fn truncate_head_line_limit() {
381 let s: String = (0..3000)
382 .map(|i| format!("line{}", i))
383 .collect::<Vec<_>>()
384 .join("\n");
385 let r = truncate_head(&s, TruncationOptions::default());
386 assert!(r.truncated);
387 assert_eq!(r.truncated_by, Some(TruncationLimit::Lines));
388 assert_eq!(r.output_lines, DEFAULT_MAX_LINES);
389 assert!(r.content.starts_with("line0\nline1"));
391 }
392
393 #[test]
394 fn truncate_head_first_line_exceeds_bytes() {
395 let big: String = "a".repeat(DEFAULT_MAX_BYTES + 10);
396 let r = truncate_head(&big, TruncationOptions::default());
397 assert!(r.truncated);
398 assert!(r.first_line_exceeds_limit);
399 assert_eq!(r.truncated_by, Some(TruncationLimit::Bytes));
400 assert!(r.content.is_empty());
401 }
402
403 #[test]
404 fn truncate_tail_under_limits_is_noop() {
405 let r = truncate_tail("a\nb\n", TruncationOptions::default());
406 assert!(!r.truncated);
407 assert_eq!(r.content, "a\nb\n");
408 }
409
410 #[test]
411 fn truncate_tail_keeps_last_n_lines() {
412 let s: String = (0..3000)
413 .map(|i| format!("line{}", i))
414 .collect::<Vec<_>>()
415 .join("\n");
416 let r = truncate_tail(&s, TruncationOptions::default());
417 assert!(r.truncated);
418 assert_eq!(r.truncated_by, Some(TruncationLimit::Lines));
419 assert!(r.content.contains("line2999"));
421 }
422
423 #[test]
424 fn truncate_tail_partial_last_line() {
425 let big: String = "a".repeat(DEFAULT_MAX_BYTES + 100);
427 let r = truncate_tail(&big, TruncationOptions::default());
428 assert!(r.truncated);
429 assert!(r.last_line_partial);
430 assert!(r.content.len() <= DEFAULT_MAX_BYTES);
431 }
432
433 #[test]
434 fn trim_to_last_utf8_bytes_snaps_to_char_boundary() {
435 let s = "aéb";
438 assert_eq!(s.len(), 4); let out = trim_to_last_utf8_bytes(s, 3);
440 assert_eq!(out, "éb");
443 assert_eq!(trim_to_last_utf8_bytes(s, 0), "");
444 assert_eq!(trim_to_last_utf8_bytes(s, 10), s);
445 }
446
447 #[test]
448 fn truncate_line_default_appends_marker() {
449 let s: String = "x".repeat(GREP_MAX_LINE_LENGTH + 50);
450 let out = truncate_line_default(&s);
451 assert!(out.was_truncated);
452 assert!(out.text.ends_with("... [truncated]"));
453 let short = "hello";
454 let out2 = truncate_line_default(short);
455 assert!(!out2.was_truncated);
456 assert_eq!(out2.text, short);
457 }
458}