skimmer/engine/
fuzzy.rs

1use std::cmp::min;
2use std::fmt::{Display, Error, Formatter};
3use std::sync::Arc;
4
5use fuzzy_matcher::clangd::ClangdMatcher;
6use fuzzy_matcher::skim::SkimMatcherV2;
7use fuzzy_matcher::FuzzyMatcher;
8
9use crate::item::RankBuilder;
10use crate::{CaseMatching, MatchEngine};
11use crate::{MatchRange, MatchResult, SkimItem};
12
13//------------------------------------------------------------------------------
14#[derive(Debug, Copy, Clone, Default)]
15pub enum FuzzyAlgorithm {
16    #[default]
17    SkimV2,
18    SkimV1,
19    Clangd,
20}
21
22impl FuzzyAlgorithm {
23    pub fn of(algorithm: &str) -> Self {
24        match algorithm.to_ascii_lowercase().as_ref() {
25            "skim_v1" => FuzzyAlgorithm::SkimV1,
26            "skim_v2" | "skim" => FuzzyAlgorithm::SkimV2,
27            "clangd" => FuzzyAlgorithm::Clangd,
28            _ => FuzzyAlgorithm::SkimV2,
29        }
30    }
31}
32
33impl clap::ValueEnum for FuzzyAlgorithm {
34    fn value_variants<'a>() -> &'a [Self] {
35        &[Self::SkimV1, Self::SkimV2, Self::Clangd]
36    }
37
38    fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
39        match self {
40            Self::SkimV1 => Some(clap::builder::PossibleValue::new("skim_v1")),
41            Self::SkimV2 => Some(clap::builder::PossibleValue::new("skim_v2")),
42            Self::Clangd => Some(clap::builder::PossibleValue::new("clangd")),
43        }
44    }
45}
46
47const BYTES_1M: usize = 1024 * 1024 * 1024;
48
49//------------------------------------------------------------------------------
50// Fuzzy engine
51#[derive(Default)]
52pub struct FuzzyEngineBuilder {
53    query: String,
54    case: CaseMatching,
55    algorithm: FuzzyAlgorithm,
56    rank_builder: Arc<RankBuilder>,
57}
58
59impl FuzzyEngineBuilder {
60    pub fn query(mut self, query: &str) -> Self {
61        self.query = query.to_string();
62        self
63    }
64
65    pub fn case(mut self, case: CaseMatching) -> Self {
66        self.case = case;
67        self
68    }
69
70    pub fn algorithm(mut self, algorithm: FuzzyAlgorithm) -> Self {
71        self.algorithm = algorithm;
72        self
73    }
74
75    pub fn rank_builder(mut self, rank_builder: Arc<RankBuilder>) -> Self {
76        self.rank_builder = rank_builder;
77        self
78    }
79
80    #[allow(deprecated)]
81    pub fn build(self) -> FuzzyEngine {
82        use fuzzy_matcher::skim::SkimMatcher;
83        let matcher: Box<dyn FuzzyMatcher> = match self.algorithm {
84            FuzzyAlgorithm::SkimV1 => Box::new(SkimMatcher::default()),
85            FuzzyAlgorithm::SkimV2 => {
86                let matcher = SkimMatcherV2::default().element_limit(BYTES_1M);
87                let matcher = match self.case {
88                    CaseMatching::Respect => matcher.respect_case(),
89                    CaseMatching::Ignore => matcher.ignore_case(),
90                    CaseMatching::Smart => matcher.smart_case(),
91                };
92                Box::new(matcher)
93            }
94            FuzzyAlgorithm::Clangd => {
95                let matcher = ClangdMatcher::default();
96                let matcher = match self.case {
97                    CaseMatching::Respect => matcher.respect_case(),
98                    CaseMatching::Ignore => matcher.ignore_case(),
99                    CaseMatching::Smart => matcher.smart_case(),
100                };
101                Box::new(matcher)
102            }
103        };
104
105        FuzzyEngine {
106            matcher,
107            query: self.query,
108            rank_builder: self.rank_builder,
109        }
110    }
111}
112
113pub struct FuzzyEngine {
114    query: String,
115    matcher: Box<dyn FuzzyMatcher>,
116    rank_builder: Arc<RankBuilder>,
117}
118
119impl FuzzyEngine {
120    pub fn builder() -> FuzzyEngineBuilder {
121        FuzzyEngineBuilder::default()
122    }
123
124    fn fuzzy_match(&self, choice: &str, pattern: &str) -> Option<(i64, Vec<usize>)> {
125        if pattern.is_empty() {
126            return Some((0, Vec::new()));
127        } else if choice.is_empty() {
128            return None;
129        }
130
131        self.matcher.fuzzy_indices(choice, pattern)
132    }
133}
134
135impl MatchEngine for FuzzyEngine {
136    fn match_item(&self, item: Arc<dyn SkimItem>) -> Option<MatchResult> {
137        // iterate over all matching fields:
138        let mut matched_result = None;
139        let item_text = item.text();
140        let default_range = [(0, item_text.len())];
141        for &(start, end) in item.get_matching_ranges().unwrap_or(&default_range) {
142            let start = min(start, item_text.len());
143            let end = min(end, item_text.len());
144            matched_result = self.fuzzy_match(&item_text[start..end], &self.query).map(|(s, vec)| {
145                if start != 0 {
146                    let start_char = &item_text[..start].chars().count();
147                    (s, vec.iter().map(|x| x + start_char).collect())
148                } else {
149                    (s, vec)
150                }
151            });
152
153            if matched_result.is_some() {
154                break;
155            }
156        }
157
158        matched_result.as_ref()?;
159
160        let (score, matched_range) = matched_result.unwrap();
161
162        let begin = *matched_range.first().unwrap_or(&0);
163        let end = *matched_range.last().unwrap_or(&0);
164
165        let item_len = item_text.len();
166        Some(MatchResult {
167            rank: self.rank_builder.build_rank(score as i32, begin, end, item_len),
168            matched_range: MatchRange::Chars(matched_range),
169        })
170    }
171}
172
173impl Display for FuzzyEngine {
174    fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
175        write!(f, "(Fuzzy: {})", self.query)
176    }
177}