skim/engine/
fuzzy.rs

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