pub mod arinae;
pub mod clangd;
#[cfg(feature = "frizbee")]
pub mod frizbee;
pub mod fzy;
pub mod skim;
mod util;
pub(crate) type IndexType = usize;
pub(crate) type ScoreType = i64;
pub(crate) type MatchIndices = Vec<IndexType>;
pub trait FuzzyMatcher: Send + Sync {
fn fuzzy_indices(&self, choice: &str, pattern: &str) -> Option<(i64, MatchIndices)>;
fn fuzzy_match(&self, choice: &str, pattern: &str) -> Option<i64> {
self.fuzzy_indices(choice, pattern).map(|(score, _)| score)
}
fn fuzzy_match_range(&self, choice: &str, pattern: &str) -> Option<(i64, usize, usize)> {
self.fuzzy_indices(choice, pattern).map(|(score, indices)| {
let begin = indices.first().copied().unwrap_or(0);
let end = indices.last().copied().unwrap_or(0);
(score, begin, end)
})
}
}
#[cfg(test)]
#[cfg_attr(coverage, coverage(off))]
mod tests {
use super::*;
struct StubMatcher;
impl FuzzyMatcher for StubMatcher {
fn fuzzy_indices(&self, choice: &str, pattern: &str) -> Option<(i64, MatchIndices)> {
if pattern.is_empty() {
return Some((0, vec![]));
}
choice
.starts_with(pattern)
.then(|| (10, (0..pattern.chars().count()).collect()))
}
}
#[test]
fn default_fuzzy_match_uses_indices_score() {
assert_eq!(StubMatcher.fuzzy_match("hello", "he"), Some(10));
assert_eq!(StubMatcher.fuzzy_match("hello", "xy"), None);
}
#[test]
fn default_fuzzy_match_range_spans_first_to_last() {
assert_eq!(StubMatcher.fuzzy_match_range("hello", "hel"), Some((10, 0, 2)));
assert_eq!(StubMatcher.fuzzy_match_range("hello", "zz"), None);
}
#[test]
fn default_fuzzy_match_range_empty_indices_default_to_zero() {
assert_eq!(StubMatcher.fuzzy_match_range("hello", ""), Some((0, 0, 0)));
}
#[test]
fn multichar_lowercase_does_not_panic() {
use crate::fuzzy_matcher::clangd::ClangdMatcher;
use crate::fuzzy_matcher::fzy::FzyMatcher;
use crate::fuzzy_matcher::skim::SkimMatcherV2;
let skim = SkimMatcherV2::default();
let fzy = FzyMatcher::default();
let clangd = ClangdMatcher::default();
let matchers: [(&str, &dyn FuzzyMatcher); 3] = [("skim", &skim), ("fzy", &fzy), ("clangd", &clangd)];
let cases = [
("Jİ:I", "İ:İ"),
("I", "İ"),
("İ", "I"),
("i", "İ"),
("İ", "i"),
("Jİ:Iİ", "İİ"),
("straße", "STRASSE"),
("ffly", "ffl"),
];
for (choice, pattern) in cases {
let num_chars = choice.chars().count();
for (name, matcher) in matchers {
if let Some((_score, indices)) = matcher.fuzzy_indices(choice, pattern) {
for idx in indices {
assert!(
idx < num_chars,
"{name}: match index {idx} out of bounds for {choice:?} ({num_chars} chars)"
);
}
}
}
}
}
}