use nucleo_matcher::pattern::{CaseMatching, Normalization, Pattern};
use nucleo_matcher::{Config, Matcher, Utf32Str};
pub fn filter_indices(haystacks: &[String], query: &str) -> Vec<usize> {
if query.trim().is_empty() {
return (0..haystacks.len()).collect();
}
let mut matcher = Matcher::new(Config::DEFAULT);
let pattern = Pattern::parse(query, CaseMatching::Smart, Normalization::Smart);
let mut scored: Vec<(usize, u32)> = Vec::new();
let mut buf = Vec::new();
for (index, haystack) in haystacks.iter().enumerate() {
let utf32 = Utf32Str::new(haystack, &mut buf);
if let Some(score) = pattern.score(utf32, &mut matcher) {
scored.push((index, score));
}
}
scored.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
scored.into_iter().map(|(index, _)| index).collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn haystacks() -> Vec<String> {
vec![
"feature/login".to_string(),
"feature/logout".to_string(),
"main".to_string(),
"hotfix/crash".to_string(),
]
}
#[test]
fn empty_query_returns_all_in_order() {
assert_eq!(filter_indices(&haystacks(), ""), vec![0, 1, 2, 3]);
assert_eq!(filter_indices(&haystacks(), " "), vec![0, 1, 2, 3]);
}
#[test]
fn matches_subsequence() {
let result = filter_indices(&haystacks(), "flog");
assert!(result.contains(&0));
assert!(result.contains(&1));
assert!(!result.contains(&2));
}
#[test]
fn exact_substring_matches() {
let result = filter_indices(&haystacks(), "main");
assert_eq!(result, vec![2]);
}
#[test]
fn no_match_is_empty() {
assert!(filter_indices(&haystacks(), "zzzzz").is_empty());
}
#[test]
fn ranks_better_matches_first() {
let result = filter_indices(&haystacks(), "hotfix");
assert_eq!(result.first(), Some(&3));
}
}