1use std::path::{Path, PathBuf};
4
5use crate::tree::{ActionValues, Node, NodeId, Tree};
6
7pub fn scan(dir: &Path, no_ignore: bool) -> std::io::Result<Tree> {
11 let _span = crate::profile::span("fstree::scan");
12 let root_dir = dir.canonicalize()?;
13 let mut tree = Tree::new();
14 let mut ids_by_path: std::collections::HashMap<PathBuf, NodeId> =
15 std::collections::HashMap::new();
16
17 let walk = ignore::WalkBuilder::new(&root_dir)
18 .standard_filters(!no_ignore)
19 .hidden(false)
20 .sort_by_file_name(|a, b| a.to_ascii_lowercase().cmp(&b.to_ascii_lowercase()))
21 .build();
22 for entry in walk {
23 let Ok(entry) = entry else { continue };
24 if entry.depth() == 0 {
25 continue; }
27 let path = entry.path().to_path_buf();
28 let is_dir = entry.file_type().is_some_and(|t| t.is_dir());
29 let parent = path.parent().and_then(|p| ids_by_path.get(p)).copied();
30 let relpath = path.strip_prefix(&root_dir).unwrap_or(&path);
31 let action = ActionValues::new(path.as_os_str(), path.as_os_str(), relpath.as_os_str())
32 .with_alternate_output(entry.file_name());
33 let id = tree.push(parent, entry.file_name().to_string_lossy(), is_dir, action);
34 if is_dir {
35 ids_by_path.insert(path, id);
36 }
37 }
38
39 let dirs_first = |nodes: &[Node], ids: &mut Vec<NodeId>| {
42 ids.sort_by_key(|&id| !nodes[id].is_container);
43 };
44 let mut roots = std::mem::take(&mut tree.roots);
45 dirs_first(&tree.nodes, &mut roots);
46 tree.roots = roots;
47 for id in 0..tree.nodes.len() {
48 let mut children = std::mem::take(&mut tree.nodes[id].children);
49 dirs_first(&tree.nodes, &mut children);
50 tree.nodes[id].children = children;
51 }
52 Ok(tree)
53}
54
55#[cfg(test)]
56mod tests {
57 use super::*;
58 use tui_treelistview::{TreeChildren, TreeModel};
59
60 fn fixture() -> tempfile::TempDir {
70 let dir = tempfile::tempdir().unwrap();
71 let p = dir.path();
72 std::fs::write(p.join(".hidden-file"), "").unwrap();
73 std::fs::create_dir(p.join("b-dir")).unwrap();
74 std::fs::write(p.join("b-dir/inner.txt"), "").unwrap();
75 std::fs::create_dir(p.join("empty-dir")).unwrap();
76 std::fs::write(p.join("a-file.txt"), "").unwrap();
77 std::fs::write(p.join("z-file.txt"), "").unwrap();
78 std::fs::write(p.join("ignored.log"), "").unwrap();
79 std::fs::write(p.join(".ignore"), "*.log\n").unwrap();
80 dir
81 }
82
83 fn root_names(tree: &Tree) -> Vec<String> {
84 tree.root_ids()
85 .iter()
86 .map(|&id| tree.node(id).name.clone())
87 .collect()
88 }
89
90 #[test]
91 fn default_scan_shows_dotfiles_but_honors_ignore_files() {
92 let dir = fixture();
93 let tree = scan(dir.path(), false).unwrap();
94 assert_eq!(
96 root_names(&tree),
97 [
98 "b-dir",
99 "empty-dir",
100 ".hidden-file",
101 ".ignore",
102 "a-file.txt",
103 "z-file.txt",
104 ]
105 );
106 assert!(!root_names(&tree).contains(&"ignored.log".to_string()));
107 }
108
109 #[test]
110 fn no_ignore_reveals_ignored_files() {
111 let dir = fixture();
112 let tree = scan(dir.path(), true).unwrap();
113 let names = root_names(&tree);
114 assert!(names.contains(&".hidden-file".to_string()));
115 assert!(names.contains(&"ignored.log".to_string()));
116 }
117
118 #[test]
119 fn children_and_depth() {
120 let dir = fixture();
121 let tree = scan(dir.path(), false).unwrap();
122 let b_dir = tree.root_ids()[0];
123 assert_eq!(tree.node(b_dir).name, "b-dir");
124 assert_eq!(tree.node(b_dir).depth, 0);
125 let kids = tree.node(b_dir).children.clone();
126 assert_eq!(kids.len(), 1);
127 assert_eq!(tree.node(kids[0]).name, "inner.txt");
128 assert_eq!(tree.node(kids[0]).depth, 1);
129 assert_eq!(tree.node(kids[0]).parent, Some(b_dir));
130 }
131
132 #[test]
133 fn paths_are_absolute_and_relative() {
134 let dir = fixture();
135 let tree = scan(dir.path(), false).unwrap();
136 let b_dir = tree.root_ids()[0];
137 let inner = tree.node(b_dir).children[0];
138 assert!(Path::new(&tree.node(inner).action.path).is_absolute());
139 assert!(Path::new(&tree.node(inner).action.path).ends_with("b-dir/inner.txt"));
140 assert_eq!(
141 tree.node(inner).action.relpath,
142 Path::new("b-dir/inner.txt").as_os_str()
143 );
144 assert_eq!(
145 tree.node(inner).action.alternate_output,
146 std::ffi::OsStr::new("inner.txt")
147 );
148 }
149
150 #[test]
151 fn leaf_classification() {
152 let dir = fixture();
153 let tree = scan(dir.path(), false).unwrap();
154 let by_name = |name: &str| {
155 tree.root_ids()
156 .iter()
157 .copied()
158 .find(|&id| tree.node(id).name == name)
159 .unwrap()
160 };
161 assert!(!tree.is_leaf(by_name("b-dir")));
162 assert!(tree.is_leaf(by_name("empty-dir")));
164 assert!(tree.is_leaf(by_name("a-file.txt")));
165 }
166
167 #[test]
168 fn tree_model_children_match_nodes() {
169 let dir = fixture();
170 let tree = scan(dir.path(), false).unwrap();
171 let b_dir = tree.root_ids()[0];
172 match tree.children(b_dir) {
173 TreeChildren::Loaded(kids) => assert_eq!(kids, tree.node(b_dir).children.as_slice()),
174 other => panic!("expected Loaded, got {other:?}"),
175 }
176 let empty = tree.root_ids()[1];
177 assert_eq!(tree.children(empty), TreeChildren::Leaf);
178 }
179
180 #[test]
181 fn branches_lists_expandable_dirs() {
182 let dir = fixture();
183 let tree = scan(dir.path(), false).unwrap();
184 let names: Vec<&str> = tree
185 .branches()
186 .map(|(id, _)| tree.node(id).name.as_str())
187 .collect();
188 assert_eq!(names, ["b-dir"]);
189 }
190}