Skip to main content

toggle/
walk.rs

1// Directory traversal for recursive file discovery
2
3use anyhow::Result;
4use std::path::{Path, PathBuf};
5use walkdir::WalkDir;
6
7use crate::core::supported_extensions;
8use crate::exit_codes::UsageError;
9
10/// Configuration for directory walking
11pub struct WalkOptions {
12    pub skip_hidden: bool,
13    pub max_depth: Option<usize>,
14    pub verbose: bool,
15    /// When true, only collect files with extensions in `supported_extensions()`.
16    /// When false, collect all files (callers handle extension filtering themselves).
17    pub skip_unsupported_extensions: bool,
18}
19
20impl Default for WalkOptions {
21    fn default() -> Self {
22        Self {
23            skip_hidden: true,
24            max_depth: None,
25            verbose: false,
26            skip_unsupported_extensions: true,
27        }
28    }
29}
30
31/// Directories to always skip during recursive walks
32const SKIP_DIRS: &[&str] = &[
33    "node_modules",
34    "target",
35    "__pycache__",
36    "dist",
37    "build",
38    ".git",
39    ".hg",
40    ".svn",
41];
42
43/// Returns true if the directory entry should be skipped.
44fn should_skip_dir(name: &str, skip_hidden: bool) -> bool {
45    if skip_hidden && name.starts_with('.') {
46        return true;
47    }
48    SKIP_DIRS.contains(&name)
49}
50
51/// Returns true if the file has a supported extension for toggling.
52fn is_supported_file(path: &Path) -> bool {
53    path.extension()
54        .and_then(|ext| ext.to_str())
55        .map(|ext| supported_extensions().contains(&ext))
56        .unwrap_or(false)
57}
58
59/// Collect files from the given paths.
60///
61/// - If a path is a file, it is included directly (regardless of extension).
62/// - If a path is a directory and `recursive` is true, it is walked recursively,
63///   filtering to supported file extensions and skipping hidden/ignored directories.
64/// - If a path is a directory and `recursive` is false, an error is returned.
65///
66/// Results are sorted for deterministic output.
67pub fn collect_files(
68    paths: &[PathBuf],
69    recursive: bool,
70    opts: &WalkOptions,
71) -> Result<Vec<PathBuf>> {
72    let mut files = Vec::new();
73
74    for path in paths {
75        if path.is_file() || !path.exists() {
76            // Pass files (and nonexistent paths) through directly;
77            // downstream I/O will produce appropriate per-file errors.
78            files.push(path.clone());
79        } else if path.is_dir() {
80            if !recursive {
81                return Err(UsageError(format!(
82                    "'{}' is a directory; use -R/--recursive to process directories",
83                    path.display()
84                ))
85                .into());
86            }
87            walk_directory(path, opts, &mut files)?;
88        }
89    }
90
91    files.sort();
92    files.dedup();
93    Ok(files)
94}
95
96/// Walk a directory recursively, collecting supported files.
97fn walk_directory(dir: &Path, opts: &WalkOptions, files: &mut Vec<PathBuf>) -> Result<()> {
98    let mut walker = WalkDir::new(dir).follow_links(false);
99
100    if let Some(depth) = opts.max_depth {
101        walker = walker.max_depth(depth);
102    }
103
104    for entry in walker.into_iter().filter_entry(|e| {
105        // Allow the root directory through
106        if e.depth() == 0 {
107            return true;
108        }
109        // Skip filtered directories
110        if e.file_type().is_dir() {
111            let name = e.file_name().to_str().unwrap_or("");
112            return !should_skip_dir(name, opts.skip_hidden);
113        }
114        true
115    }) {
116        match entry {
117            Ok(entry) => {
118                if entry.file_type().is_file()
119                    && (!opts.skip_unsupported_extensions || is_supported_file(entry.path()))
120                {
121                    files.push(entry.into_path());
122                }
123            }
124            Err(e) => {
125                // Skip unreadable entries but continue walking
126                if opts.verbose {
127                    eprintln!("Warning: {}", e);
128                }
129            }
130        }
131    }
132
133    Ok(())
134}