winx_code_agent/utils/
display_tree.rs1use std::collections::HashSet;
9use std::fmt::Write as _;
10use std::fs;
11use std::path::{Path, PathBuf};
12
13pub struct DirectoryTree {
14 root: PathBuf,
15 expanded_files: HashSet<PathBuf>,
16 expanded_dirs: HashSet<PathBuf>,
17}
18
19impl DirectoryTree {
20 pub fn new(root: &Path) -> Self {
21 Self {
22 root: root.to_path_buf(),
23 expanded_files: HashSet::new(),
24 expanded_dirs: HashSet::new(),
25 }
26 }
27
28 pub fn expand(&mut self, rel_path: &str) {
31 let abs_path = self.root.join(rel_path);
32 if !abs_path.is_file() || !abs_path.starts_with(&self.root) {
33 return;
34 }
35 self.expanded_files.insert(abs_path.clone());
36
37 let mut current = abs_path.parent().map(Path::to_path_buf);
38 while let Some(dir) = current {
39 if dir != self.root && !dir.starts_with(&self.root) {
40 break;
41 }
42 self.expanded_dirs.insert(dir.clone());
43 if dir == self.root {
44 break;
45 }
46 current = dir.parent().map(Path::to_path_buf);
47 }
48 }
49
50 fn list_directory(dir_path: &Path) -> Vec<PathBuf> {
52 let Ok(read_dir) = fs::read_dir(dir_path) else {
53 return Vec::new();
54 };
55 let mut contents: Vec<PathBuf> =
56 read_dir.filter_map(|entry| entry.ok().map(|e| e.path())).collect();
57 contents.sort_by(|a, b| {
58 let a_is_dir = a.is_dir();
59 let b_is_dir = b.is_dir();
60 (!a_is_dir, file_name_lower(a)).cmp(&(!b_is_dir, file_name_lower(b)))
62 });
63 contents
64 }
65
66 fn count_hidden(dir_path: &Path, shown: &[PathBuf]) -> (usize, usize) {
67 let shown_set: HashSet<&PathBuf> = shown.iter().collect();
68 let mut hidden_files = 0;
69 let mut hidden_dirs = 0;
70 for item in Self::list_directory(dir_path) {
71 if shown_set.contains(&item) {
72 continue;
73 }
74 if item.is_dir() {
75 hidden_dirs += 1;
76 } else {
77 hidden_files += 1;
78 }
79 }
80 (hidden_files, hidden_dirs)
81 }
82
83 pub fn display(&self) -> String {
84 let mut out = String::new();
85 self.display_recursive(&self.root, 0, 0, &mut out);
86 out
87 }
88
89 fn display_recursive(&self, current: &Path, indent: usize, depth: usize, out: &mut String) {
90 if current == self.root {
91 let _ = writeln!(out, "{}/", current.display());
92 } else {
93 let name = file_name_str(current);
94 let _ = writeln!(out, "{:indent$}{}/", "", name, indent = indent);
95 }
96
97 if depth > 0 && !self.expanded_dirs.contains(current) {
99 return;
100 }
101
102 let mut shown = Vec::new();
103 for item in Self::list_directory(current) {
104 let should_show =
105 self.expanded_files.contains(&item) || self.expanded_dirs.contains(&item);
106 if !should_show {
107 continue;
108 }
109 shown.push(item.clone());
110 if item.is_dir() {
111 self.display_recursive(&item, indent + 2, depth + 1, out);
112 } else {
113 let _ = writeln!(out, "{:width$}{}", "", file_name_str(&item), width = indent + 2);
114 }
115 }
116
117 let (hidden_files, hidden_dirs) = Self::count_hidden(current, &shown);
118 if hidden_files > 0 || hidden_dirs > 0 {
119 let _ = writeln!(out, "{:width$}...", "", width = indent + 2);
120 }
121 }
122}
123
124fn file_name_str(path: &Path) -> String {
125 path.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default()
126}
127
128fn file_name_lower(path: &Path) -> String {
129 file_name_str(path).to_lowercase()
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135 use crate::errors::Result;
136 use tempfile::TempDir;
137
138 #[test]
139 fn renders_expanded_files_and_collapses_rest() -> Result<()> {
140 let temp = TempDir::new()?;
141 let root = temp.path();
142 fs::create_dir(root.join("src"))?;
143 fs::write(root.join("src/main.rs"), "fn main() {}\n")?;
144 fs::write(root.join("src/hidden.rs"), "\n")?;
145 fs::write(root.join("README.md"), "x\n")?;
146
147 let mut tree = DirectoryTree::new(root);
148 tree.expand("src/main.rs");
149 let display = tree.display();
150
151 assert!(display.contains("src/"));
152 assert!(display.contains("main.rs"));
153 assert!(display.contains("..."));
155 assert!(!display.contains("hidden.rs"));
156 Ok(())
157 }
158}