use crate::{FileSystemStore, SearchMatch, SearchResult};
use anyhow::{Context, Result};
use async_trait::async_trait;
use grep::regex::RegexMatcher;
use grep::searcher::{sinks::UTF8, Searcher};
use std::sync::Arc;
#[derive(Debug)]
pub struct FileSystemGrepSearcher {
file_store: Arc<FileSystemStore>,
}
impl FileSystemGrepSearcher {
pub fn new(file_store: Arc<FileSystemStore>) -> Self {
Self { file_store }
}
async fn search_with_grep(
&self,
base_path: &str,
content_pattern: &str,
file_pattern: Option<&str>,
) -> Result<Vec<SearchMatch>> {
let mut matches = Vec::new();
let matcher = RegexMatcher::new(content_pattern)
.with_context(|| format!("Invalid regex pattern: {}", content_pattern))?;
self.search_directory(
base_path,
&matcher,
content_pattern,
file_pattern,
&mut matches,
)
.await?;
Ok(matches)
}
fn search_directory<'a>(
&'a self,
dir_path: &'a str,
matcher: &'a RegexMatcher,
pattern: &'a str,
file_pattern: Option<&'a str>,
matches: &'a mut Vec<SearchMatch>,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send + 'a>> {
Box::pin(async move {
let listing = self.file_store.list(dir_path).await?;
for entry in &listing.entries {
let entry_path = if dir_path.is_empty() {
entry.name.clone()
} else {
format!("{}/{}", dir_path, entry.name)
};
if entry.is_dir {
self.search_directory(&entry_path, matcher, pattern, file_pattern, matches)
.await?;
} else {
if let Some(fp) = file_pattern {
if let Some(extension) = fp.strip_prefix("*.") {
if !entry.name.ends_with(&format!(".{}", extension)) {
continue;
}
} else if !entry.name.contains(fp) {
continue;
}
}
if let Ok(read_result) = self
.file_store
.read(&entry_path, crate::ReadParams::default())
.await
{
let mut searcher = Searcher::new();
let content_reader = std::io::Cursor::new(read_result.content.as_bytes());
let path_clone = entry_path.clone();
let _result = searcher.search_reader(
matcher,
content_reader,
UTF8(|line_num, line_content| {
matches.push(SearchMatch {
file_path: path_clone.clone(),
line_number: Some(line_num),
line_content: line_content.trim_end().to_string(),
match_text: pattern.to_string(),
});
Ok(true)
}),
);
}
}
}
Ok(())
})
}
}
#[async_trait]
impl crate::GrepSearcher for FileSystemGrepSearcher {
async fn search(
&self,
path: &str,
content_pattern: Option<&str>,
file_pattern: Option<&str>,
) -> Result<SearchResult> {
let matches = if let Some(pattern) = content_pattern {
self.search_with_grep(path, pattern, file_pattern).await?
} else {
let listing = self.file_store.list(path).await?;
let mut matches = Vec::new();
for entry in &listing.entries {
if let Some(pattern) = file_pattern {
if entry.name.contains(pattern) {
let file_path = if path.is_empty() {
entry.name.clone()
} else {
format!("{}/{}", path, entry.name)
};
matches.push(SearchMatch {
file_path,
line_number: None,
line_content: String::new(),
match_text: pattern.to_string(),
});
}
}
}
matches
};
Ok(SearchResult {
path: path.to_string(),
matches,
})
}
}