1pub mod glob;
11pub mod grep;
12pub mod ripgrep;
13
14use serde::{Deserialize, Serialize};
15use std::path::PathBuf;
16use std::time::SystemTime;
17
18pub use glob::GlobTool;
20pub use grep::{GrepOutputMode, GrepTool};
21
22pub const DEFAULT_MAX_RESULTS: usize = 100;
24
25pub const DEFAULT_MAX_CONTEXT_LINES: usize = 5;
27
28pub const MAX_OUTPUT_SIZE: usize = 100_000;
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct SearchResult {
34 pub path: PathBuf,
36
37 pub line_number: Option<usize>,
39
40 pub line_content: Option<String>,
42
43 pub context_before: Vec<String>,
45
46 pub context_after: Vec<String>,
48
49 pub mtime: Option<SystemTime>,
51
52 pub size: Option<u64>,
54
55 pub match_count: Option<usize>,
57}
58
59impl SearchResult {
60 pub fn file_match(path: PathBuf) -> Self {
62 Self {
63 path,
64 line_number: None,
65 line_content: None,
66 context_before: Vec::new(),
67 context_after: Vec::new(),
68 mtime: None,
69 size: None,
70 match_count: None,
71 }
72 }
73
74 pub fn content_match(path: PathBuf, line_number: usize, line_content: String) -> Self {
76 Self {
77 path,
78 line_number: Some(line_number),
79 line_content: Some(line_content),
80 context_before: Vec::new(),
81 context_after: Vec::new(),
82 mtime: None,
83 size: None,
84 match_count: None,
85 }
86 }
87
88 pub fn count_match(path: PathBuf, count: usize) -> Self {
90 Self {
91 path,
92 line_number: None,
93 line_content: None,
94 context_before: Vec::new(),
95 context_after: Vec::new(),
96 mtime: None,
97 size: None,
98 match_count: Some(count),
99 }
100 }
101
102 pub fn with_metadata(mut self, mtime: SystemTime, size: u64) -> Self {
104 self.mtime = Some(mtime);
105 self.size = Some(size);
106 self
107 }
108
109 pub fn with_context(mut self, before: Vec<String>, after: Vec<String>) -> Self {
111 self.context_before = before;
112 self.context_after = after;
113 self
114 }
115}
116
117pub fn format_search_results(results: &[SearchResult], truncated: bool) -> String {
119 let mut output = String::new();
120
121 for result in results {
122 if let Some(line_number) = result.line_number {
123 output.push_str(&format!(
125 "{}:{}:{}\n",
126 result.path.display(),
127 line_number,
128 result.line_content.as_deref().unwrap_or("")
129 ));
130 } else if let Some(count) = result.match_count {
131 output.push_str(&format!("{}:{}\n", result.path.display(), count));
133 } else {
134 output.push_str(&format!("{}\n", result.path.display()));
136 }
137 }
138
139 if truncated {
140 output.push_str(&format!(
141 "\n[Results truncated. Showing {} of more results.]\n",
142 results.len()
143 ));
144 }
145
146 output
147}
148
149pub fn truncate_results(
151 results: Vec<SearchResult>,
152 max_results: usize,
153) -> (Vec<SearchResult>, bool) {
154 if results.len() > max_results {
155 (results.into_iter().take(max_results).collect(), true)
156 } else {
157 (results, false)
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 #[test]
166 fn test_search_result_file_match() {
167 let result = SearchResult::file_match(PathBuf::from("/tmp/test.txt"));
168 assert_eq!(result.path, PathBuf::from("/tmp/test.txt"));
169 assert!(result.line_number.is_none());
170 assert!(result.line_content.is_none());
171 }
172
173 #[test]
174 fn test_search_result_content_match() {
175 let result = SearchResult::content_match(
176 PathBuf::from("/tmp/test.txt"),
177 42,
178 "Hello, World!".to_string(),
179 );
180 assert_eq!(result.path, PathBuf::from("/tmp/test.txt"));
181 assert_eq!(result.line_number, Some(42));
182 assert_eq!(result.line_content, Some("Hello, World!".to_string()));
183 }
184
185 #[test]
186 fn test_search_result_count_match() {
187 let result = SearchResult::count_match(PathBuf::from("/tmp/test.txt"), 10);
188 assert_eq!(result.path, PathBuf::from("/tmp/test.txt"));
189 assert_eq!(result.match_count, Some(10));
190 }
191
192 #[test]
193 fn test_search_result_with_metadata() {
194 let mtime = SystemTime::now();
195 let result =
196 SearchResult::file_match(PathBuf::from("/tmp/test.txt")).with_metadata(mtime, 1024);
197 assert_eq!(result.mtime, Some(mtime));
198 assert_eq!(result.size, Some(1024));
199 }
200
201 #[test]
202 fn test_search_result_with_context() {
203 let result = SearchResult::content_match(
204 PathBuf::from("/tmp/test.txt"),
205 5,
206 "match line".to_string(),
207 )
208 .with_context(
209 vec!["line 3".to_string(), "line 4".to_string()],
210 vec!["line 6".to_string(), "line 7".to_string()],
211 );
212 assert_eq!(result.context_before.len(), 2);
213 assert_eq!(result.context_after.len(), 2);
214 }
215
216 #[test]
217 fn test_format_search_results_grep() {
218 let results = vec![
219 SearchResult::content_match(PathBuf::from("/tmp/test.txt"), 10, "Hello".to_string()),
220 SearchResult::content_match(PathBuf::from("/tmp/test.txt"), 20, "World".to_string()),
221 ];
222
223 let output = format_search_results(&results, false);
224 assert!(output.contains("/tmp/test.txt:10:Hello"));
225 assert!(output.contains("/tmp/test.txt:20:World"));
226 }
227
228 #[test]
229 fn test_format_search_results_glob() {
230 let results = vec![
231 SearchResult::file_match(PathBuf::from("/tmp/a.txt")),
232 SearchResult::file_match(PathBuf::from("/tmp/b.txt")),
233 ];
234
235 let output = format_search_results(&results, false);
236 assert!(output.contains("/tmp/a.txt"));
237 assert!(output.contains("/tmp/b.txt"));
238 }
239
240 #[test]
241 fn test_format_search_results_truncated() {
242 let results = vec![SearchResult::file_match(PathBuf::from("/tmp/test.txt"))];
243 let output = format_search_results(&results, true);
244 assert!(output.contains("[Results truncated"));
245 }
246
247 #[test]
248 fn test_truncate_results() {
249 let results: Vec<SearchResult> = (0..10)
250 .map(|i| SearchResult::file_match(PathBuf::from(format!("/tmp/test{}.txt", i))))
251 .collect();
252
253 let (truncated, was_truncated) = truncate_results(results.clone(), 5);
254 assert_eq!(truncated.len(), 5);
255 assert!(was_truncated);
256
257 let (not_truncated, was_truncated) = truncate_results(results, 20);
258 assert_eq!(not_truncated.len(), 10);
259 assert!(!was_truncated);
260 }
261}