use crate::ai::token_budget::TokenBudget;
use std::ops::Range;
pub struct Truncator;
impl Truncator {
pub fn truncate_diff(diff: &str, max_tokens: usize) -> String {
let estimated = TokenBudget::estimate_tokens(diff);
if estimated <= max_tokens {
return diff.to_string();
}
let max_chars = max_tokens * 4;
let lines: Vec<&str> = diff.lines().collect();
let total_lines = lines.len();
let allowed_lines = max_chars / 50;
if total_lines <= allowed_lines {
let kept: String = diff.chars().take(max_chars).collect();
return format!(
"{}\n... [Output truncated. Content too large ({} tokens). Displaying first {} chars] ...\n",
kept, estimated, max_chars
);
}
let keep_top = allowed_lines / 2;
let keep_bottom = allowed_lines / 2;
if keep_top + keep_bottom >= total_lines {
let kept: String = diff.chars().take(max_chars).collect();
return format!(
"{}\n... [Output truncated. Content too large. Displaying first {} chars] ...\n",
kept, max_chars
);
}
let mut result = String::new();
for line in &lines[..keep_top] {
result.push_str(line);
result.push('\n');
}
result.push_str(&format!(
"\n... [Diff truncated. Dropped {} lines] ...\n\n",
total_lines - (keep_top + keep_bottom)
));
for line in &lines[total_lines - keep_bottom..] {
result.push_str(line);
result.push('\n');
}
if TokenBudget::estimate_tokens(&result) > max_tokens {
let kept: String = result.chars().take(max_chars).collect();
return format!(
"{}\n... [Output truncated after line filtering. Original size: {} tokens] ...\n",
kept, estimated
);
}
result
}
pub fn truncate_code(
content: &str,
focus_lines: Option<Range<usize>>,
max_tokens: usize,
) -> String {
let estimated = TokenBudget::estimate_tokens(content);
if estimated <= max_tokens {
return content.to_string();
}
let lines: Vec<&str> = content.lines().collect();
let total_lines = lines.len();
let (start_focus, end_focus) = if let Some(range) = focus_lines {
(range.start.max(1) - 1, range.end.min(total_lines))
} else {
return Self::truncate_diff(content, max_tokens);
};
let context_size = 50;
let start_keep = start_focus.saturating_sub(context_size);
let end_keep = (end_focus + context_size).min(total_lines);
let mut result = String::new();
if start_keep > 0 {
result.push_str(&format!("... [{} lines collapsed] ...\n", start_keep));
}
for line in &lines[start_keep..end_keep] {
result.push_str(line);
result.push('\n');
}
if end_keep < total_lines {
result.push_str(&format!(
"... [{} lines collapsed] ...\n",
total_lines - end_keep
));
}
if TokenBudget::estimate_tokens(&result) > max_tokens {
let max_chars = max_tokens * 4;
let kept: String = result.chars().take(max_chars).collect();
return format!(
"{}\n... [Output truncated. Code context too large. Original size: {} tokens] ...\n",
kept, estimated
);
}
result
}
#[allow(dead_code)]
fn regex_collapse(_content: &str) -> String {
String::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_truncate_diff() {
let diff = "line1\nline2\nline3\nline4\nline5\nline6";
let truncated = Truncator::truncate_diff(diff, 5);
assert!(truncated.contains("Diff truncated"));
}
#[test]
fn test_truncate_code_focus() {
let code = (0..200)
.map(|i| format!("line {}", i))
.collect::<Vec<_>>()
.join("\n");
let truncated = Truncator::truncate_code(&code, Some(100..105), 300);
assert!(truncated.contains("line 100"));
assert!(truncated.contains("line 105"));
assert!(truncated.contains("lines collapsed"));
}
#[test]
fn test_truncate_diff_long_line() {
let long_line = "a".repeat(1000);
let truncated = Truncator::truncate_diff(&long_line, 20);
assert!(truncated.len() < 300);
assert!(truncated.contains("Output truncated"));
assert!(truncated.starts_with("aaaa"));
}
}