1use similar::{ChangeTag, TextDiff};
2
3macro_rules! static_regex {
4 ($pattern:expr_2021) => {{
5 static RE: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
6 RE.get_or_init(|| {
7 regex::Regex::new($pattern).expect(concat!("BUG: invalid static regex: ", $pattern))
8 })
9 }};
10}
11
12pub fn strip_ansi(s: &str) -> String {
14 if !s.contains('\x1b') {
15 return s.to_string();
16 }
17 let mut result = String::with_capacity(s.len());
18 let mut in_escape = false;
19 for c in s.chars() {
20 if c == '\x1b' {
21 in_escape = true;
22 continue;
23 }
24 if in_escape {
25 if c.is_ascii_alphabetic() {
26 in_escape = false;
27 }
28 continue;
29 }
30 result.push(c);
31 }
32 result
33}
34
35pub fn ansi_density(s: &str) -> f64 {
37 if s.is_empty() {
38 return 0.0;
39 }
40 let escape_bytes = s.chars().filter(|&c| c == '\x1b').count();
41 escape_bytes as f64 / s.len() as f64
42}
43
44pub fn aggressive_compress(content: &str, ext: Option<&str>) -> String {
46 if let Some(compacted) = crate::core::structured_compact::compact_structured(content, ext) {
50 return compacted;
51 }
52
53 if let Some(delim) = tabular_delimiter(ext)
58 && let Some(crushed) = crate::core::tabular_crush::crush_text_if_beneficial(content, delim)
59 {
60 return crushed;
61 }
62
63 if is_yaml_ext(ext)
69 && let Some(crushed) = crate::core::yaml_crush::crush_text_if_beneficial(content)
70 {
71 return crushed;
72 }
73
74 let mut result: Vec<String> = Vec::new();
75 let is_python = matches!(ext, Some("py"));
76 let is_html = matches!(ext, Some("html" | "htm" | "xml" | "svg"));
77 let is_sql = matches!(ext, Some("sql"));
78 let is_shell = matches!(ext, Some("sh" | "bash" | "zsh" | "fish"));
79
80 let mut in_block_comment = false;
81
82 for line in content.lines() {
83 let trimmed = line.trim();
84
85 if trimmed.is_empty() {
86 continue;
87 }
88
89 if in_block_comment {
90 if trimmed.contains("*/") || (is_html && trimmed.contains("-->")) {
91 in_block_comment = false;
92 }
93 continue;
94 }
95
96 if trimmed.starts_with("/*") || (is_html && trimmed.starts_with("<!--")) {
97 if !(trimmed.contains("*/") || trimmed.contains("-->")) {
98 in_block_comment = true;
99 }
100 continue;
101 }
102
103 if trimmed.starts_with("//") && !trimmed.starts_with("///") {
104 continue;
105 }
106 if trimmed.starts_with('*') || trimmed.starts_with("*/") {
107 continue;
108 }
109 if is_python && trimmed.starts_with('#') {
110 continue;
111 }
112 if is_sql && trimmed.starts_with("--") {
113 continue;
114 }
115 if is_shell && trimmed.starts_with('#') && !trimmed.starts_with("#!") {
116 continue;
117 }
118 if !is_python && trimmed.starts_with('#') && trimmed.contains('[') {
119 continue;
120 }
121
122 if trimmed == "}" || trimmed == "};" || trimmed == ");" || trimmed == "});" {
123 if let Some(last) = result.last() {
124 let last_trimmed = last.trim();
125 if matches!(last_trimmed, "}" | "};" | ");" | "});") {
126 if let Some(last_mut) = result.last_mut() {
127 last_mut.push_str(trimmed);
128 }
129 continue;
130 }
131 }
132 result.push(trimmed.to_string());
133 continue;
134 }
135
136 let normalized = normalize_indentation(line);
137 result.push(normalized);
138 }
139
140 result.join("\n")
141}
142
143pub fn lightweight_cleanup(content: &str) -> String {
146 let lines: Vec<&str> = content.lines().collect();
147 let total = lines.len();
148
149 let mut result: Vec<String> = Vec::new();
150 let mut blank_count = 0u32;
151 let mut brace_run: Vec<&str> = Vec::new();
152
153 let flush_brace_run = |run: &mut Vec<&str>, out: &mut Vec<String>| {
154 if total <= 200 || run.len() <= 5 {
155 for l in run.iter() {
156 out.push(l.to_string());
157 }
158 } else {
159 out.push(run[0].to_string());
160 out.push(run[1].to_string());
161 out.push(format!("[{} brace-only lines collapsed]", run.len() - 2));
162 }
163 run.clear();
164 };
165
166 for line in &lines {
167 let trimmed = line.trim();
168
169 if trimmed.is_empty() {
170 flush_brace_run(&mut brace_run, &mut result);
171 blank_count += 1;
172 if blank_count <= 1 {
173 result.push(String::new());
174 }
175 continue;
176 }
177 blank_count = 0;
178
179 if matches!(trimmed, "}" | "};" | ");" | "});" | ")") {
180 brace_run.push(trimmed);
181 continue;
182 }
183
184 flush_brace_run(&mut brace_run, &mut result);
185 result.push(line.to_string());
186 }
187 flush_brace_run(&mut brace_run, &mut result);
188
189 result.join("\n")
190}
191
192pub fn safeguard_ratio(original: &str, compressed: &str) -> String {
196 let orig_tokens = super::tokens::count_tokens(original);
197 let comp_tokens = super::tokens::count_tokens(compressed);
198
199 if orig_tokens == 0 {
200 return compressed.to_string();
201 }
202
203 if comp_tokens > orig_tokens {
204 return original.to_string();
205 }
206
207 let ratio = comp_tokens as f64 / orig_tokens as f64;
208 if ratio < 0.05 && orig_tokens < 2000 {
209 original.to_string()
210 } else {
211 compressed.to_string()
212 }
213}
214
215pub(crate) fn tabular_delimiter(ext: Option<&str>) -> Option<char> {
217 match ext {
218 Some("csv") => Some(','),
219 Some("tsv" | "tab") => Some('\t'),
220 _ => None,
221 }
222}
223
224pub(crate) fn is_yaml_ext(ext: Option<&str>) -> bool {
226 matches!(ext, Some("yaml" | "yml"))
227}
228
229fn normalize_indentation(line: &str) -> String {
230 let content = line.trim_start();
231 let leading = line.len() - content.len();
232 let has_tabs = line.starts_with('\t');
233 let reduced = if has_tabs { leading } else { leading / 2 };
234 format!("{}{}", " ".repeat(reduced), content)
235}
236
237pub fn diff_content(old_content: &str, new_content: &str) -> String {
239 if old_content == new_content {
240 return "(no changes)".to_string();
241 }
242
243 let diff = TextDiff::from_lines(old_content, new_content);
244 let mut changes = Vec::new();
245 let mut additions = 0usize;
246 let mut deletions = 0usize;
247
248 for change in diff.iter_all_changes() {
249 let line_no = change.new_index().or(change.old_index()).map(|i| i + 1);
250 let text = change.value().trim_end_matches('\n');
251 match change.tag() {
252 ChangeTag::Insert => {
253 additions += 1;
254 if let Some(n) = line_no {
255 changes.push(format!("+{n}: {text}"));
256 }
257 }
258 ChangeTag::Delete => {
259 deletions += 1;
260 if let Some(n) = line_no {
261 changes.push(format!("-{n}: {text}"));
262 }
263 }
264 ChangeTag::Equal => {}
265 }
266 }
267
268 if changes.is_empty() {
269 return "(no changes)".to_string();
270 }
271
272 changes.push(format!("\ndiff +{additions}/-{deletions} lines"));
273 changes.join("\n")
274}
275
276pub fn verbatim_compact(text: &str) -> String {
278 let mut lines: Vec<String> = Vec::new();
279 let mut blank_count = 0u32;
280 let mut prev_line: Option<String> = None;
281 let mut repeat_count = 0u32;
282
283 for line in text.lines() {
284 let trimmed = line.trim();
285
286 if trimmed.is_empty() {
287 blank_count += 1;
288 if blank_count <= 1 {
289 flush_repeats(&mut lines, &mut prev_line, &mut repeat_count);
290 lines.push(String::new());
291 }
292 continue;
293 }
294 blank_count = 0;
295
296 if is_boilerplate_line(trimmed) {
297 continue;
298 }
299
300 let normalized = normalize_whitespace(trimmed);
301 let stripped = strip_timestamps_hashes(&normalized);
302
303 if let Some(ref prev) = prev_line
304 && *prev == stripped
305 {
306 repeat_count += 1;
307 continue;
308 }
309
310 flush_repeats(&mut lines, &mut prev_line, &mut repeat_count);
311 prev_line = Some(stripped.clone());
312 repeat_count = 1;
313 lines.push(stripped);
314 }
315
316 flush_repeats(&mut lines, &mut prev_line, &mut repeat_count);
317 lines.join("\n")
318}
319
320pub fn task_aware_compress(
322 content: &str,
323 ext: Option<&str>,
324 intent: &super::intent_engine::StructuredIntent,
325) -> String {
326 use super::intent_engine::{IntentScope, TaskType};
327
328 let budget_ratio = match intent.scope {
329 IntentScope::SingleFile => 0.7,
330 IntentScope::MultiFile => 0.5,
331 IntentScope::CrossModule => 0.35,
332 IntentScope::ProjectWide => 0.25,
333 };
334
335 match intent.task_type {
336 TaskType::FixBug | TaskType::Debug => {
337 let filtered = super::task_relevance::information_bottleneck_filter_typed(
338 content,
339 &intent.keywords,
340 budget_ratio,
341 Some(intent.task_type),
342 &[],
343 );
344 safeguard_ratio(content, &filtered)
345 }
346 TaskType::Refactor | TaskType::Review => {
347 let cleaned = lightweight_cleanup(content);
348 let filtered = super::task_relevance::information_bottleneck_filter_typed(
349 &cleaned,
350 &intent.keywords,
351 budget_ratio.max(0.5),
352 Some(intent.task_type),
353 &[],
354 );
355 safeguard_ratio(content, &filtered)
356 }
357 TaskType::Generate | TaskType::Test => {
358 let compressed = aggressive_compress(content, ext);
359 safeguard_ratio(content, &compressed)
360 }
361 TaskType::Explore | TaskType::Config | TaskType::Deploy => {
362 let cleaned = lightweight_cleanup(content);
363 safeguard_ratio(content, &cleaned)
364 }
365 }
366}
367
368fn flush_repeats(lines: &mut [String], prev_line: &mut Option<String>, count: &mut u32) {
369 if *count > 1
370 && let &mut Some(ref prev) = prev_line
371 {
372 let last_idx = lines.len().saturating_sub(1);
373 if last_idx < lines.len() {
374 lines[last_idx] = format!("[{count}x] {prev}");
375 }
376 }
377 *count = 0;
378 *prev_line = None;
379}
380
381fn normalize_whitespace(line: &str) -> String {
382 let mut result = String::with_capacity(line.len());
383 let mut prev_space = false;
384 for ch in line.chars() {
385 if ch == ' ' || ch == '\t' {
386 if !prev_space {
387 result.push(' ');
388 prev_space = true;
389 }
390 } else {
391 result.push(ch);
392 prev_space = false;
393 }
394 }
395 result
396}
397
398fn strip_timestamps_hashes(line: &str) -> String {
399 let ts_re =
400 static_regex!(r"\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?");
401 let hash_re = static_regex!(r"\b[0-9a-f]{32,64}\b");
402
403 let s = ts_re.replace_all(line, "[TS]");
404 let s = hash_re.replace_all(&s, "[HASH]");
405 s.into_owned()
406}
407
408fn is_boilerplate_line(trimmed: &str) -> bool {
409 let lower = trimmed.to_lowercase();
410 if lower.starts_with("copyright")
411 || lower.starts_with("licensed under")
412 || lower.starts_with("license:")
413 || lower.starts_with("all rights reserved")
414 {
415 return true;
416 }
417 if lower.starts_with("generated by") || lower.starts_with("auto-generated") {
418 return true;
419 }
420 if trimmed.len() >= 4 {
421 let chars: Vec<char> = trimmed.chars().collect();
422 let first = chars[0];
423 if matches!(first, '=' | '-' | '*' | '─' | '━') {
424 let same = chars.iter().filter(|c| **c == first).count();
425 if same as f64 / chars.len() as f64 > 0.8 {
426 return true;
427 }
428 }
429 }
430 false
431}
432
433#[cfg(test)]
434mod tests {
435 use super::*;
436
437 #[test]
438 fn test_diff_insertion() {
439 let old = "line1\nline2\nline3";
440 let new = "line1\nline2\nnew_line\nline3";
441 let result = diff_content(old, new);
442 assert!(result.contains('+'), "should show additions");
443 assert!(result.contains("new_line"));
444 }
445
446 #[test]
447 fn test_diff_deletion() {
448 let old = "line1\nline2\nline3";
449 let new = "line1\nline3";
450 let result = diff_content(old, new);
451 assert!(result.contains('-'), "should show deletions");
452 assert!(result.contains("line2"));
453 }
454
455 #[test]
456 fn test_diff_no_changes() {
457 let content = "same\ncontent";
458 assert_eq!(diff_content(content, content), "(no changes)");
459 }
460
461 #[test]
462 fn test_lightweight_cleanup_collapses_braces() {
463 let mut lines: Vec<String> = (0..210).map(|i| format!("line {i}")).collect();
464 lines.extend(
465 ["}", "}", "}", "}", "}", "}", "}", "}"]
466 .iter()
467 .map(std::string::ToString::to_string),
468 );
469 lines.push("fn next() {}".to_string());
470 let input = lines.join("\n");
471 let result = lightweight_cleanup(&input);
472 assert!(
473 result.contains("[6 brace-only lines collapsed]"),
474 "should collapse long brace runs in large files"
475 );
476 assert!(result.contains("fn next()"));
477 }
478
479 #[test]
480 fn test_lightweight_cleanup_blank_lines() {
481 let input = "line1\n\n\n\n\nline2";
482 let result = lightweight_cleanup(input);
483 let blank_runs = result.split("line1").nth(1).unwrap();
484 let blanks = blank_runs.matches('\n').count();
485 assert!(blanks <= 2, "should collapse multiple blank lines");
486 }
487
488 #[test]
489 fn test_safeguard_ratio_prevents_over_compression_on_small_output() {
490 let original = "a ".repeat(100); let too_compressed = "a";
492 let result = safeguard_ratio(&original, too_compressed);
493 assert_eq!(
494 result, original,
495 "should return original when ratio < 0.05 and output is small"
496 );
497 }
498
499 #[test]
500 fn test_safeguard_ratio_allows_strong_compression_on_large_output() {
501 let original = "line content here\n".repeat(1000); let compressed = "summary: 1000 lines";
503 let result = safeguard_ratio(&original, compressed);
504 assert_eq!(
505 result, compressed,
506 "should allow strong compression for large outputs"
507 );
508 }
509
510 #[test]
511 fn test_aggressive_strips_comments() {
512 let code = "fn main() {\n // a comment\n let x = 1;\n}";
513 let result = aggressive_compress(code, Some("rs"));
514 assert!(!result.contains("// a comment"));
515 assert!(result.contains("let x = 1"));
516 }
517
518 #[test]
519 fn test_aggressive_python_comments() {
520 let code = "def main():\n # comment\n x = 1";
521 let result = aggressive_compress(code, Some("py"));
522 assert!(!result.contains("# comment"));
523 assert!(result.contains("x = 1"));
524 }
525
526 #[test]
527 fn test_aggressive_preserves_doc_comments() {
528 let code = "/// Doc comment\nfn main() {}";
529 let result = aggressive_compress(code, Some("rs"));
530 assert!(result.contains("/// Doc comment"));
531 }
532
533 #[test]
534 fn test_aggressive_block_comment() {
535 let code = "/* start\n * middle\n */ end\nfn main() {}";
536 let result = aggressive_compress(code, Some("rs"));
537 assert!(!result.contains("start"));
538 assert!(!result.contains("middle"));
539 assert!(result.contains("fn main()"));
540 }
541
542 #[test]
543 fn test_strip_ansi_removes_escape_codes() {
544 let input = "\x1b[31mERROR\x1b[0m: something failed";
545 let result = strip_ansi(input);
546 assert_eq!(result, "ERROR: something failed");
547 assert!(!result.contains('\x1b'));
548 }
549
550 #[test]
551 fn test_strip_ansi_passthrough_clean_text() {
552 let input = "clean text without escapes";
553 let result = strip_ansi(input);
554 assert_eq!(result, input);
555 }
556
557 #[test]
558 fn test_ansi_density_zero_for_clean() {
559 assert_eq!(ansi_density("hello world"), 0.0);
560 }
561
562 #[test]
563 fn test_ansi_density_nonzero_for_colored() {
564 let input = "\x1b[31mred\x1b[0m";
565 assert!(ansi_density(input) > 0.0);
566 }
567}