1use crate::error::Result;
2use ignore::WalkBuilder;
3use std::path::PathBuf;
4
5#[derive(Debug, Clone)]
7pub struct FileMatch {
8 pub path: PathBuf,
9}
10
11pub struct FileSearcher {
13 base_dir: PathBuf,
14 case_sensitive: bool,
15 exclusions: Vec<String>,
16}
17
18impl FileSearcher {
19 pub fn new(base_dir: PathBuf) -> Self {
20 Self {
21 base_dir,
22 case_sensitive: false,
23 exclusions: Vec::new(),
24 }
25 }
26
27 pub fn case_sensitive(mut self, value: bool) -> Self {
28 self.case_sensitive = value;
29 self
30 }
31
32 pub fn add_exclusions(mut self, exclusions: Vec<String>) -> Self {
33 self.exclusions.extend(exclusions);
34 self
35 }
36
37 pub fn search(&self, pattern: &str) -> Result<Vec<FileMatch>> {
39 let mut matches = Vec::new();
40
41 let pattern_lower = if self.case_sensitive {
42 pattern.to_string()
43 } else {
44 pattern.to_lowercase()
45 };
46
47 let walker = WalkBuilder::new(&self.base_dir)
48 .git_ignore(true)
49 .git_global(true)
50 .git_exclude(true)
51 .hidden(false)
52 .build();
53
54 for entry in walker.filter_map(|e| e.ok()) {
55 if !entry.file_type().map(|ft| ft.is_file()).unwrap_or(false) {
56 continue;
57 }
58
59 let path = entry.path();
60
61 let path_str = path.to_string_lossy();
63 if self
64 .exclusions
65 .iter()
66 .any(|ex| path_str.contains(ex.as_str()))
67 {
68 continue;
69 }
70
71 if let Some(file_name) = path.file_name() {
73 let file_name_str = file_name.to_string_lossy();
74 let file_name_compare = if self.case_sensitive {
75 file_name_str.to_string()
76 } else {
77 file_name_str.to_lowercase()
78 };
79
80 if file_name_compare.contains(&pattern_lower) {
81 matches.push(FileMatch {
82 path: path.to_path_buf(),
83 });
84 }
85 }
86 }
87
88 Ok(matches)
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95 use std::fs;
96 use tempfile::TempDir;
97
98 #[test]
99 fn test_file_search_basic() {
100 let temp_dir = TempDir::new().unwrap();
101 fs::write(temp_dir.path().join("test.txt"), "content").unwrap();
102 fs::write(temp_dir.path().join("other.txt"), "content").unwrap();
103
104 let searcher = FileSearcher::new(temp_dir.path().to_path_buf());
105 let matches = searcher.search("test").unwrap();
106
107 assert_eq!(matches.len(), 1);
108 assert!(matches[0].path.to_string_lossy().contains("test.txt"));
109 }
110
111 #[test]
112 fn test_file_search_case_insensitive() {
113 let temp_dir = TempDir::new().unwrap();
114 fs::write(temp_dir.path().join("Test.txt"), "content").unwrap();
115
116 let searcher = FileSearcher::new(temp_dir.path().to_path_buf());
117 let matches = searcher.search("test").unwrap();
118
119 assert_eq!(matches.len(), 1);
120 }
121
122 #[test]
123 fn test_file_search_case_sensitive() {
124 let temp_dir = TempDir::new().unwrap();
125 fs::write(temp_dir.path().join("Test.txt"), "content").unwrap();
126
127 let searcher = FileSearcher::new(temp_dir.path().to_path_buf()).case_sensitive(true);
128 let matches = searcher.search("test").unwrap();
129
130 assert_eq!(matches.len(), 0);
131 }
132}