1#![expect(
2 unused_results,
3 reason = "WalkBuilder configuration uses fluent setters only for their mutation side effects."
4)]
5
6use ignore::{DirEntry, WalkBuilder};
13use std::path::Path;
14
15use crate::exclusions::{DEFAULT_EXCLUDED_DIRS, VTCODE_IGNORE_FILE};
16
17pub fn build_default_walker(root: &Path) -> WalkBuilder {
26 let mut builder = WalkBuilder::new(root);
27 apply_defaults(&mut builder);
28 builder
29}
30
31pub fn build_walker_single_threaded(root: &Path) -> WalkBuilder {
38 let mut builder = WalkBuilder::new(root);
39 builder.threads(1);
40 apply_defaults(&mut builder);
41 builder
42}
43
44pub fn apply_defaults(builder: &mut WalkBuilder) {
50 builder.git_ignore(true);
52 builder.git_global(true);
53 builder.git_exclude(true);
54 builder.ignore(true);
55 builder.parents(true);
56
57 builder.add_custom_ignore_filename(VTCODE_IGNORE_FILE);
62
63 builder.follow_links(false);
65
66 builder.hidden(false);
70}
71
72pub fn entry_is_dir(entry: &std::fs::DirEntry) -> bool {
79 match entry.file_type() {
80 Ok(file_type) if !file_type.is_symlink() => file_type.is_dir(),
81 _ => entry.path().is_dir(),
82 }
83}
84
85pub fn entry_is_file(entry: &std::fs::DirEntry) -> bool {
89 match entry.file_type() {
90 Ok(file_type) if !file_type.is_symlink() => file_type.is_file(),
91 _ => entry.path().is_file(),
92 }
93}
94
95pub fn is_excluded_dir(entry: &DirEntry) -> bool {
104 if !entry.file_type().is_some_and(|ft| ft.is_dir()) {
105 return false;
106 }
107
108 entry
109 .file_name()
110 .to_str()
111 .is_some_and(|name| DEFAULT_EXCLUDED_DIRS.contains(&name))
112}
113
114#[cfg(test)]
115mod tests {
116 use std::fs;
117
118 use tempfile::tempdir;
119
120 use super::*;
121
122 fn collected_paths(root: &Path) -> Vec<String> {
123 let walker = build_default_walker(root).build();
124 let mut paths = walker
125 .filter_map(Result::ok)
126 .filter(|entry| entry.path() != root)
127 .map(|entry| {
128 entry
129 .path()
130 .strip_prefix(root)
131 .unwrap_or_else(|_| entry.path())
132 .to_string_lossy()
133 .into_owned()
134 })
135 .collect::<Vec<_>>();
136 paths.sort();
137 paths
138 }
139
140 #[test]
141 fn entry_type_helpers_match_the_resolving_answer() {
142 let temp = tempdir().expect("tempdir");
143 let root = temp.path();
144 fs::create_dir(root.join("dir")).expect("mkdir");
145 fs::write(root.join("file.txt"), "x").expect("write file");
146
147 for entry in fs::read_dir(root).expect("read_dir").flatten() {
148 let path = entry.path();
149 assert_eq!(entry_is_dir(&entry), path.is_dir(), "dir mismatch for {}", path.display());
151 assert_eq!(entry_is_file(&entry), path.is_file(), "file mismatch for {}", path.display());
152 }
153 }
154
155 #[cfg(unix)]
156 #[test]
157 fn entry_type_helpers_resolve_symlink_targets() {
158 use std::os::unix::fs::symlink;
159
160 let temp = tempdir().expect("tempdir");
161 let root = temp.path();
162 fs::create_dir(root.join("real-dir")).expect("mkdir");
163 fs::write(root.join("real-file.txt"), "x").expect("write file");
164 symlink(root.join("real-dir"), root.join("link-dir")).expect("dir symlink");
165 symlink(root.join("real-file.txt"), root.join("link-file")).expect("file symlink");
166
167 for entry in fs::read_dir(root).expect("read_dir").flatten() {
168 let path = entry.path();
169 assert_eq!(entry_is_dir(&entry), path.is_dir(), "dir mismatch for {}", path.display());
172 assert_eq!(entry_is_file(&entry), path.is_file(), "file mismatch for {}", path.display());
173 }
174 }
175
176 #[test]
177 fn default_walker_respects_vtcodegitignore() {
178 let temp = tempdir().expect("tempdir");
179 let root = temp.path();
180 fs::write(root.join(".vtcodegitignore"), "ignored_dir/\nignored_file.txt\n").expect("write ignore file");
181 fs::create_dir(root.join("ignored_dir")).expect("mkdir ignored_dir");
182 fs::write(root.join("ignored_dir/secret.rs"), "x").expect("write secret");
183 fs::write(root.join("ignored_file.txt"), "x").expect("write ignored file");
184 fs::write(root.join("kept.rs"), "x").expect("write kept file");
185
186 let paths = collected_paths(root);
187
188 assert!(paths.contains(&"kept.rs".to_owned()), "kept file should remain: {paths:?}");
189 assert!(!paths.iter().any(|p| p.contains("ignored_file.txt")), "ignored file leaked: {paths:?}");
190 assert!(!paths.iter().any(|p| p.contains("ignored_dir")), "ignored dir leaked: {paths:?}");
191 }
192
193 #[test]
194 fn default_walker_vtcodegitignore_negation_reincludes() {
195 let temp = tempdir().expect("tempdir");
196 let root = temp.path();
197 fs::write(root.join(".vtcodegitignore"), "*.log\n!important.log\n").expect("write ignore file");
201 fs::write(root.join("important.log"), "x").expect("write important log");
202 fs::write(root.join("noise.log"), "x").expect("write noise log");
203 fs::write(root.join("kept.rs"), "x").expect("write kept file");
204
205 let paths = collected_paths(root);
206
207 assert!(paths.contains(&"important.log".to_owned()), "negated file should be re-included: {paths:?}");
208 assert!(!paths.iter().any(|p| p.ends_with("noise.log")), "non-negated file leaked: {paths:?}");
209 assert!(paths.contains(&"kept.rs".to_owned()), "unrelated file should remain: {paths:?}");
210 }
211}