vtcode_core/tools/
file_search_bridge.rs1use anyhow::Result;
13use std::num::NonZero;
14use std::path::PathBuf;
15use std::sync::Arc;
16use std::sync::atomic::AtomicBool;
17use vtcode_file_search::{
18 FileMatch, FileSearchResults, MatchType, file_name_from_path, run as file_search_run,
19};
20
21pub use vtcode_file_search::MatchType as FileMatchType;
22
23#[derive(Debug, Clone)]
25pub struct FileSearchConfig {
26 pub pattern: String,
28 pub search_dir: PathBuf,
30 pub exclude_patterns: Vec<String>,
32 pub max_results: usize,
34 pub num_threads: usize,
36 pub respect_gitignore: bool,
38 pub compute_indices: bool,
40}
41
42impl FileSearchConfig {
43 pub fn new(pattern: String, search_dir: PathBuf) -> Self {
45 Self {
46 pattern,
47 search_dir,
48 exclude_patterns: vec![],
49 max_results: 100,
50 num_threads: num_cpus::get(),
51 respect_gitignore: true,
52 compute_indices: false,
53 }
54 }
55
56 pub fn exclude(mut self, pattern: impl Into<String>) -> Self {
58 self.exclude_patterns.push(pattern.into());
59 self
60 }
61
62 pub fn with_limit(mut self, limit: usize) -> Self {
64 self.max_results = limit;
65 self
66 }
67
68 pub fn with_threads(mut self, threads: usize) -> Self {
70 self.num_threads = threads.max(1);
71 self
72 }
73
74 pub fn respect_gitignore(mut self, respect: bool) -> Self {
76 self.respect_gitignore = respect;
77 self
78 }
79
80 pub fn compute_indices(mut self, compute: bool) -> Self {
82 self.compute_indices = compute;
83 self
84 }
85}
86
87pub fn search_files(
98 config: FileSearchConfig,
99 cancel_flag: Option<Arc<AtomicBool>>,
100) -> Result<FileSearchResults> {
101 let cancel = cancel_flag.unwrap_or_else(|| Arc::new(AtomicBool::new(false)));
102
103 let limit = NonZero::new(config.max_results).unwrap_or(NonZero::<usize>::MIN);
104 let threads = NonZero::new(config.num_threads).unwrap_or(NonZero::<usize>::MIN);
105
106 file_search_run(vtcode_file_search::FileSearchConfig {
107 pattern_text: config.pattern,
108 limit,
109 search_directory: config.search_dir,
110 exclude: config.exclude_patterns,
111 threads,
112 cancel_flag: cancel,
113 compute_indices: config.compute_indices,
114 respect_gitignore: config.respect_gitignore,
115 })
116}
117
118pub fn match_filename(file_match: &FileMatch) -> String {
122 file_name_from_path(&file_match.path)
123}
124
125pub fn file_matches_only(matches: Vec<FileMatch>) -> Vec<FileMatch> {
127 matches
128 .into_iter()
129 .filter(|m| matches!(m.match_type, MatchType::File))
130 .collect()
131}
132
133pub fn filter_by_extension(matches: Vec<FileMatch>, extensions: &[&str]) -> Vec<FileMatch> {
140 matches
141 .into_iter()
142 .filter(|m| {
143 extensions
144 .iter()
145 .any(|ext| m.path.ends_with(&format!(".{}", ext)) || m.path.ends_with(ext))
146 })
147 .collect()
148}
149
150pub fn filter_by_pattern(matches: Vec<FileMatch>, path_pattern: &str) -> Vec<FileMatch> {
157 if let Ok(pattern) = glob::Pattern::new(path_pattern) {
158 matches
159 .into_iter()
160 .filter(|m| pattern.matches(&m.path))
161 .collect()
162 } else {
163 matches
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 #[test]
172 fn test_file_search_config_builder() {
173 let config = FileSearchConfig::new("test".to_string(), PathBuf::from("."))
174 .exclude("target/**")
175 .with_limit(50)
176 .with_threads(4);
177
178 assert_eq!(config.pattern, "test");
179 assert_eq!(config.max_results, 50);
180 assert_eq!(config.num_threads, 4);
181 assert_eq!(config.exclude_patterns.len(), 1);
182 }
183
184 #[test]
185 fn test_match_filename() {
186 let file_match = FileMatch {
187 score: 100,
188 path: "src/utils/helper.rs".to_string(),
189 match_type: MatchType::File,
190 indices: None,
191 };
192
193 assert_eq!(match_filename(&file_match), "helper.rs");
194 }
195
196 #[test]
197 fn test_filter_by_extension() {
198 let matches = vec![
199 FileMatch {
200 score: 100,
201 path: "src/main.rs".to_string(),
202 match_type: MatchType::File,
203 indices: None,
204 },
205 FileMatch {
206 score: 90,
207 path: "src/config.toml".to_string(),
208 match_type: MatchType::File,
209 indices: None,
210 },
211 FileMatch {
212 score: 80,
213 path: "src/data.json".to_string(),
214 match_type: MatchType::File,
215 indices: None,
216 },
217 ];
218
219 let filtered = filter_by_extension(matches, &["rs", "toml"]);
220 assert_eq!(filtered.len(), 2);
221 assert!(
222 filtered
223 .iter()
224 .all(|m| { m.path.ends_with(".rs") || m.path.ends_with(".toml") })
225 );
226 }
227
228 #[test]
229 fn test_file_matches_only_filters_directories() {
230 let matches = vec![
231 FileMatch {
232 score: 100,
233 path: "src".to_string(),
234 match_type: MatchType::Directory,
235 indices: None,
236 },
237 FileMatch {
238 score: 90,
239 path: "src/main.rs".to_string(),
240 match_type: MatchType::File,
241 indices: None,
242 },
243 ];
244
245 let filtered = file_matches_only(matches);
246
247 assert_eq!(filtered.len(), 1);
248 assert!(matches!(filtered[0].match_type, MatchType::File));
249 }
250}