pub fn fuzzy_match(query: &str, text: &str) -> Option<f64> {
if query.is_empty() {
return Some(0.0);
}
let norm = |s: &str| -> String {
s.chars()
.map(|c| match c {
'_' | '-' | '.' | '/' | '\\' => ' ',
c => c.to_ascii_lowercase(),
})
.collect()
};
let nq = norm(query);
let nt = norm(text);
let tokens: Vec<&str> = nq.split_whitespace().collect();
if tokens.is_empty() {
return Some(0.0);
}
let text_chars: Vec<char> = nt.chars().collect();
let text_len = text_chars.len();
let mut total_score = 0.0_f64;
for token in &tokens {
let token_chars: Vec<char> = token.chars().collect();
let tlen = token_chars.len();
let mut best: Option<(usize, usize)> = None;
'outer: for start in 0..text_len {
let mut ti = 0;
let mut pos = start;
while ti < tlen && pos < text_len {
if text_chars[pos] == token_chars[ti] {
ti += 1;
}
pos += 1;
}
if ti == tlen {
best = Some((start, pos));
break 'outer;
}
}
let (start, end) = best?;
let span = (end - start) as f64;
let tightness = tlen as f64 / span.max(1.0);
let position_bonus = 1.0 / (1.0 + start as f64 * 0.05);
total_score += tightness * position_bonus;
}
Some(total_score / tokens.len() as f64)
}
use std::path::PathBuf;
pub fn filter_and_rank<'a>(items: &'a [PathBuf], query: &str) -> Vec<&'a PathBuf> {
if query.is_empty() {
return items.iter().collect();
}
let mut matched: Vec<(&PathBuf, f64)> = items
.iter()
.filter_map(|item| {
let item_str = item.to_str().unwrap_or_default();
let depth = item_str.bytes().filter(|&b| b == b'/').count();
let filename = item
.file_name()
.and_then(|s| s.to_str())
.unwrap_or_default();
let score = if let Some(score) = fuzzy_match(query, filename) {
score + 1.0 + 1.0 / (depth as f64 + 1.0)
} else {
let score = fuzzy_match(query, item_str)?;
score + 1.0 / (depth as f64 + 1.0)
};
Some((item, score))
})
.collect();
matched.sort_unstable_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
matched.into_iter().map(|(p, _)| p).collect()
}
pub fn filter_and_rank_by<'a, T, F>(items: &'a [T], query: &str, text_of: F) -> Vec<&'a T>
where
F: Fn(&T) -> String,
{
if query.is_empty() {
return items.iter().collect();
}
let mut matched: Vec<(&T, f64)> = items
.iter()
.filter_map(|item| {
let text = text_of(item);
let score = fuzzy_match(query, &text)?;
Some((item, score))
})
.collect();
matched.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
matched.into_iter().map(|(item, _)| item).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_query_always_matches() {
assert!(fuzzy_match("", "anything").is_some());
assert!(fuzzy_match("", "").is_some());
}
#[test]
fn exact_match_scores_highest() {
let exact = fuzzy_match("main", "main").unwrap();
let partial = fuzzy_match("main", "main.rs").unwrap();
assert!(exact >= partial);
}
#[test]
fn subsequence_match() {
assert!(fuzzy_match("algo", "multi_view_algorithm.cpp").is_some());
assert!(fuzzy_match("mvc", "multi_view_algorithm.cpp").is_some());
}
#[test]
fn non_match_returns_none() {
assert!(fuzzy_match("xyz", "multi_view_algorithm.cpp").is_none());
assert!(fuzzy_match("zzz", "abc").is_none());
}
#[test]
fn separator_treated_as_space() {
assert!(fuzzy_match("mul view", "multi_view_algorithm.cpp").is_some());
assert!(fuzzy_match("src main", "src/main.rs").is_some());
}
#[test]
fn case_insensitive() {
assert!(fuzzy_match("MAIN", "main.rs").is_some());
assert!(fuzzy_match("Main", "MAIN.RS").is_some());
}
#[test]
fn multi_token_all_must_match() {
assert!(fuzzy_match("app rs", "src/app.rs").is_some());
assert!(fuzzy_match("app xyz", "src/app.rs").is_none());
}
#[test]
fn consecutive_run_scores_higher_than_scattered() {
let tight = fuzzy_match("abc", "abcdef").unwrap();
let loose = fuzzy_match("abc", "axbxcdef").unwrap();
assert!(tight > loose);
}
#[test]
fn earlier_match_scores_higher() {
let early = fuzzy_match("rs", "rs_parser.rs").unwrap();
let late = fuzzy_match("rs", "some_file.rs").unwrap();
assert!(early > late);
}
#[test]
fn empty_query_returns_all_in_order() {
let items = vec![
PathBuf::from("src/main.rs"),
PathBuf::from("src/app.rs"),
PathBuf::from("README.md"),
];
let result = filter_and_rank(&items, "");
assert_eq!(result.len(), 3);
assert_eq!(result[0], &items[0]);
assert_eq!(result[1], &items[1]);
assert_eq!(result[2], &items[2]);
}
#[test]
fn filters_out_non_matching_items() {
let items = vec![
PathBuf::from("src/main.rs"),
PathBuf::from("src/app.rs"),
PathBuf::from("README.md"),
];
let result = filter_and_rank(&items, "main");
assert_eq!(result.len(), 1);
assert_eq!(result[0].to_str().unwrap(), "src/main.rs");
}
#[test]
fn filename_match_ranks_above_path_match() {
let items = vec![
PathBuf::from("src/domain.rs"), PathBuf::from("src/main.rs"), ];
let result = filter_and_rank(&items, "main");
assert_eq!(result[0].to_str().unwrap(), "src/main.rs");
}
#[test]
fn results_sorted_best_first() {
let items = vec![
PathBuf::from("very/deep/path/to/app.rs"),
PathBuf::from("app.rs"),
];
let result = filter_and_rank(&items, "app");
assert_eq!(result[0].to_str().unwrap(), "app.rs");
}
#[test]
fn empty_items_returns_empty() {
let items: Vec<PathBuf> = vec![];
assert!(filter_and_rank(&items, "anything").is_empty());
}
}