Skip to main content

lean_ctx/core/patterns/
fd.rs

1use std::collections::HashMap;
2
3pub fn compress(output: &str) -> Option<String> {
4    let lines: Vec<&str> = output.lines().filter(|l| !l.trim().is_empty()).collect();
5    if lines.len() < 5 {
6        return None;
7    }
8
9    let mut by_dir: HashMap<String, Vec<String>> = HashMap::new();
10    let mut total_files = 0usize;
11
12    for line in &lines {
13        let path = line.trim();
14        if should_skip(path) {
15            continue;
16        }
17
18        total_files += 1;
19
20        if let Some(slash_pos) = path.rfind('/') {
21            let dir = &path[..slash_pos];
22            let file = &path[slash_pos + 1..];
23            by_dir
24                .entry(dir.to_string())
25                .or_default()
26                .push(file.to_string());
27        } else {
28            by_dir
29                .entry(".".to_string())
30                .or_default()
31                .push(path.to_string());
32        }
33    }
34
35    if total_files == 0 {
36        return None;
37    }
38
39    let mut result = format!("{total_files}F {}D:\n", by_dir.len());
40    let mut sorted_dirs: Vec<_> = by_dir.iter().collect();
41    sorted_dirs.sort_by_key(|(dir, _)| (*dir).clone());
42
43    for (dir, files) in &sorted_dirs {
44        result.push_str(&format!("\n{dir}/"));
45        let show: Vec<_> = files.iter().take(10).collect();
46        let mut line_buf = String::new();
47        for f in &show {
48            if line_buf.len() + f.len() + 1 > 60 {
49                result.push_str(&format!("\n  {line_buf}"));
50                line_buf.clear();
51            }
52            if !line_buf.is_empty() {
53                line_buf.push(' ');
54            }
55            line_buf.push_str(f);
56        }
57        if !line_buf.is_empty() {
58            result.push_str(&format!("\n  {line_buf}"));
59        }
60        if files.len() > 10 {
61            result.push_str(&format!("\n  ... +{} more", files.len() - 10));
62        }
63    }
64
65    Some(result)
66}
67
68fn should_skip(path: &str) -> bool {
69    const SKIP: &[&str] = &[
70        "node_modules/",
71        ".git/",
72        "target/debug/",
73        "target/release/",
74        "__pycache__/",
75        ".svelte-kit/",
76        ".next/",
77        "dist/",
78        ".DS_Store",
79    ];
80    SKIP.iter().any(|d| path.contains(d))
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn groups_files_by_directory() {
89        let output = "src/main.rs\nsrc/lib.rs\nsrc/util/helpers.rs\nsrc/util/math.rs\ntests/integration.rs\n";
90        let result = compress(output).unwrap();
91        assert!(result.contains("5F"), "should count 5 files");
92        assert!(result.contains("src/"), "should group by src");
93        assert!(result.contains("tests/"), "should group by tests");
94    }
95
96    #[test]
97    fn skips_noisy_dirs() {
98        let output = "node_modules/foo/bar.js\nsrc/a.rs\nsrc/b.rs\nsrc/c.rs\nsrc/d.rs\nsrc/e.rs\n";
99        let result = compress(output).unwrap();
100        assert!(!result.contains("node_modules"), "should skip node_modules");
101    }
102
103    #[test]
104    fn too_few_lines_returns_none() {
105        assert!(compress("a.rs\nb.rs\n").is_none());
106    }
107}