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#[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#[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 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}