cs/search/
file_search.rs

1use crate::error::Result;
2use ignore::WalkBuilder;
3use std::path::PathBuf;
4
5/// Result of a file search
6#[derive(Debug, Clone)]
7pub struct FileMatch {
8    pub path: PathBuf,
9}
10
11/// File searcher that finds files by name pattern
12pub 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    /// Search for files matching the pattern
38    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            // Apply exclusions
62            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            // Check if filename matches pattern
72            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}