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 {
40 let mut builder = WalkBuilder::new(root);
41 builder.threads(1);
42 apply_defaults(&mut builder);
43 builder
44}
45
46pub fn apply_defaults(builder: &mut WalkBuilder) {
52 builder.git_ignore(true);
54 builder.git_global(true);
55 builder.git_exclude(true);
56 builder.ignore(true);
57 builder.parents(true);
58
59 builder.add_custom_ignore_filename(VTCODE_IGNORE_FILE);
64
65 builder.follow_links(false);
67
68 builder.hidden(false);
72}
73
74pub fn entry_is_dir(entry: &std::fs::DirEntry) -> bool {
81 match entry.file_type() {
82 Ok(file_type) if !file_type.is_symlink() => file_type.is_dir(),
83 _ => entry.path().is_dir(),
84 }
85}
86
87pub fn entry_is_file(entry: &std::fs::DirEntry) -> bool {
91 match entry.file_type() {
92 Ok(file_type) if !file_type.is_symlink() => file_type.is_file(),
93 _ => entry.path().is_file(),
94 }
95}
96
97pub fn is_excluded_dir(entry: &DirEntry) -> bool {
106 if !entry.file_type().is_some_and(|ft| ft.is_dir()) {
107 return false;
108 }
109
110 entry
111 .file_name()
112 .to_str()
113 .is_some_and(|name| DEFAULT_EXCLUDED_DIRS.contains(&name))
114}
115
116#[cfg(test)]
117mod tests {
118 use std::fs;
119
120 use tempfile::tempdir;
121
122 use super::*;
123
124 fn collected_paths(root: &Path) -> Vec<String> {
125 let walker = build_default_walker(root).build();
126 let mut paths = walker
127 .filter_map(Result::ok)
128 .filter(|entry| entry.path() != root)
129 .map(|entry| {
130 entry
131 .path()
132 .strip_prefix(root)
133 .unwrap_or_else(|_| entry.path())
134 .to_string_lossy()
135 .into_owned()
136 })
137 .collect::<Vec<_>>();
138 paths.sort();
139 paths
140 }
141
142 #[test]
143 fn entry_type_helpers_match_the_resolving_answer() {
144 let temp = tempdir().expect("tempdir");
145 let root = temp.path();
146 fs::create_dir(root.join("dir")).expect("mkdir");
147 fs::write(root.join("file.txt"), "x").expect("write file");
148
149 for entry in fs::read_dir(root).expect("read_dir").flatten() {
150 let path = entry.path();
151 assert_eq!(entry_is_dir(&entry), path.is_dir(), "dir mismatch for {}", path.display());
153 assert_eq!(entry_is_file(&entry), path.is_file(), "file mismatch for {}", path.display());
154 }
155 }
156
157 #[cfg(unix)]
158 #[test]
159 fn entry_type_helpers_resolve_symlink_targets() {
160 use std::os::unix::fs::symlink;
161
162 let temp = tempdir().expect("tempdir");
163 let root = temp.path();
164 fs::create_dir(root.join("real-dir")).expect("mkdir");
165 fs::write(root.join("real-file.txt"), "x").expect("write file");
166 symlink(root.join("real-dir"), root.join("link-dir")).expect("dir symlink");
167 symlink(root.join("real-file.txt"), root.join("link-file")).expect("file symlink");
168
169 for entry in fs::read_dir(root).expect("read_dir").flatten() {
170 let path = entry.path();
171 assert_eq!(entry_is_dir(&entry), path.is_dir(), "dir mismatch for {}", path.display());
174 assert_eq!(entry_is_file(&entry), path.is_file(), "file mismatch for {}", path.display());
175 }
176 }
177
178 #[test]
179 fn default_walker_respects_vtcodegitignore() {
180 let temp = tempdir().expect("tempdir");
181 let root = temp.path();
182 fs::write(root.join(".vtcodegitignore"), "ignored_dir/\nignored_file.txt\n").expect("write ignore file");
183 fs::create_dir(root.join("ignored_dir")).expect("mkdir ignored_dir");
184 fs::write(root.join("ignored_dir/secret.rs"), "x").expect("write secret");
185 fs::write(root.join("ignored_file.txt"), "x").expect("write ignored file");
186 fs::write(root.join("kept.rs"), "x").expect("write kept file");
187
188 let paths = collected_paths(root);
189
190 assert!(paths.contains(&"kept.rs".to_owned()), "kept file should remain: {paths:?}");
191 assert!(!paths.iter().any(|p| p.contains("ignored_file.txt")), "ignored file leaked: {paths:?}");
192 assert!(!paths.iter().any(|p| p.contains("ignored_dir")), "ignored dir leaked: {paths:?}");
193 }
194
195 #[test]
196 fn default_walker_vtcodegitignore_negation_reincludes() {
197 let temp = tempdir().expect("tempdir");
198 let root = temp.path();
199 fs::write(root.join(".vtcodegitignore"), "*.log\n!important.log\n").expect("write ignore file");
203 fs::write(root.join("important.log"), "x").expect("write important log");
204 fs::write(root.join("noise.log"), "x").expect("write noise log");
205 fs::write(root.join("kept.rs"), "x").expect("write kept file");
206
207 let paths = collected_paths(root);
208
209 assert!(paths.contains(&"important.log".to_owned()), "negated file should be re-included: {paths:?}");
210 assert!(!paths.iter().any(|p| p.ends_with("noise.log")), "non-negated file leaked: {paths:?}");
211 assert!(paths.contains(&"kept.rs".to_owned()), "unrelated file should remain: {paths:?}");
212 }
213}