use std::{cmp::Reverse, path::PathBuf};
use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher};
#[derive(Debug, Clone)]
pub struct SearchMatch {
pub line_number: usize,
pub line_content: String,
}
#[derive(Debug, Clone)]
pub struct SearchResult {
pub path: PathBuf,
pub file_name: String,
pub matches: Vec<SearchMatch>,
}
pub fn search_workspace(root: &PathBuf, query: &str) -> Vec<SearchResult> {
use std::fs;
use ignore::WalkBuilder;
let query_lower = query.to_lowercase();
let mut results = Vec::new();
let walker = WalkBuilder::new(root).hidden(true).git_ignore(true).git_global(true).build();
for entry in walker.flatten() {
let path = entry.path();
if !path.is_file() {
continue;
}
let Ok(content) = fs::read_to_string(path) else {
continue;
};
let mut matches = Vec::new();
for (line_idx, line) in content.lines().enumerate() {
if line.to_lowercase().contains(&query_lower) {
matches.push(SearchMatch {
line_number: line_idx + 1,
line_content: line.to_string(),
});
}
}
if !matches.is_empty() {
results.push(SearchResult {
path: path.to_path_buf(),
file_name: path.file_name().unwrap_or_default().to_string_lossy().to_string(),
matches,
});
}
}
results
}
pub fn collect_all_files(root: &PathBuf) -> Vec<(String, PathBuf)> {
use ignore::WalkBuilder;
let mut files = Vec::new();
let walker = WalkBuilder::new(root).hidden(true).git_ignore(true).git_global(true).build();
for entry in walker.flatten() {
let path = entry.path();
if !path.is_file() {
continue;
}
let display = path.strip_prefix(root).unwrap_or(path).to_string_lossy().to_string();
files.push((display, path.to_path_buf()));
}
files
}
pub fn fuzzy_find_files(
query: &str,
files: &[(String, PathBuf)],
max_results: usize,
) -> Vec<(i64, String, PathBuf)> {
let matcher = SkimMatcherV2::default();
let mut scored: Vec<(i64, String, PathBuf)> = files
.iter()
.filter_map(|(display, abs_path)| {
matcher
.fuzzy_match(display, query)
.map(|score| (score, display.clone(), abs_path.clone()))
})
.collect();
scored.sort_by_key(|b| Reverse(b.0));
scored.truncate(max_results);
scored
}