1use std::path::{Path, PathBuf};
12
13use crate::tree::{NodeId, Tree};
14
15pub fn scan(dir: &Path, no_ignore: bool) -> std::io::Result<Tree> {
19 let _span = crate::profile::span("fstree::scan");
20 let root_dir = dir.canonicalize()?;
21 let mut tree = Tree::new_fs(root_dir.clone(), no_ignore);
22 walk_into(&mut tree, None, &root_dir, no_ignore);
23 tree.containers_first();
24 Ok(tree)
25}
26
27pub(crate) fn materialize(tree: &mut Tree, id: NodeId) {
29 let path = PathBuf::from(tree.path(id));
30 let no_ignore = tree.fs_no_ignore();
31 walk_into(tree, Some(id), &path, no_ignore);
32 tree.containers_first_children(id);
33 tree.mark_children_loaded(id);
34}
35
36fn walk_into(tree: &mut Tree, parent: Option<NodeId>, path: &Path, no_ignore: bool) {
39 let walk = ignore::WalkBuilder::new(path)
40 .standard_filters(!no_ignore)
41 .hidden(false)
42 .max_depth(Some(1))
43 .sort_by_file_name(|a, b| a.to_ascii_lowercase().cmp(&b.to_ascii_lowercase()))
44 .build();
45 for entry in walk {
46 match entry {
47 Ok(entry) => {
48 if entry.depth() == 0 {
49 continue; }
51 let is_dir = entry.file_type().is_some_and(|t| t.is_dir());
52 tree.push_fs(parent, entry.file_name(), is_dir);
53 }
54 Err(error) => tree.record_error(error.to_string()),
55 }
56 }
57}
58
59#[cfg(test)]
60mod tests {
61 use super::*;
62 use tui_treelistview::{TreeChildren, TreeModel};
63
64 fn fixture() -> tempfile::TempDir {
74 let dir = tempfile::tempdir().unwrap();
75 let p = dir.path();
76 std::fs::write(p.join(".hidden-file"), "").unwrap();
77 std::fs::create_dir(p.join("b-dir")).unwrap();
78 std::fs::write(p.join("b-dir/inner.txt"), "").unwrap();
79 std::fs::create_dir(p.join("empty-dir")).unwrap();
80 std::fs::write(p.join("a-file.txt"), "").unwrap();
81 std::fs::write(p.join("z-file.txt"), "").unwrap();
82 std::fs::write(p.join("ignored.log"), "").unwrap();
83 std::fs::write(p.join(".ignore"), "*.log\n").unwrap();
84 dir
85 }
86
87 fn scan_all(dir: &Path) -> Tree {
90 let mut tree = scan(dir, false).unwrap();
91 tree.index_all();
92 tree
93 }
94
95 fn root_names(tree: &Tree) -> Vec<String> {
96 tree.root_ids().iter().map(|&id| tree.name(id)).collect()
97 }
98
99 #[test]
100 fn default_scan_shows_dotfiles_but_honors_ignore_files() {
101 let dir = fixture();
102 let tree = scan(dir.path(), false).unwrap();
103 assert_eq!(
105 root_names(&tree),
106 [
107 "b-dir",
108 "empty-dir",
109 ".hidden-file",
110 ".ignore",
111 "a-file.txt",
112 "z-file.txt",
113 ]
114 );
115 assert!(!root_names(&tree).contains(&"ignored.log".to_string()));
116 }
117
118 #[test]
119 fn no_ignore_reveals_ignored_files() {
120 let dir = fixture();
121 let tree = scan(dir.path(), true).unwrap();
122 let names = root_names(&tree);
123 assert!(names.contains(&".hidden-file".to_string()));
124 assert!(names.contains(&"ignored.log".to_string()));
125 }
126
127 #[test]
128 fn scan_builds_only_the_top_level_and_dirs_materialize_on_demand() {
129 let dir = fixture();
130 let mut tree = scan(dir.path(), false).unwrap();
131 let b_dir = tree.root_ids()[0];
132
133 assert!(tree.children_of(b_dir).is_empty());
134 assert!(!tree.is_leaf(b_dir), "an unwalked dir must stay expandable");
135 assert!(!tree.fully_indexed());
136
137 assert!(tree.ensure_children(b_dir));
138 assert_eq!(tree.name(tree.children_of(b_dir)[0]), "inner.txt");
139
140 tree.index_all();
141 assert!(tree.fully_indexed());
142 assert!(tree.errors().is_empty());
143 }
144
145 #[test]
146 fn ancestor_ignore_rules_apply_to_lazily_walked_subdirectories() {
147 let dir = fixture();
148 std::fs::write(dir.path().join("b-dir/nested.log"), "").unwrap();
149 let mut tree = scan(dir.path(), false).unwrap();
150 let b_dir = tree.root_ids()[0];
151
152 tree.ensure_children(b_dir);
153
154 let names: Vec<String> = tree
155 .children_of(b_dir)
156 .iter()
157 .map(|&id| tree.name(id))
158 .collect();
159 assert!(!names.contains(&"nested.log".to_string()), "{names:?}");
160 assert!(names.contains(&"inner.txt".to_string()), "{names:?}");
161 }
162
163 #[cfg(unix)]
164 #[test]
165 fn unreadable_directories_record_an_error_instead_of_vanishing() {
166 use std::os::unix::fs::PermissionsExt;
167 let dir = tempfile::tempdir().unwrap();
168 let locked = dir.path().join("locked");
169 std::fs::create_dir(&locked).unwrap();
170 std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o000)).unwrap();
171
172 let mut tree = scan(dir.path(), false).unwrap();
173 tree.index_all();
174 std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o755)).unwrap();
175
176 assert!(tree.fully_indexed());
177 assert!(!tree.errors().is_empty());
178 }
179
180 #[test]
181 fn children_and_depth() {
182 let dir = fixture();
183 let tree = scan_all(dir.path());
184 let b_dir = tree.root_ids()[0];
185 assert_eq!(tree.name(b_dir), "b-dir");
186 assert_eq!(tree.depth(b_dir), 0);
187 let kids = tree.children_of(b_dir).to_vec();
188 assert_eq!(kids.len(), 1);
189 assert_eq!(tree.name(kids[0]), "inner.txt");
190 assert_eq!(tree.depth(kids[0]), 1);
191 assert_eq!(tree.parent(kids[0]), Some(b_dir));
192 }
193
194 #[test]
195 fn paths_are_absolute_and_relative() {
196 let dir = fixture();
197 let tree = scan_all(dir.path());
198 let b_dir = tree.root_ids()[0];
199 let inner = tree.children_of(b_dir)[0];
200 assert!(Path::new(&tree.path(inner)).is_absolute());
201 assert!(Path::new(&tree.path(inner)).ends_with("b-dir/inner.txt"));
202 assert_eq!(
203 tree.relpath(inner),
204 Path::new("b-dir/inner.txt").as_os_str()
205 );
206 assert_eq!(
207 tree.alternate_output(inner),
208 std::ffi::OsStr::new("inner.txt")
209 );
210 }
211
212 #[test]
213 fn leaf_classification() {
214 let dir = fixture();
215 let tree = scan_all(dir.path());
216 let by_name = |name: &str| {
217 tree.root_ids()
218 .iter()
219 .copied()
220 .find(|&id| tree.name(id) == name)
221 .unwrap()
222 };
223 assert!(!tree.is_leaf(by_name("b-dir")));
224 assert!(!tree.is_leaf(by_name("empty-dir")));
227 assert!(tree.is_leaf(by_name("a-file.txt")));
228 }
229
230 #[test]
231 fn tree_model_children_match_nodes() {
232 let dir = fixture();
233 let tree = scan_all(dir.path());
234 let b_dir = tree.root_ids()[0];
235 match tree.children(b_dir) {
236 TreeChildren::Loaded(kids) => assert_eq!(kids, tree.children_of(b_dir)),
237 other => panic!("expected Loaded, got {other:?}"),
238 }
239 let empty = tree.root_ids()[1];
242 assert_eq!(tree.children(empty), TreeChildren::Unloaded);
243 }
244
245 #[test]
246 fn unwalked_directories_report_unloaded_children() {
247 let dir = fixture();
248 let tree = scan(dir.path(), false).unwrap();
249 let b_dir = tree.root_ids()[0];
250 assert_eq!(tree.children(b_dir), TreeChildren::Unloaded);
251 }
252
253 #[test]
254 fn branches_lists_expandable_dirs() {
255 let dir = fixture();
256 let tree = scan_all(dir.path());
257 let names: Vec<String> = tree.branches().map(|(id, _)| tree.name(id)).collect();
258 assert_eq!(names, ["b-dir", "empty-dir"]);
259 }
260}