pub(crate) struct FuzzyQuery {
lower: String,
}
impl FuzzyQuery {
pub(crate) fn new(query: &str) -> Self {
FuzzyQuery {
lower: query.to_lowercase(),
}
}
pub(crate) fn camel_match(&self, candidate: &str) -> bool {
if self.lower.is_empty() {
return true;
}
if starts_with_at(candidate, &self.lower, 0) {
return true;
}
self.camel_abbrev(candidate)
}
pub(crate) fn symbol_match(&self, candidate: &str) -> bool {
if self.camel_match(candidate) {
return true;
}
candidate
.char_indices()
.any(|(i, _)| starts_with_at(candidate, &self.lower, i))
}
fn camel_abbrev(&self, candidate: &str) -> bool {
let mut query = self.lower.chars().peekable();
let mut prev: Option<char> = None;
for cc in candidate.chars() {
let Some(&qc) = query.peek() else {
return true;
};
let is_boundary = match prev {
None => true,
Some('_') | Some('$') => true,
Some(p) => cc.is_uppercase() && p.is_lowercase(),
};
if is_boundary && cc.to_lowercase().next() == Some(qc) {
query.next();
}
prev = Some(cc);
}
query.peek().is_none()
}
}
fn starts_with_at(candidate: &str, query_lower: &str, at: usize) -> bool {
let mut c = candidate[at..].chars().flat_map(char::to_lowercase);
let mut q = query_lower.chars();
loop {
match (q.next(), c.next()) {
(None, _) => return true,
(Some(qc), Some(cc)) if qc == cc => continue,
_ => return false,
}
}
}
pub(crate) fn fuzzy_camel_match(query: &str, candidate: &str) -> bool {
FuzzyQuery::new(query).camel_match(candidate)
}
pub(crate) fn fuzzy_symbol_match(query: &str, candidate: &str) -> bool {
FuzzyQuery::new(query).symbol_match(candidate)
}
pub(crate) fn camel_sort_key(query: &str, label: &str) -> String {
let lq = query.to_lowercase();
let ll = label.to_lowercase();
if ll.starts_with(&lq) {
format!("0{}", ll)
} else {
format!("1{}", ll)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fuzzy_camel_match_prefix() {
assert!(fuzzy_camel_match("Blog", "BlogController"));
assert!(fuzzy_camel_match("blog", "BlogController"));
}
#[test]
fn fuzzy_camel_match_abbreviation() {
assert!(fuzzy_camel_match("BC", "BlogController"));
assert!(fuzzy_camel_match("GRF", "getRecentFiles"));
assert!(fuzzy_camel_match("str_r", "str_replace")); }
#[test]
fn fuzzy_camel_match_no_substring() {
assert!(!fuzzy_camel_match("Controller", "BlogController"));
assert!(!fuzzy_camel_match("xyz", "BlogController"));
}
#[test]
fn fuzzy_symbol_match_substring_fallback() {
assert!(fuzzy_symbol_match("Controller", "BlogController"));
assert!(fuzzy_symbol_match("controller", "BlogController"));
assert!(fuzzy_symbol_match("controller", "UserController"));
assert!(fuzzy_symbol_match("Blog", "BlogController"));
assert!(fuzzy_symbol_match("BC", "BlogController"));
assert!(!fuzzy_symbol_match("xyz", "BlogController"));
}
}