use crate::types::{Match, SearchResult};
use fuzzy_matcher::FuzzyMatcher;
use fuzzy_matcher::skim::SkimMatcherV2;
use std::fs;
use std::io::{BufRead, BufReader};
use std::path::Path;
pub fn search_in_file_parallel(
file_path: &Path,
matcher: &super::SearchMatcher,
fuzzy: bool,
fuzzy_threshold: f64,
query: &str,
max_results: usize,
rank: bool,
bm25_stats: Option<&super::bm25::Bm25Stats>,
context_filter: Option<&crate::types::ContextFilter>,
declaration_filter: Option<&crate::types::DeclarationFilter>,
) -> Result<Vec<SearchResult>, Box<dyn std::error::Error>> {
let file = fs::File::open(file_path)?;
let reader = BufReader::new(file);
let mut results = Vec::new();
let mut line_count = 0;
let mut term_freq = 0usize;
let fuzzy_matcher = SkimMatcherV2::default();
for line in reader.lines() {
line_count += 1;
let line = line?;
if results.len() >= max_results {
break;
}
if let Some(filter) = context_filter {
let ctx = super::context::classify_line(&line);
if !super::context::should_include(ctx, Some(filter)) {
continue;
}
}
if fuzzy {
if let Some((score, indices)) = fuzzy_matcher.fuzzy_indices(&line, query) {
if score as f64 >= fuzzy_threshold {
term_freq += 1;
let line_chars: Vec<char> = line.chars().collect();
let mut matches = Vec::new();
for &idx in &indices {
if matches.is_empty()
|| idx >= matches.last().map(|m: &Match| m.end).unwrap_or(0)
{
let text = if idx < line_chars.len() {
line_chars[idx].to_string()
} else {
String::new()
};
matches.push(Match {
start: idx,
end: idx + 1,
text,
});
}
}
let (score_val, relevance) = if rank {
let s = calculate_relevance_score(
&line,
query,
line_count,
file_path,
true,
Some(score),
);
let r = relevance_label(s);
(s, r)
} else {
(score as f64, "Medium".to_string())
};
if let Some(filter) = declaration_filter {
if !super::declarations::should_include_declaration(&line, Some(filter)) {
continue;
}
}
results.push(SearchResult {
file: file_path.to_string_lossy().into_owned(),
line_number: line_count,
content: line.clone(),
matches,
score: score_val,
relevance,
});
}
}
} else if let Some(mat) = matcher.find(&line) {
term_freq += 1;
let (score_val, relevance) = if rank {
let s = calculate_relevance_score(&line, query, line_count, file_path, false, None);
let r = relevance_label(s);
(s, r)
} else {
(50.0, "Medium".to_string())
};
if let Some(filter) = declaration_filter {
if !super::declarations::should_include_declaration(&line, Some(filter)) {
continue;
}
}
let matches = vec![Match {
start: mat.start,
end: mat.end,
text: line[mat.clone()].to_string(),
}];
results.push(SearchResult {
file: file_path.to_string_lossy().into_owned(),
line_number: line_count,
content: line.clone(),
matches,
score: score_val,
relevance,
});
}
}
if let Some(stats) = bm25_stats {
if !results.is_empty() {
let bm25_score = stats.normalized_score(term_freq, line_count);
let relevance = relevance_label(bm25_score);
for r in &mut results {
r.score = bm25_score;
r.relevance = relevance.clone();
}
}
}
Ok(results)
}
fn relevance_label(score: f64) -> String {
if score >= 80.0 {
"Very High".to_string()
} else if score >= 60.0 {
"High".to_string()
} else if score >= 40.0 {
"Medium".to_string()
} else {
"Low".to_string()
}
}
pub fn calculate_relevance_score(
line: &str,
query: &str,
line_number: usize,
file_path: &Path,
_is_fuzzy: bool,
fuzzy_score: Option<i64>,
) -> f64 {
let mut score = 50.0;
if line.contains(query) {
score += 30.0;
}
if let Some(fs) = fuzzy_score {
score += (fs as f64) / 10.0;
}
if line_number < 100 {
score += 5.0;
}
if let Some(ext) = file_path.extension().and_then(|e| e.to_str()) {
match ext {
"rs" | "py" | "js" | "ts" => score += 10.0,
"md" | "txt" => score += 5.0,
_ => {}
}
}
let path_str = file_path.to_string_lossy();
let is_test_file = path_str.contains("/test")
|| path_str.contains("/spec/")
|| path_str.contains("/mock")
|| path_str.contains("/fixture")
|| path_str.contains("_test.")
|| path_str.contains("_spec.")
|| path_str.contains("test_");
if is_test_file {
score *= 0.7;
}
if line.len() > 500 {
score *= 0.5;
}
let definition_patterns = [
"fn ",
"def ",
"function ",
"class ",
"struct ",
"impl ",
"trait ",
];
for pattern in &definition_patterns {
if line.contains(pattern) {
score += 15.0;
break;
}
}
score.clamp(0.0, 100.0)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::tempdir;
#[test]
fn test_fuzzy_search_performance() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("test.rs");
let content = "fn test_function() {\n let test_var = 123;\n println!(\"test\");\n}\n";
fs::write(&file_path, content).unwrap();
let matcher = crate::search::SearchMatcher::Regex(
std::sync::Arc::new(regex::Regex::new(r"test").unwrap()),
);
let result = search_in_file_parallel(&file_path, &matcher, true, 0.0, "test", 100, false, None, None, None);
assert!(result.is_ok());
let results = result.unwrap();
assert!(!results.is_empty());
}
#[test]
fn test_fuzzy_indices_optimization() {
let line = "fn test_function() {";
let query = "test";
let matcher = SkimMatcherV2::default();
if let Some((_score, indices)) = matcher.fuzzy_indices(line, query) {
let line_chars: Vec<char> = line.chars().collect();
for &idx in &indices {
assert!(
idx < line_chars.len(),
"Index {} should be less than line length {}",
idx,
line_chars.len()
);
let _c = line_chars[idx];
}
}
}
#[test]
fn test_test_file_dampening() {
let src_score = calculate_relevance_score(
"fn helper() {}",
"helper",
1,
std::path::Path::new("src/main.rs"),
false,
None,
);
let test_score = calculate_relevance_score(
"fn helper() {}",
"helper",
1,
std::path::Path::new("tests/test_main.rs"),
false,
None,
);
assert!(
test_score < src_score,
"Test file score ({}) should be lower than src file score ({})",
test_score,
src_score
);
}
#[test]
fn test_noise_penalty() {
let normal_line = "fn helper() { println!(\"hello\"); }";
let minified_line = "a".repeat(600);
let normal_score = calculate_relevance_score(
normal_line,
"helper",
1,
std::path::Path::new("src/main.rs"),
false,
None,
);
let noisy_score = calculate_relevance_score(
&minified_line,
"a",
1,
std::path::Path::new("dist/bundle.js"),
false,
None,
);
assert!(
noisy_score < normal_score,
"Noisy line score ({}) should be lower than normal line score ({})",
noisy_score,
normal_score
);
}
}