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