lean_ctx/core/patterns/
fd.rs1use 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}