#![expect(
unused_results,
reason = "WalkBuilder configuration uses fluent setters only for their mutation side effects."
)]
use ignore::{DirEntry, WalkBuilder};
use std::path::Path;
use crate::exclusions::{DEFAULT_EXCLUDED_DIRS, VTCODE_IGNORE_FILE};
pub fn build_default_walker(root: &Path) -> WalkBuilder {
let mut builder = WalkBuilder::new(root);
apply_defaults(&mut builder);
builder
}
pub fn build_walker_single_threaded(root: &Path) -> WalkBuilder {
let mut builder = WalkBuilder::new(root);
builder.threads(1);
apply_defaults(&mut builder);
builder
}
pub fn apply_defaults(builder: &mut WalkBuilder) {
builder.git_ignore(true);
builder.git_global(true);
builder.git_exclude(true);
builder.ignore(true);
builder.parents(true);
builder.add_custom_ignore_filename(VTCODE_IGNORE_FILE);
builder.follow_links(false);
builder.hidden(false);
}
pub fn entry_is_dir(entry: &std::fs::DirEntry) -> bool {
match entry.file_type() {
Ok(file_type) if !file_type.is_symlink() => file_type.is_dir(),
_ => entry.path().is_dir(),
}
}
pub fn entry_is_file(entry: &std::fs::DirEntry) -> bool {
match entry.file_type() {
Ok(file_type) if !file_type.is_symlink() => file_type.is_file(),
_ => entry.path().is_file(),
}
}
pub fn is_excluded_dir(entry: &DirEntry) -> bool {
if !entry.file_type().is_some_and(|ft| ft.is_dir()) {
return false;
}
entry
.file_name()
.to_str()
.is_some_and(|name| DEFAULT_EXCLUDED_DIRS.contains(&name))
}
#[cfg(test)]
mod tests {
use std::fs;
use tempfile::tempdir;
use super::*;
fn collected_paths(root: &Path) -> Vec<String> {
let walker = build_default_walker(root).build();
let mut paths = walker
.filter_map(Result::ok)
.filter(|entry| entry.path() != root)
.map(|entry| {
entry
.path()
.strip_prefix(root)
.unwrap_or_else(|_| entry.path())
.to_string_lossy()
.into_owned()
})
.collect::<Vec<_>>();
paths.sort();
paths
}
#[test]
fn entry_type_helpers_match_the_resolving_answer() {
let temp = tempdir().expect("tempdir");
let root = temp.path();
fs::create_dir(root.join("dir")).expect("mkdir");
fs::write(root.join("file.txt"), "x").expect("write file");
for entry in fs::read_dir(root).expect("read_dir").flatten() {
let path = entry.path();
assert_eq!(entry_is_dir(&entry), path.is_dir(), "dir mismatch for {}", path.display());
assert_eq!(entry_is_file(&entry), path.is_file(), "file mismatch for {}", path.display());
}
}
#[cfg(unix)]
#[test]
fn entry_type_helpers_resolve_symlink_targets() {
use std::os::unix::fs::symlink;
let temp = tempdir().expect("tempdir");
let root = temp.path();
fs::create_dir(root.join("real-dir")).expect("mkdir");
fs::write(root.join("real-file.txt"), "x").expect("write file");
symlink(root.join("real-dir"), root.join("link-dir")).expect("dir symlink");
symlink(root.join("real-file.txt"), root.join("link-file")).expect("file symlink");
for entry in fs::read_dir(root).expect("read_dir").flatten() {
let path = entry.path();
assert_eq!(entry_is_dir(&entry), path.is_dir(), "dir mismatch for {}", path.display());
assert_eq!(entry_is_file(&entry), path.is_file(), "file mismatch for {}", path.display());
}
}
#[test]
fn default_walker_respects_vtcodegitignore() {
let temp = tempdir().expect("tempdir");
let root = temp.path();
fs::write(root.join(".vtcodegitignore"), "ignored_dir/\nignored_file.txt\n").expect("write ignore file");
fs::create_dir(root.join("ignored_dir")).expect("mkdir ignored_dir");
fs::write(root.join("ignored_dir/secret.rs"), "x").expect("write secret");
fs::write(root.join("ignored_file.txt"), "x").expect("write ignored file");
fs::write(root.join("kept.rs"), "x").expect("write kept file");
let paths = collected_paths(root);
assert!(paths.contains(&"kept.rs".to_owned()), "kept file should remain: {paths:?}");
assert!(!paths.iter().any(|p| p.contains("ignored_file.txt")), "ignored file leaked: {paths:?}");
assert!(!paths.iter().any(|p| p.contains("ignored_dir")), "ignored dir leaked: {paths:?}");
}
#[test]
fn default_walker_vtcodegitignore_negation_reincludes() {
let temp = tempdir().expect("tempdir");
let root = temp.path();
fs::write(root.join(".vtcodegitignore"), "*.log\n!important.log\n").expect("write ignore file");
fs::write(root.join("important.log"), "x").expect("write important log");
fs::write(root.join("noise.log"), "x").expect("write noise log");
fs::write(root.join("kept.rs"), "x").expect("write kept file");
let paths = collected_paths(root);
assert!(paths.contains(&"important.log".to_owned()), "negated file should be re-included: {paths:?}");
assert!(!paths.iter().any(|p| p.ends_with("noise.log")), "non-negated file leaked: {paths:?}");
assert!(paths.contains(&"kept.rs".to_owned()), "unrelated file should remain: {paths:?}");
}
}