use nucleo_matcher::pattern::{CaseMatching, Normalization, Pattern};
use nucleo_matcher::{Config, Matcher, Utf32Str};
pub fn normalize_query(query: &str) -> String {
let trimmed = query.trim();
if trimmed.is_empty() {
return String::new();
}
let mut normalized = String::with_capacity(trimmed.len());
let mut last_was_space = false;
for ch in trimmed.chars() {
if ch.is_whitespace() {
if !last_was_space && !normalized.is_empty() {
normalized.push(' ');
}
last_was_space = true;
} else {
normalized.extend(ch.to_lowercase());
last_was_space = false;
}
}
normalized.trim_end().to_owned()
}
pub fn fuzzy_match(query: &str, candidate: &str) -> bool {
if query.is_empty() {
return true;
}
let mut matcher = Matcher::new(Config::DEFAULT);
let mut buffer = Vec::new();
let pattern = Pattern::parse(query, CaseMatching::Ignore, Normalization::Smart);
let utf32_candidate = Utf32Str::new(candidate, &mut buffer);
pattern.score(utf32_candidate, &mut matcher).is_some()
}
#[inline]
pub fn exact_terms_match(query: &str, candidate: &str) -> bool {
if query.is_empty() {
return true;
}
query
.split_whitespace()
.all(|term| candidate.contains(term))
}
pub fn fuzzy_subsequence(needle: &str, haystack: &str) -> bool {
if needle.is_empty() {
return true;
}
let mut needle_chars = needle.chars();
let mut current = match needle_chars.next() {
Some(value) => value,
None => return true,
};
for ch in haystack.chars() {
if ch == current {
match needle_chars.next() {
Some(next) => current = next,
None => return true,
}
}
}
false
}
pub fn fuzzy_score(query: &str, candidate: &str) -> Option<u32> {
if query.is_empty() {
return Some(0); }
let mut matcher = Matcher::new(Config::DEFAULT);
let mut buffer = Vec::new();
let pattern = Pattern::parse(query, CaseMatching::Ignore, Normalization::Smart);
let utf32_candidate = Utf32Str::new(candidate, &mut buffer);
pattern.score(utf32_candidate, &mut matcher)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn normalize_query_trims_and_lowercases() {
let normalized = normalize_query(" Foo Bar BAZ ");
assert_eq!(normalized, "foo bar baz");
}
#[test]
fn normalize_query_handles_whitespace_only() {
assert!(normalize_query(" ").is_empty());
}
#[test]
fn fuzzy_subsequence_requires_in_order_match() {
assert!(fuzzy_subsequence("abc", "a_b_c"));
assert!(!fuzzy_subsequence("abc", "acb"));
}
#[test]
fn fuzzy_match_supports_multiple_terms() {
assert!(fuzzy_match("run cmd", "run command"));
assert!(!fuzzy_match("missing", "run command"));
}
#[test]
fn fuzzy_match_with_nucleo_basic() {
assert!(fuzzy_match("smr", "src/main.rs"));
assert!(fuzzy_match("src main", "src/main.rs"));
assert!(fuzzy_match("main", "src/main.rs"));
assert!(!fuzzy_match("xyz", "src/main.rs"));
}
#[test]
fn fuzzy_score_returns_some_for_matches() {
assert!(fuzzy_score("smr", "src/main.rs").is_some());
assert!(fuzzy_score("main", "src/main.rs").is_some());
}
#[test]
fn fuzzy_score_returns_none_for_non_matches() {
assert!(fuzzy_score("xyz", "src/main.rs").is_none());
}
#[test]
fn exact_terms_match_requires_substring() {
assert!(exact_terms_match("openai", "openai openai gpt-5.4 gpt-5.4"));
assert!(exact_terms_match("gpt", "openai openai gpt-5.4 gpt-5.4"));
assert!(!exact_terms_match(
"anthropic",
"openai openai gpt-5.4 gpt-5.4"
));
}
#[test]
fn exact_terms_match_multi_term_requires_all() {
let candidate = "anthropic anthropic claude 4 sonnet claude-4-sonnet";
assert!(exact_terms_match("anthropic claude", candidate));
assert!(exact_terms_match("sonnet", candidate));
assert!(!exact_terms_match("anthropic gpt", candidate));
}
#[test]
fn exact_terms_match_empty_query_matches_everything() {
assert!(exact_terms_match("", "anything"));
}
#[test]
fn exact_terms_match_rejects_fuzzy_subsequences() {
assert!(!exact_terms_match("smr", "src/main.rs"));
}
#[test]
fn exact_terms_match_provider_filtering() {
let openai = "openai openai gpt-5.4 gpt-5.4 reasoning tools image";
let anthropic = "anthropic anthropic claude 4 sonnet claude-4-sonnet reasoning tools";
let gemini = "gemini gemini gemini 2.5 pro gemini-2.5-pro reasoning tools";
assert!(exact_terms_match("openai", openai));
assert!(!exact_terms_match("openai", anthropic));
assert!(!exact_terms_match("openai", gemini));
assert!(exact_terms_match("openai gpt", openai));
assert!(!exact_terms_match("openai claude", openai));
assert!(exact_terms_match("reasoning", openai));
assert!(exact_terms_match("reasoning", anthropic));
}
}