aft/compress/
listing_fold.rs1pub const FOLD_THRESHOLD: usize = 8;
4pub const MAX_LINES: usize = 400;
7
8pub fn mask_digits_in_name(name: &str) -> String {
10 let mut out = String::with_capacity(name.len());
11 let mut chars = name.chars().peekable();
12 while let Some(c) = chars.next() {
13 if c.is_ascii_digit() {
14 while chars.peek().is_some_and(|p| p.is_ascii_digit()) {
15 chars.next();
16 }
17 out.push('#');
18 } else {
19 out.push(c);
20 }
21 }
22 out
23}
24
25pub fn shape_key_for_basename(dir: &str, basename: &str) -> String {
27 let masked = mask_digits_in_name(basename);
28 if dir.is_empty() {
29 masked
30 } else {
31 format!("{dir}|{masked}")
32 }
33}
34
35pub fn shape_pattern(masked_basename: &str) -> String {
37 masked_basename.replace('#', "*")
38}
39
40#[derive(Clone, Debug)]
41pub struct FoldEntry {
42 pub line: String,
43 pub dir: String,
44 pub basename: String,
45 pub shape_key: String,
46}
47
48pub fn fold_consecutive_runs(entries: Vec<FoldEntry>) -> Vec<String> {
49 if entries.is_empty() {
50 return Vec::new();
51 }
52
53 let mut out = Vec::new();
54 let mut i = 0;
55 while i < entries.len() {
56 let key = entries[i].shape_key.clone();
57 let mut j = i + 1;
58 while j < entries.len() && entries[j].shape_key == key {
59 j += 1;
60 }
61 let run = &entries[i..j];
62 if run.len() >= FOLD_THRESHOLD {
63 let masked = mask_digits_in_name(&run[0].basename);
64 let pattern = if run[0].dir.is_empty() {
65 shape_pattern(&masked)
66 } else {
67 format!(
68 "{}/{}",
69 run[0].dir.trim_end_matches('/'),
70 shape_pattern(&masked)
71 )
72 };
73 let first = display_name(&run[0]);
74 let last = display_name(run.last().expect("non-empty run"));
75 let count = run.len();
76 let noun = if count == 1 { "file" } else { "files" };
77 out.push(format!("{pattern} — {count} {noun} ({first} … {last})"));
78 } else {
79 for e in run {
80 out.push(e.line.clone());
81 }
82 }
83 i = j;
84 }
85 out
86}
87
88fn display_name(e: &FoldEntry) -> String {
89 if e.dir.is_empty() {
90 e.basename.clone()
91 } else {
92 format!("{}/{}", e.dir.trim_end_matches('/'), e.basename)
93 }
94}
95
96pub fn finish_folded(lines: Vec<String>) -> String {
99 if lines.len() <= MAX_LINES {
100 return lines.join("\n");
101 }
102 let omitted = lines.len() - (MAX_LINES - 1);
103 let head_count = (MAX_LINES - 1) / 2;
104 let tail_count = (MAX_LINES - 1) - head_count;
105 let mut kept: Vec<String> = lines.iter().take(head_count).cloned().collect();
106 kept.push(format!(
107 "… +{omitted} entries omitted (listing too long; narrow with a path/glob)"
108 ));
109 kept.extend(lines.iter().skip(lines.len() - tail_count).cloned());
110 kept.join("\n")
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116
117 #[test]
118 fn masks_digit_runs_in_filename() {
119 assert_eq!(mask_digits_in_name("module_017.ts"), "module_#.ts");
120 assert_eq!(
121 mask_digits_in_name("module_100_NEEDLE_FILE_marker.ts"),
122 "module_#_NEEDLE_FILE_marker.ts"
123 );
124 assert_ne!(
125 shape_key_for_basename("", "module_100.ts"),
126 shape_key_for_basename("", "module_100_NEEDLE_FILE_marker.ts")
127 );
128 assert_eq!(
129 shape_pattern(&mask_digits_in_name("module_100_NEEDLE_FILE_marker.ts")),
130 "module_*_NEEDLE_FILE_marker.ts"
131 );
132 }
133}