lean_ctx/core/patterns/
grep.rs1use std::collections::HashMap;
2
3pub fn compress(output: &str) -> Option<String> {
4 let lines: Vec<&str> = output.lines().collect();
5 if lines.len() < 3 {
6 return None;
7 }
8
9 let mut by_file: HashMap<&str, Vec<(usize, &str)>> = HashMap::new();
10 let mut total_matches = 0usize;
11
12 for line in &lines {
13 if let Some((file, rest)) = parse_grep_line(line) {
14 total_matches += 1;
15 let line_num = extract_line_num(rest);
16 let content = strip_line_num(rest);
17 by_file.entry(file).or_default().push((line_num, content));
18 }
19 }
20
21 if total_matches == 0 {
22 return None;
23 }
24
25 let mut result = format!("{total_matches} matches in {}F:\n", by_file.len());
26 let mut sorted_files: Vec<_> = by_file.iter().collect();
27 sorted_files.sort_by_key(|(_, matches)| std::cmp::Reverse(matches.len()));
28
29 for (file, matches) in &sorted_files {
30 let short = shorten_path(file);
31 result.push_str(&format!("\n{short} ({}):", matches.len()));
32 let show = matches.iter().take(5);
33 for (ln, content) in show {
34 let trimmed = content.trim();
35 let short_content = if trimmed.len() > 80 {
36 let truncated: String = trimmed.chars().take(79).collect();
37 format!("{truncated}…")
38 } else {
39 trimmed.to_string()
40 };
41 if *ln > 0 {
42 result.push_str(&format!("\n {ln}: {short_content}"));
43 } else {
44 result.push_str(&format!("\n {short_content}"));
45 }
46 }
47 if matches.len() > 5 {
48 result.push_str(&format!("\n ... +{} more", matches.len() - 5));
49 }
50 }
51
52 Some(result)
53}
54
55fn parse_grep_line(line: &str) -> Option<(&str, &str)> {
56 if let Some(pos) = line.find(':') {
57 let file = &line[..pos];
58 if file.contains('/') || file.contains('.') {
59 let rest = &line[pos + 1..];
60 return Some((file, rest));
61 }
62 }
63 None
64}
65
66fn extract_line_num(rest: &str) -> usize {
67 if let Some(pos) = rest.find(':') {
68 rest[..pos].parse().unwrap_or(0)
69 } else {
70 0
71 }
72}
73
74fn strip_line_num(rest: &str) -> &str {
75 if let Some(pos) = rest.find(':') {
76 if rest[..pos].chars().all(|c| c.is_ascii_digit()) {
77 return &rest[pos + 1..];
78 }
79 }
80 rest
81}
82
83fn shorten_path(path: &str) -> &str {
84 path.strip_prefix("./").unwrap_or(path)
85}