use anyhow::Result;
use crate::storage::{SegmentReader, decode_line_table, byte_to_line};
use crate::types::*;
use globset::Glob;
use crate::index::{PathIndex, HandlesMap};
use regex::Regex;
impl Snapshot {
pub fn find(&mut self, query: &str, path_glob: Option<&str>, limit: Option<usize>) -> Result<Vec<Hit>> {
let path_index = &self.path_index;
let handles_map = &self.handles_map;
let limit = limit.unwrap_or(1000);
let mut hits = Vec::new();
let path_filter = if let Some(glob) = path_glob {
Some(globset::Glob::new(glob)?.compile_matcher())
} else {
None
};
let candidate_files = if self.inverted_index.term_count() > 0 {
self.inverted_index.find_files_with_term(query)
} else {
let mut all_files = std::collections::HashSet::new();
for &handle in self.path_index.paths.values() {
all_files.insert(handle as u32);
}
all_files
};
for handle in candidate_files {
if hits.len() >= limit {
break;
}
let path = if let Some((path, _)) = self.path_index.paths.iter()
.find(|(_, &h)| h == handle as u64) {
path
} else {
continue;
};
if let Some(ref filter) = path_filter {
if !filter.is_match(path) {
continue;
}
}
if let Some(metadata) = self.handles_map.get_metadata(handle as u64) {
let store_path = self.collection_path.join("store");
let reader = if let Some(reader) = self.segment_cache.get_mut(&metadata.seg_id) {
reader
} else {
let new_reader = SegmentReader::new(&store_path, metadata.seg_id)?;
self.segment_cache.insert(metadata.seg_id, new_reader);
self.segment_cache.get_mut(&metadata.seg_id).unwrap()
};
let frame = reader.read_frame(metadata)?;
if let Ok(content_str) = String::from_utf8(frame.content.clone()) {
let newline_positions = decode_line_table(&frame.line_table)?;
for (byte_offset, line_content) in find_matches_in_content(&content_str, query) {
let line_num = byte_to_line(byte_offset, &newline_positions);
hits.push(Hit {
path: path.clone(),
line: line_num,
text: line_content,
});
if hits.len() >= limit {
return Ok(hits);
}
}
}
}
}
Ok(hits)
}
pub fn regex_find(&self, pattern: &str, path_glob: Option<&str>, limit: Option<usize>) -> Result<Vec<Hit>> {
let path_index = PathIndex::read_from_file(&self.collection_path.join("index/path.json"))?;
let handles_map = HandlesMap::read_from_file(&self.collection_path.join("index/handles.json"))?;
let trigram_candidates: Option<Vec<u32>> = None;
let regex = Regex::new(pattern)?;
let mut hits = Vec::new();
let limit = limit.unwrap_or(1000);
let file_handles: Vec<u32> = if let Some(candidates) = trigram_candidates {
candidates.into_iter().collect()
} else {
path_index.paths.values().map(|&h| h as u32).collect()
};
let path_filter = path_glob.map(|pattern| {
Glob::new(pattern).unwrap().compile_matcher()
});
for file_handle in file_handles {
if hits.len() >= limit {
break;
}
if let Some(metadata) = handles_map.handles.get(&(file_handle as u64)) {
if let Some(ref filter) = path_filter {
if let Some(path_entry) = path_index.paths.iter().find(|(_, &h)| h as u32 == file_handle) {
if !filter.is_match(path_entry.0) {
continue;
}
}
}
let mut reader = SegmentReader::new(&self.collection_path.join("segments"), metadata.seg_id)?;
if let Ok(frame) = reader.read_frame(metadata) {
let content_str = String::from_utf8_lossy(&frame.content);
for (line_idx, line) in content_str.lines().enumerate() {
if regex.is_match(line) {
let file_path = path_index.paths.iter()
.find(|(_, &h)| h as u32 == file_handle)
.map(|(path, _)| path.to_string())
.unwrap_or_else(|| "unknown".to_string());
hits.push(Hit {
path: file_path,
line: (line_idx + 1) as u32,
text: line.to_string(),
});
if hits.len() >= limit {
break;
}
}
}
}
}
}
Ok(hits)
}
pub fn grep(&mut self, pattern: &str, path_glob: Option<&str>, limit: Option<usize>) -> Result<Vec<Hit>> {
self.find(pattern, path_glob, limit)
}
pub fn open_span(&self, path: &str, start_line: u32, end_line: u32) -> Result<TextSpan> {
let path_index = PathIndex::read_from_file(&self.collection_path.join("index/path.json"))?;
let handles_map = HandlesMap::read_from_file(&self.collection_path.join("index/handles.json"))?;
let handle = path_index.get_handle(path)
.ok_or_else(|| anyhow::anyhow!("Path not found: {}", path))?;
let metadata = handles_map.get_metadata(handle)
.ok_or_else(|| anyhow::anyhow!("Handle metadata not found: {}", handle))?;
let store_path = self.collection_path.join("store");
let mut reader = SegmentReader::new(&store_path, metadata.seg_id)?;
let frame = reader.read_frame(metadata)?;
let content_str = String::from_utf8(frame.content)
.map_err(|_| anyhow::anyhow!("File contains non-UTF8 content"))?;
let newline_positions = decode_line_table(&frame.line_table)?;
let lines = extract_line_range(&content_str, &newline_positions, start_line, end_line)?;
Ok(TextSpan {
path: path.to_string(),
content: lines,
start_line,
end_line,
})
}
}
fn find_matches_in_content(content: &str, query: &str) -> Vec<(usize, String)> {
let mut matches = Vec::new();
let lines: Vec<&str> = content.lines().collect();
let mut byte_offset = 0;
for line in lines.iter() {
if line.contains(query) {
matches.push((byte_offset, line.to_string()));
}
byte_offset += line.len() + 1; }
matches
}
fn extract_line_range(
content: &str,
_newline_positions: &[u32],
start_line: u32,
end_line: u32
) -> Result<String> {
let lines: Vec<&str> = content.lines().collect();
if start_line == 0 || end_line == 0 {
anyhow::bail!("Line numbers must be 1-based");
}
let start_idx = (start_line - 1) as usize;
let end_idx = std::cmp::min(end_line as usize, lines.len());
if start_idx >= lines.len() {
anyhow::bail!("Start line {} exceeds file length {}", start_line, lines.len());
}
let selected_lines = &lines[start_idx..end_idx];
Ok(selected_lines.join("\n"))
}
#[allow(dead_code)]
fn glob_match(pattern: &str, text: &str) -> bool {
if pattern == "**/*" || pattern == "*" {
return true;
}
if pattern.starts_with("**/*.") {
let ext = &pattern[5..];
return text.ends_with(ext);
}
if pattern.starts_with("**/") {
let suffix = &pattern[3..];
return text.contains(suffix);
}
if pattern.ends_with("/**") {
let prefix = &pattern[..pattern.len() - 3];
return text.starts_with(prefix);
}
pattern == text
}