1use nucleo_matcher::pattern::{CaseMatching, Normalization, Pattern};
5use nucleo_matcher::{Config, Matcher, Utf32Str};
6
7pub fn filter_indices(haystacks: &[String], query: &str) -> Vec<usize> {
11 if query.trim().is_empty() {
12 return (0..haystacks.len()).collect();
13 }
14 let mut matcher = Matcher::new(Config::DEFAULT);
15 let pattern = Pattern::parse(query, CaseMatching::Smart, Normalization::Smart);
16 let mut scored: Vec<(usize, u32)> = Vec::new();
17 let mut buf = Vec::new();
18 for (index, haystack) in haystacks.iter().enumerate() {
19 let utf32 = Utf32Str::new(haystack, &mut buf);
20 if let Some(score) = pattern.score(utf32, &mut matcher) {
21 scored.push((index, score));
22 }
23 }
24 scored.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
25 scored.into_iter().map(|(index, _)| index).collect()
26}
27
28#[cfg(test)]
29mod tests {
30 use super::*;
31
32 fn haystacks() -> Vec<String> {
33 vec![
34 "feature/login".to_string(),
35 "feature/logout".to_string(),
36 "main".to_string(),
37 "hotfix/crash".to_string(),
38 ]
39 }
40
41 #[test]
42 fn empty_query_returns_all_in_order() {
43 assert_eq!(filter_indices(&haystacks(), ""), vec![0, 1, 2, 3]);
44 assert_eq!(filter_indices(&haystacks(), " "), vec![0, 1, 2, 3]);
45 }
46
47 #[test]
48 fn matches_subsequence() {
49 let result = filter_indices(&haystacks(), "flog");
51 assert!(result.contains(&0));
52 assert!(result.contains(&1));
53 assert!(!result.contains(&2));
54 }
55
56 #[test]
57 fn exact_substring_matches() {
58 let result = filter_indices(&haystacks(), "main");
59 assert_eq!(result, vec![2]);
60 }
61
62 #[test]
63 fn no_match_is_empty() {
64 assert!(filter_indices(&haystacks(), "zzzzz").is_empty());
65 }
66
67 #[test]
68 fn ranks_better_matches_first() {
69 let result = filter_indices(&haystacks(), "hotfix");
71 assert_eq!(result.first(), Some(&3));
72 }
73}