use nucleo::pattern::{CaseMatching, Normalization, Pattern};
use nucleo::{Config, Matcher, Utf32Str};
pub fn rank(query: &str, haystacks: &[String]) -> Vec<usize> {
let q = query.trim();
if q.is_empty() {
return (0..haystacks.len()).collect();
}
let mut matcher = Matcher::new(Config::DEFAULT);
let pattern = Pattern::parse(q, CaseMatching::Smart, Normalization::Smart);
let mut buf: Vec<char> = Vec::new();
let mut scored: Vec<(usize, u32)> = Vec::new();
for (i, h) in haystacks.iter().enumerate() {
let hs = Utf32Str::new(h, &mut buf);
if let Some(score) = pattern.score(hs, &mut matcher) {
scored.push((i, score));
}
}
scored.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
scored.into_iter().map(|(i, _)| i).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_query_preserves_order() {
let h = vec!["b".to_string(), "a".to_string(), "c".to_string()];
assert_eq!(rank("", &h), vec![0, 1, 2]);
assert_eq!(rank(" ", &h), vec![0, 1, 2]);
}
#[test]
fn matches_are_ordered_and_non_matches_dropped() {
let h = vec![
"redis mcp server".to_string(),
"postgres skill".to_string(),
"redis cache helper".to_string(),
];
let r = rank("redis", &h);
assert!(r.contains(&0));
assert!(r.contains(&2));
assert!(!r.contains(&1));
}
#[test]
fn closer_match_ranks_first() {
let h = vec![
"a-redis-thing-with-extra-words".to_string(),
"redis".to_string(),
];
let r = rank("redis", &h);
assert_eq!(r.first().copied(), Some(1));
}
#[test]
fn no_match_returns_empty() {
let h = vec!["alpha".to_string(), "beta".to_string()];
assert!(rank("zzzzz", &h).is_empty());
}
}