1use anyhow::Result;
4use std::path::{Path, PathBuf};
5use std::sync::Arc;
6use tracing::debug;
7
8pub mod engine;
9pub mod matcher;
10pub mod walker;
11
12pub use engine::SearchEngine;
13pub use matcher::{Matcher, MatchType};
14
15#[derive(Debug, Clone, serde::Serialize)]
17pub struct Match {
18 pub line_number: usize,
20 #[serde(skip_serializing)]
22 pub column_range: std::ops::Range<usize>,
23 pub line_text: String,
25 pub before_context: Vec<String>,
27 pub after_context: Vec<String>,
29 pub ast_context: Option<crate::ast::AstContext>,
31 pub score: f64,
33}
34
35#[derive(Debug, Clone, serde::Serialize)]
37pub struct SearchResult {
38 pub path: PathBuf,
40 pub matches: Vec<Match>,
42 pub metadata: FileMetadata,
44 #[cfg(feature = "ai")]
46 pub ai_insights: Option<AiInsights>,
47}
48
49#[derive(Debug, Clone, serde::Serialize)]
51pub struct FileMetadata {
52 pub size: u64,
54 pub language: Option<String>,
56 pub encoding: String,
58 #[serde(skip_serializing)]
60 pub modified: std::time::SystemTime,
61}
62
63#[cfg(feature = "ai")]
65#[derive(Debug, Clone, serde::Serialize)]
66pub struct AiInsights {
67 pub summary: String,
69 pub explanation: Option<String>,
71 pub suggestions: Vec<String>,
73 pub related_locations: Vec<PathBuf>,
75}
76
77#[derive(Debug, Default)]
79pub struct SearchStats {
80 pub files_searched: usize,
82 pub files_matched: usize,
84 pub total_matches: usize,
86 pub files_skipped: usize,
88 pub duration: std::time::Duration,
90}
91
92pub struct SearchOptions {
94 pub pattern: String,
96 pub matcher: Arc<dyn Matcher>,
98 pub config: Arc<crate::Config>,
100}
101
102impl Clone for SearchOptions {
103 fn clone(&self) -> Self {
104 Self {
105 pattern: self.pattern.clone(),
106 matcher: Arc::clone(&self.matcher),
107 config: Arc::clone(&self.config),
108 }
109 }
110}
111
112impl SearchOptions {
113 pub fn new(pattern: &str, config: Arc<crate::Config>) -> Result<Self> {
115 let matcher: Arc<dyn Matcher> = if config.search.regex {
116 Arc::new(matcher::RegexMatcher::new(pattern, config.search.ignore_case)?)
117 } else {
118 Arc::new(matcher::LiteralMatcher::new(pattern, config.search.ignore_case))
119 };
120
121 Ok(Self {
122 pattern: pattern.to_string(),
123 matcher,
124 config,
125 })
126 }
127}
128
129pub async fn search_file(
131 path: &Path,
132 options: &SearchOptions,
133) -> Result<Option<SearchResult>> {
134 use tokio::fs;
135 use tokio::io::AsyncReadExt;
136
137 let metadata = fs::metadata(path).await?;
139
140 if let Some(max_size) = options.config.search.max_file_size {
142 if metadata.len() > max_size {
143 debug!("Skipping large file: {}", path.display());
144 return Ok(None);
145 }
146 }
147
148 let mut file = fs::File::open(path).await?;
149 let mut contents = String::new();
150 file.read_to_string(&mut contents).await?;
151
152 let language = crate::parsers::detect_language(path);
154
155 let matches = find_matches(&contents, options, path, language.as_deref()).await?;
157
158 if matches.is_empty() {
159 return Ok(None);
160 }
161
162 let result = SearchResult {
164 path: path.to_path_buf(),
165 matches,
166 metadata: FileMetadata {
167 size: metadata.len(),
168 language,
169 encoding: "UTF-8".to_string(),
170 modified: metadata.modified()?,
171 },
172 #[cfg(feature = "ai")]
173 ai_insights: None,
174 };
175
176 Ok(Some(result))
177}
178
179async fn find_matches(
181 content: &str,
182 options: &SearchOptions,
183 path: &Path,
184 language: Option<&str>,
185) -> Result<Vec<Match>> {
186 let lines: Vec<&str> = content.lines().collect();
187 let mut matches = Vec::new();
188
189 let parsed_ast = if options.config.output.show_ast_context && language.is_some() {
191 match crate::parsers::parse_ast(content, language.unwrap()) {
192 Ok(ast) => Some(ast),
193 Err(e) => {
194 debug!("Failed to parse AST for {}: {}", path.display(), e);
195 None
196 }
197 }
198 } else {
199 None
200 };
201
202 for (line_idx, line) in lines.iter().enumerate() {
204 if let Some(column_range) = options.matcher.find_match(line) {
205 let line_number = line_idx + 1;
206
207 let before_start = line_idx.saturating_sub(options.config.output.before_context);
209 let after_end = (line_idx + options.config.output.after_context + 1).min(lines.len());
210
211 let before_context = lines[before_start..line_idx]
212 .iter()
213 .map(|s| s.to_string())
214 .collect();
215
216 let after_context = lines[(line_idx + 1)..after_end]
217 .iter()
218 .map(|s| s.to_string())
219 .collect();
220
221 let ast_context = parsed_ast
223 .as_ref()
224 .map(|ast| ast.get_context_for_line(line_idx));
225
226 matches.push(Match {
227 line_number,
228 column_range,
229 line_text: line.to_string(),
230 before_context,
231 after_context,
232 ast_context: ast_context.flatten(),
233 score: 1.0,
234 });
235 }
236 }
237
238 Ok(matches)
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 #[test]
246 fn test_match_creation() {
247 let m = Match {
248 line_number: 10,
249 column_range: 5..10,
250 line_text: "test line".to_string(),
251 before_context: vec![],
252 after_context: vec![],
253 ast_context: None,
254 score: 1.0,
255 };
256
257 assert_eq!(m.line_number, 10);
258 assert_eq!(m.column_range, 5..10);
259 }
260}