1use nucleo_matcher::pattern::{CaseMatching, Normalization, Pattern};
2use nucleo_matcher::{Config, Matcher, Utf32Str};
3
4pub fn normalize_query(query: &str) -> String {
7 let trimmed = query.trim();
8 if trimmed.is_empty() {
9 return String::new();
10 }
11
12 let mut normalized = String::with_capacity(trimmed.len());
13 let mut last_was_space = false;
14 for ch in trimmed.chars() {
15 if ch.is_whitespace() {
16 if !last_was_space && !normalized.is_empty() {
17 normalized.push(' ');
18 }
19 last_was_space = true;
20 } else {
21 normalized.extend(ch.to_lowercase());
22 last_was_space = false;
23 }
24 }
25
26 normalized.trim_end().to_owned()
27}
28
29pub fn fuzzy_match(query: &str, candidate: &str) -> bool {
32 if query.is_empty() {
33 return true;
34 }
35
36 let mut matcher = Matcher::new(Config::DEFAULT);
37 let mut buffer = Vec::new();
38 let pattern = Pattern::parse(query, CaseMatching::Ignore, Normalization::Smart);
39 let utf32_candidate = Utf32Str::new(candidate, &mut buffer);
40 pattern.score(utf32_candidate, &mut matcher).is_some()
41}
42
43pub fn fuzzy_subsequence(needle: &str, haystack: &str) -> bool {
46 if needle.is_empty() {
47 return true;
48 }
49
50 let mut needle_chars = needle.chars();
51 let mut current = match needle_chars.next() {
52 Some(value) => value,
53 None => return true,
54 };
55
56 for ch in haystack.chars() {
57 if ch == current {
58 match needle_chars.next() {
59 Some(next) => current = next,
60 None => return true,
61 }
62 }
63 }
64
65 false
66}
67
68pub fn fuzzy_score(query: &str, candidate: &str) -> Option<u32> {
71 if query.is_empty() {
72 return Some(0); }
74
75 let mut matcher = Matcher::new(Config::DEFAULT);
76 let mut buffer = Vec::new();
77 let pattern = Pattern::parse(query, CaseMatching::Ignore, Normalization::Smart);
78 let utf32_candidate = Utf32Str::new(candidate, &mut buffer);
79 pattern.score(utf32_candidate, &mut matcher)
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85
86 #[test]
87 fn normalize_query_trims_and_lowercases() {
88 let normalized = normalize_query(" Foo Bar BAZ ");
89 assert_eq!(normalized, "foo bar baz");
90 }
91
92 #[test]
93 fn normalize_query_handles_whitespace_only() {
94 assert!(normalize_query(" ").is_empty());
95 }
96
97 #[test]
98 fn fuzzy_subsequence_requires_in_order_match() {
99 assert!(fuzzy_subsequence("abc", "a_b_c"));
100 assert!(!fuzzy_subsequence("abc", "acb"));
101 }
102
103 #[test]
104 fn fuzzy_match_supports_multiple_terms() {
105 assert!(fuzzy_match("run cmd", "run command"));
106 assert!(!fuzzy_match("missing", "run command"));
107 }
108
109 #[test]
110 fn fuzzy_match_with_nucleo_basic() {
111 assert!(fuzzy_match("smr", "src/main.rs"));
113 assert!(fuzzy_match("src main", "src/main.rs"));
114 assert!(fuzzy_match("main", "src/main.rs"));
115 assert!(!fuzzy_match("xyz", "src/main.rs"));
116 }
117
118 #[test]
119 fn fuzzy_score_returns_some_for_matches() {
120 assert!(fuzzy_score("smr", "src/main.rs").is_some());
122 assert!(fuzzy_score("main", "src/main.rs").is_some());
123 }
124
125 #[test]
126 fn fuzzy_score_returns_none_for_non_matches() {
127 assert!(fuzzy_score("xyz", "src/main.rs").is_none());
129 }
130}