join_ai/walker.rs
1use crate::cli::JoinArgs;
2use ignore::{WalkBuilder, WalkState};
3use std::path::PathBuf;
4use std::sync::mpsc;
5
6/// This module is responsible for efficiently finding all files that match the
7/// user's criteria using the `ignore` crate, which is excellent at respecting
8/// rules like `.gitignore` and handling parallel directory traversal.
9///
10/// The walker runs in a separate thread pool and sends valid file paths back to the
11/// main thread through a multi-producer, single-consumer (mpsc) channel.
12///
13/// # Arguments
14/// * `args` - A reference to the parsed `JoinArgs` containing all CLI options.
15///
16/// # Returns
17/// A `Result` containing the receiver end of the channel, which will be used by
18/// the processor to receive file paths.
19pub fn find_files(args: &JoinArgs) -> anyhow::Result<mpsc::Receiver<PathBuf>> {
20 // Create a channel for communication between the walker threads and the main thread.
21 let (tx, rx) = mpsc::channel();
22 let input_folder = args.input_folder.clone();
23
24 // --- 1. Configure the base walker ---
25 let mut walker_builder = WalkBuilder::new(&input_folder);
26 walker_builder
27 .follow_links(!args.no_follow)
28 .max_depth(args.max_depth);
29
30 // --- 2. Build a set of override rules for inclusion and exclusion ---
31 // The `OverrideBuilder` allows us to programmatically add glob patterns that
32 // take precedence over any `.gitignore` or similar rules.
33 let mut override_builder = ignore::overrides::OverrideBuilder::new(&input_folder);
34
35 // Add inclusion patterns. If none are provided, default to including everything.
36 if let Some(patterns) = &args.patterns {
37 for pattern in patterns {
38 override_builder.add(pattern)?;
39 }
40 } else {
41 // A single "*" will match all files, which is a good default.
42 override_builder.add("*")?;
43 }
44
45 // Add all exclusion patterns. These are prefixed with "!" to negate the match.
46 if let Some(exclude_patterns) = &args.exclude {
47 for pattern in exclude_patterns {
48 let exclusion_pattern = format!("!{pattern}");
49 override_builder.add(&exclusion_pattern)?;
50 }
51 }
52
53 // If hidden files are not requested, add a global ignore pattern for them.
54 // This is necessary because the `*` override would otherwise include them.
55 if !args.hidden {
56 override_builder.add("!.*")?;
57 }
58
59 // Apply the built override rules to the walker.
60 let overrides = override_builder.build()?;
61 walker_builder.overrides(overrides);
62
63 // --- 3. Run the walker in parallel ---
64 let walker = walker_builder.build_parallel();
65 let output_file_path = args.output_file.clone();
66
67 // The `run` method spawns a thread pool to perform the walk.
68 // We provide a closure that builds a "move closure" for each thread.
69 walker.run(move || {
70 // Clone the transmitter and other necessary data for each thread.
71 let tx = tx.clone();
72 let output_file_path = output_file_path.clone();
73
74 // This inner closure is executed for each directory entry found.
75 Box::new(move |result| {
76 if let Ok(entry) = result {
77 let path = entry.path();
78 // Skip directories and the application's own output file.
79 if path.is_dir() || path == output_file_path {
80 return WalkState::Continue;
81 }
82
83 // All filtering is now handled by the `overrides`, so we don't
84 // need to manually check extensions or folders here.
85
86 // If all checks pass, send the valid file path to the processor.
87 tx.send(path.to_path_buf()).expect("Failed to send path");
88 }
89 // Continue the walk regardless of the result.
90 WalkState::Continue
91 })
92 });
93
94 // Return the receiver end of the channel to the caller.
95 Ok(rx)
96}