Skip to main content

skim/engine/
factory.rs

1use regex::Regex;
2
3use crate::engine::all::MatchAllEngine;
4use crate::engine::andor::{AndEngine, OrEngine};
5use crate::engine::exact::{ExactEngine, ExactMatchingParam};
6use crate::engine::fuzzy::{FuzzyAlgorithm, FuzzyEngine};
7use crate::engine::regexp::RegexEngine;
8use crate::item::RankBuilder;
9use crate::{CaseMatching, MatchEngine, MatchEngineFactory, Typos};
10use std::sync::{Arc, LazyLock};
11
12static RE_OR_WITH_SPACES: LazyLock<Regex> = LazyLock::new(|| Regex::new(r" *\|+ *").unwrap());
13
14//------------------------------------------------------------------------------
15// Exact engine factory
16/// Factory for creating exact or fuzzy match engines based on configuration
17pub struct ExactOrFuzzyEngineFactory {
18    exact_mode: bool,
19    fuzzy_algorithm: FuzzyAlgorithm,
20    rank_builder: Arc<RankBuilder>,
21    typos: Typos,
22    filter_mode: bool,
23    last_match: bool,
24}
25
26impl ExactOrFuzzyEngineFactory {
27    /// Creates a new builder with default settings
28    #[must_use]
29    pub fn builder() -> Self {
30        Self {
31            exact_mode: false,
32            fuzzy_algorithm: FuzzyAlgorithm::SkimV2,
33            rank_builder: Default::default(),
34            typos: Typos::Disabled,
35            filter_mode: false,
36            last_match: false,
37        }
38    }
39
40    /// Sets whether to use exact matching mode
41    #[must_use]
42    pub fn exact_mode(mut self, exact_mode: bool) -> Self {
43        self.exact_mode = exact_mode;
44        self
45    }
46
47    /// Sets the fuzzy matching algorithm to use
48    #[must_use]
49    pub fn fuzzy_algorithm(mut self, fuzzy_algorithm: FuzzyAlgorithm) -> Self {
50        self.fuzzy_algorithm = fuzzy_algorithm;
51        self
52    }
53
54    /// Sets the rank builder for scoring matches
55    #[must_use]
56    pub fn rank_builder(mut self, rank_builder: Arc<RankBuilder>) -> Self {
57        self.rank_builder = rank_builder;
58        self
59    }
60
61    /// Sets the typo tolerance configuration
62    ///
63    /// - `Typos::Disabled`: no typo tolerance
64    /// - `Typos::Smart`: adaptive typo tolerance (`pattern_length` / 4)
65    /// - `Typos::Fixed(n)`: exactly n typos allowed
66    #[must_use]
67    pub fn typos(mut self, typos: Typos) -> Self {
68        self.typos = typos;
69        self
70    }
71
72    /// Sets filter mode (skips per-character match indices for faster matching)
73    #[must_use]
74    pub fn filter_mode(mut self, filter_mode: bool) -> Self {
75        self.filter_mode = filter_mode;
76        self
77    }
78
79    /// When true, prefer the last (rightmost) occurrence on tied scores
80    #[must_use]
81    pub fn last_match(mut self, last_match: bool) -> Self {
82        self.last_match = last_match;
83        self
84    }
85
86    /// Builds the factory (currently a no-op, returns self)
87    #[must_use]
88    pub fn build(self) -> Self {
89        self
90    }
91}
92
93impl MatchEngineFactory for ExactOrFuzzyEngineFactory {
94    fn create_engine_with_case(&self, query: &str, case: CaseMatching) -> Box<dyn MatchEngine> {
95        // 'abc => match exact "abc"
96        // ^abc => starts with "abc"
97        // abc$ => ends with "abc"
98        // ^abc$ => match exact "abc"
99        // !^abc => items not starting with "abc"
100        // !abc$ => items not ending with "abc"
101        // !^abc$ => not "abc"
102
103        let mut query = query;
104        let mut exact = self.exact_mode;
105        let mut param = ExactMatchingParam::default();
106        param.case = case;
107
108        if query.starts_with('\'') {
109            exact = !exact;
110            query = &query[1..];
111        }
112
113        if query.starts_with('!') {
114            query = &query[1..];
115            exact = true;
116            param.inverse = true;
117        }
118
119        if query.is_empty() {
120            // if only "!" was provided, will still show all items
121            return Box::new(
122                MatchAllEngine::builder()
123                    .rank_builder(self.rank_builder.clone())
124                    .build(),
125            );
126        }
127
128        if query.starts_with('^') {
129            query = &query[1..];
130            exact = true;
131            param.prefix = true;
132        }
133
134        if query.ends_with('$') {
135            query = &query[..(query.len() - 1)];
136            exact = true;
137            param.postfix = true;
138        }
139
140        if exact {
141            Box::new(
142                ExactEngine::builder(query, param)
143                    .rank_builder(self.rank_builder.clone())
144                    .build(),
145            )
146        } else {
147            Box::new(
148                FuzzyEngine::builder()
149                    .query(query)
150                    .algorithm(self.fuzzy_algorithm)
151                    .case(case)
152                    .typos(self.typos)
153                    .filter_mode(self.filter_mode)
154                    .last_match(self.last_match)
155                    .rank_builder(self.rank_builder.clone())
156                    .build(),
157            )
158        }
159    }
160}
161
162//------------------------------------------------------------------------------
163/// Factory for creating AND/OR composite match engines
164pub struct AndOrEngineFactory {
165    inner: Box<dyn MatchEngineFactory>,
166}
167
168impl AndOrEngineFactory {
169    /// Creates a new AND/OR engine factory wrapping another factory
170    pub fn new(factory: impl MatchEngineFactory + 'static) -> Self {
171        Self {
172            inner: Box::new(factory),
173        }
174    }
175
176    fn parse_andor(&self, query: &str, case: CaseMatching) -> Box<dyn MatchEngine> {
177        if query.trim().is_empty() {
178            return self.inner.create_engine_with_case(query, case);
179        }
180        let and_engines = RE_OR_WITH_SPACES
181            .replace_all(&Self::mask_escape_space(query), "|")
182            .split(' ')
183            .filter_map(|and_term| {
184                if and_term.is_empty() {
185                    return None;
186                }
187                let or_engines = and_term
188                    .split('|')
189                    .filter_map(|term| {
190                        if term.is_empty() {
191                            return None;
192                        }
193                        debug!("Creating Or engine for {term}");
194                        Some(
195                            self.inner
196                                .create_engine_with_case(&Self::unmask_escape_space(term), case),
197                        )
198                    })
199                    .collect::<Vec<_>>();
200                debug!("Building or matcher engine from Ors");
201                if or_engines.len() == 1 {
202                    return Some(or_engines.into_iter().next().unwrap());
203                }
204                Some(Box::new(OrEngine::builder().engines(or_engines).build()) as Box<dyn MatchEngine>)
205            })
206            .collect();
207        debug!("Creating and matcher engine from Ors");
208        Box::new(AndEngine::builder().engines(and_engines).build())
209    }
210
211    fn mask_escape_space(string: &str) -> String {
212        string.replace("\\ ", "\0")
213    }
214
215    fn unmask_escape_space(string: &str) -> String {
216        string.replace('\0', " ")
217    }
218}
219
220impl MatchEngineFactory for AndOrEngineFactory {
221    fn create_engine_with_case(&self, query: &str, case: CaseMatching) -> Box<dyn MatchEngine> {
222        self.parse_andor(query, case)
223    }
224}
225
226//------------------------------------------------------------------------------
227/// Factory for creating regex-based match engines
228pub struct RegexEngineFactory {
229    rank_builder: Arc<RankBuilder>,
230}
231
232impl RegexEngineFactory {
233    /// Creates a new builder with default settings
234    #[must_use]
235    pub fn builder() -> Self {
236        Self {
237            rank_builder: Default::default(),
238        }
239    }
240
241    /// Sets the rank builder for scoring matches
242    #[must_use]
243    pub fn rank_builder(mut self, rank_builder: Arc<RankBuilder>) -> Self {
244        self.rank_builder = rank_builder;
245        self
246    }
247
248    /// Builds the factory (currently a no-op, returns self)
249    #[must_use]
250    pub fn build(self) -> Self {
251        self
252    }
253}
254
255impl MatchEngineFactory for RegexEngineFactory {
256    fn create_engine_with_case(&self, query: &str, case: CaseMatching) -> Box<dyn MatchEngine> {
257        Box::new(
258            RegexEngine::builder(query, case)
259                .rank_builder(self.rank_builder.clone())
260                .build(),
261        )
262    }
263}
264
265#[cfg(test)]
266mod test {
267    #[test]
268    fn test_engine_factory() {
269        use super::*;
270        let exact_or_fuzzy = ExactOrFuzzyEngineFactory::builder().build();
271        let x = exact_or_fuzzy.create_engine("'abc");
272        assert_eq!(format!("{x}"), "(Exact|(?i)abc)");
273
274        let x = exact_or_fuzzy.create_engine("^abc");
275        assert_eq!(format!("{x}"), "(Exact|(?i)^abc)");
276
277        let x = exact_or_fuzzy.create_engine("abc$");
278        assert_eq!(format!("{x}"), "(Exact|(?i)abc$)");
279
280        let x = exact_or_fuzzy.create_engine("^abc$");
281        assert_eq!(format!("{x}"), "(Exact|(?i)^abc$)");
282
283        let x = exact_or_fuzzy.create_engine("!abc");
284        assert_eq!(format!("{x}"), "(Exact|!(?i)abc)");
285
286        let x = exact_or_fuzzy.create_engine("!^abc");
287        assert_eq!(format!("{x}"), "(Exact|!(?i)^abc)");
288
289        let x = exact_or_fuzzy.create_engine("!abc$");
290        assert_eq!(format!("{x}"), "(Exact|!(?i)abc$)");
291
292        let x = exact_or_fuzzy.create_engine("!^abc$");
293        assert_eq!(format!("{x}"), "(Exact|!(?i)^abc$)");
294
295        let regex_factory = RegexEngineFactory::builder();
296        let and_or_factory = AndOrEngineFactory::new(exact_or_fuzzy);
297
298        let x = and_or_factory.create_engine("'abc | def ^gh ij | kl mn");
299        assert_eq!(
300            format!("{x}"),
301            "(And: (Or: (Exact|(?i)abc), (Fuzzy: def)), (Exact|(?i)^gh), (Or: (Fuzzy: ij), (Fuzzy: kl)), (Fuzzy: mn))"
302        );
303
304        let x = regex_factory.create_engine("'abc | def ^gh ij | kl mn");
305        assert_eq!(format!("{x}"), "(Regex: 'abc | def ^gh ij | kl mn)");
306
307        let x = and_or_factory.create_engine("readme .md$ | .markdown$");
308        assert_eq!(
309            format!("{x}"),
310            "(And: (Fuzzy: readme), (Or: (Exact|(?i)\\.md$), (Exact|(?i)\\.markdown$)))"
311        );
312    }
313}