use anyhow::Result;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tracing::debug;
pub mod engine;
pub mod matcher;
pub mod walker;
pub use engine::SearchEngine;
pub use matcher::{Matcher, MatchType};
#[derive(Debug, Clone, serde::Serialize)]
pub struct Match {
pub line_number: usize,
#[serde(skip_serializing)]
pub column_range: std::ops::Range<usize>,
pub line_text: String,
pub before_context: Vec<String>,
pub after_context: Vec<String>,
pub ast_context: Option<crate::ast::AstContext>,
pub score: f64,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct SearchResult {
pub path: PathBuf,
pub matches: Vec<Match>,
pub metadata: FileMetadata,
#[cfg(feature = "ai")]
pub ai_insights: Option<AiInsights>,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct FileMetadata {
pub size: u64,
pub language: Option<String>,
pub encoding: String,
#[serde(skip_serializing)]
pub modified: std::time::SystemTime,
}
#[cfg(feature = "ai")]
#[derive(Debug, Clone, serde::Serialize)]
pub struct AiInsights {
pub summary: String,
pub explanation: Option<String>,
pub suggestions: Vec<String>,
pub related_locations: Vec<PathBuf>,
}
#[derive(Debug, Default)]
pub struct SearchStats {
pub files_searched: usize,
pub files_matched: usize,
pub total_matches: usize,
pub files_skipped: usize,
pub duration: std::time::Duration,
}
pub struct SearchOptions {
pub pattern: String,
pub matcher: Arc<dyn Matcher>,
pub config: Arc<crate::Config>,
}
impl Clone for SearchOptions {
fn clone(&self) -> Self {
Self {
pattern: self.pattern.clone(),
matcher: Arc::clone(&self.matcher),
config: Arc::clone(&self.config),
}
}
}
impl SearchOptions {
pub fn new(pattern: &str, config: Arc<crate::Config>) -> Result<Self> {
let matcher: Arc<dyn Matcher> = if config.search.regex {
Arc::new(matcher::RegexMatcher::new(pattern, config.search.ignore_case)?)
} else {
Arc::new(matcher::LiteralMatcher::new(pattern, config.search.ignore_case))
};
Ok(Self {
pattern: pattern.to_string(),
matcher,
config,
})
}
}
pub async fn search_file(
path: &Path,
options: &SearchOptions,
) -> Result<Option<SearchResult>> {
use tokio::fs;
use tokio::io::AsyncReadExt;
let metadata = fs::metadata(path).await?;
if let Some(max_size) = options.config.search.max_file_size {
if metadata.len() > max_size {
debug!("Skipping large file: {}", path.display());
return Ok(None);
}
}
let mut file = fs::File::open(path).await?;
let mut contents = String::new();
file.read_to_string(&mut contents).await?;
let language = crate::parsers::detect_language(path);
let matches = find_matches(&contents, options, path, language.as_deref()).await?;
if matches.is_empty() {
return Ok(None);
}
let result = SearchResult {
path: path.to_path_buf(),
matches,
metadata: FileMetadata {
size: metadata.len(),
language,
encoding: "UTF-8".to_string(),
modified: metadata.modified()?,
},
#[cfg(feature = "ai")]
ai_insights: None,
};
Ok(Some(result))
}
async fn find_matches(
content: &str,
options: &SearchOptions,
path: &Path,
language: Option<&str>,
) -> Result<Vec<Match>> {
let lines: Vec<&str> = content.lines().collect();
let mut matches = Vec::new();
let parsed_ast = if options.config.output.show_ast_context && language.is_some() {
match crate::parsers::parse_ast(content, language.unwrap()) {
Ok(ast) => Some(ast),
Err(e) => {
debug!("Failed to parse AST for {}: {}", path.display(), e);
None
}
}
} else {
None
};
for (line_idx, line) in lines.iter().enumerate() {
if let Some(column_range) = options.matcher.find_match(line) {
let line_number = line_idx + 1;
let before_start = line_idx.saturating_sub(options.config.output.before_context);
let after_end = (line_idx + options.config.output.after_context + 1).min(lines.len());
let before_context = lines[before_start..line_idx]
.iter()
.map(|s| s.to_string())
.collect();
let after_context = lines[(line_idx + 1)..after_end]
.iter()
.map(|s| s.to_string())
.collect();
let ast_context = parsed_ast
.as_ref()
.map(|ast| ast.get_context_for_line(line_idx));
matches.push(Match {
line_number,
column_range,
line_text: line.to_string(),
before_context,
after_context,
ast_context: ast_context.flatten(),
score: 1.0,
});
}
}
Ok(matches)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_match_creation() {
let m = Match {
line_number: 10,
column_range: 5..10,
line_text: "test line".to_string(),
before_context: vec![],
after_context: vec![],
ast_context: None,
score: 1.0,
};
assert_eq!(m.line_number, 10);
assert_eq!(m.column_range, 5..10);
}
}