Skip to main content

lean_ctx/core/patterns/
grep.rs

1use 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}