skim/engine/
factory.rs

1use crate::engine::all::MatchAllEngine;
2use crate::engine::andor::{AndEngine, OrEngine};
3use crate::engine::exact::{ExactEngine, ExactMatchingParam};
4use crate::engine::fuzzy::{FuzzyAlgorithm, FuzzyEngine};
5use crate::engine::regexp::RegexEngine;
6use crate::item::RankBuilder;
7use crate::{CaseMatching, MatchEngine, MatchEngineFactory};
8use regex::Regex;
9use std::sync::{Arc, LazyLock};
10
11static RE_AND: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"([^ |]+( +\| +[^ |]*)+)|( +)").unwrap());
12static RE_OR: LazyLock<Regex> = LazyLock::new(|| Regex::new(r" +\| +").unwrap());
13//------------------------------------------------------------------------------
14// Exact engine factory
15/// Factory for creating exact or fuzzy match engines based on configuration
16pub struct ExactOrFuzzyEngineFactory {
17    exact_mode: bool,
18    fuzzy_algorithm: FuzzyAlgorithm,
19    rank_builder: Arc<RankBuilder>,
20}
21
22impl ExactOrFuzzyEngineFactory {
23    /// Creates a new builder with default settings
24    pub fn builder() -> Self {
25        Self {
26            exact_mode: false,
27            fuzzy_algorithm: FuzzyAlgorithm::SkimV2,
28            rank_builder: Default::default(),
29        }
30    }
31
32    /// Sets whether to use exact matching mode
33    pub fn exact_mode(mut self, exact_mode: bool) -> Self {
34        self.exact_mode = exact_mode;
35        self
36    }
37
38    /// Sets the fuzzy matching algorithm to use
39    pub fn fuzzy_algorithm(mut self, fuzzy_algorithm: FuzzyAlgorithm) -> Self {
40        self.fuzzy_algorithm = fuzzy_algorithm;
41        self
42    }
43
44    /// Sets the rank builder for scoring matches
45    pub fn rank_builder(mut self, rank_builder: Arc<RankBuilder>) -> Self {
46        self.rank_builder = rank_builder;
47        self
48    }
49
50    /// Builds the factory (currently a no-op, returns self)
51    pub fn build(self) -> Self {
52        self
53    }
54}
55
56impl MatchEngineFactory for ExactOrFuzzyEngineFactory {
57    fn create_engine_with_case(&self, query: &str, case: CaseMatching) -> Box<dyn MatchEngine> {
58        // 'abc => match exact "abc"
59        // ^abc => starts with "abc"
60        // abc$ => ends with "abc"
61        // ^abc$ => match exact "abc"
62        // !^abc => items not starting with "abc"
63        // !abc$ => items not ending with "abc"
64        // !^abc$ => not "abc"
65
66        let mut query = query;
67        let mut exact = false;
68        let mut param = ExactMatchingParam::default();
69        param.case = case;
70
71        if query.starts_with('\'') {
72            if self.exact_mode {
73                return Box::new(
74                    FuzzyEngine::builder()
75                        .query(&query[1..])
76                        .algorithm(self.fuzzy_algorithm)
77                        .case(case)
78                        .rank_builder(self.rank_builder.clone())
79                        .build(),
80                );
81            } else {
82                exact = true;
83                query = &query[1..];
84            }
85        }
86
87        if query.starts_with('!') {
88            query = &query[1..];
89            exact = true;
90            param.inverse = true;
91        }
92
93        if query.is_empty() {
94            // if only "!" was provided, will still show all items
95            return Box::new(
96                MatchAllEngine::builder()
97                    .rank_builder(self.rank_builder.clone())
98                    .build(),
99            );
100        }
101
102        if query.starts_with('^') {
103            query = &query[1..];
104            exact = true;
105            param.prefix = true;
106        }
107
108        if query.ends_with('$') {
109            query = &query[..(query.len() - 1)];
110            exact = true;
111            param.postfix = true;
112        }
113
114        if self.exact_mode {
115            exact = true;
116        }
117
118        if exact {
119            Box::new(
120                ExactEngine::builder(query, param)
121                    .rank_builder(self.rank_builder.clone())
122                    .build(),
123            )
124        } else {
125            Box::new(
126                FuzzyEngine::builder()
127                    .query(query)
128                    .algorithm(self.fuzzy_algorithm)
129                    .case(case)
130                    .rank_builder(self.rank_builder.clone())
131                    .build(),
132            )
133        }
134    }
135}
136
137//------------------------------------------------------------------------------
138/// Factory for creating AND/OR composite match engines
139pub struct AndOrEngineFactory {
140    inner: Box<dyn MatchEngineFactory>,
141}
142
143impl AndOrEngineFactory {
144    /// Creates a new AND/OR engine factory wrapping another factory
145    pub fn new(factory: impl MatchEngineFactory + 'static) -> Self {
146        Self {
147            inner: Box::new(factory),
148        }
149    }
150
151    // we want to treat `\ ` as plain white space
152    // regex crate doesn't support look around, so I use a lazy workaround
153    // that replace `\ ` with `\0` ahead of split and replace it back afterwards
154    fn parse_or(&self, query: &str, case: CaseMatching) -> Box<dyn MatchEngine> {
155        if query.trim().is_empty() {
156            self.inner.create_engine_with_case(query, case)
157        } else {
158            let engines = RE_OR
159                .split(&self.mask_escape_space(query))
160                .map(|q| self.parse_and(q, case))
161                .collect();
162            Box::new(OrEngine::builder().engines(engines).build())
163        }
164    }
165
166    fn parse_and(&self, query: &str, case: CaseMatching) -> Box<dyn MatchEngine> {
167        let query_trim = query.trim_matches(|c| c == ' ' || c == '|');
168        let mut engines = vec![];
169        let mut last = 0;
170        for mat in RE_AND.find_iter(query_trim) {
171            let (start, end) = (mat.start(), mat.end());
172            let term = query_trim[last..start].trim_matches(|c| c == ' ' || c == '|');
173            let term = self.unmask_escape_space(term);
174            if !term.is_empty() {
175                engines.push(self.inner.create_engine_with_case(&term, case));
176            }
177
178            if !mat.as_str().trim().is_empty() {
179                engines.push(self.parse_or(mat.as_str().trim(), case));
180            }
181            last = end;
182        }
183
184        let term = query_trim[last..].trim_matches(|c| c == ' ' || c == '|');
185        let term = self.unmask_escape_space(term);
186        if !term.is_empty() {
187            engines.push(self.inner.create_engine_with_case(&term, case));
188        }
189        Box::new(AndEngine::builder().engines(engines).build())
190    }
191
192    fn mask_escape_space(&self, string: &str) -> String {
193        string.replace("\\ ", "\0")
194    }
195
196    fn unmask_escape_space(&self, string: &str) -> String {
197        string.replace('\0', " ")
198    }
199}
200
201impl MatchEngineFactory for AndOrEngineFactory {
202    fn create_engine_with_case(&self, query: &str, case: CaseMatching) -> Box<dyn MatchEngine> {
203        self.parse_or(query, case)
204    }
205}
206
207//------------------------------------------------------------------------------
208/// Factory for creating regex-based match engines
209pub struct RegexEngineFactory {
210    rank_builder: Arc<RankBuilder>,
211}
212
213impl RegexEngineFactory {
214    /// Creates a new builder with default settings
215    pub fn builder() -> Self {
216        Self {
217            rank_builder: Default::default(),
218        }
219    }
220
221    /// Sets the rank builder for scoring matches
222    pub fn rank_builder(mut self, rank_builder: Arc<RankBuilder>) -> Self {
223        self.rank_builder = rank_builder;
224        self
225    }
226
227    /// Builds the factory (currently a no-op, returns self)
228    pub fn build(self) -> Self {
229        self
230    }
231}
232
233impl MatchEngineFactory for RegexEngineFactory {
234    fn create_engine_with_case(&self, query: &str, case: CaseMatching) -> Box<dyn MatchEngine> {
235        Box::new(
236            RegexEngine::builder(query, case)
237                .rank_builder(self.rank_builder.clone())
238                .build(),
239        )
240    }
241}
242
243mod test {
244    #[test]
245    fn test_engine_factory() {
246        use super::*;
247        let exact_or_fuzzy = ExactOrFuzzyEngineFactory::builder().build();
248        let x = exact_or_fuzzy.create_engine("'abc");
249        assert_eq!(format!("{x}"), "(Exact|(?i)abc)");
250
251        let x = exact_or_fuzzy.create_engine("^abc");
252        assert_eq!(format!("{x}"), "(Exact|(?i)^abc)");
253
254        let x = exact_or_fuzzy.create_engine("abc$");
255        assert_eq!(format!("{x}"), "(Exact|(?i)abc$)");
256
257        let x = exact_or_fuzzy.create_engine("^abc$");
258        assert_eq!(format!("{x}"), "(Exact|(?i)^abc$)");
259
260        let x = exact_or_fuzzy.create_engine("!abc");
261        assert_eq!(format!("{x}"), "(Exact|!(?i)abc)");
262
263        let x = exact_or_fuzzy.create_engine("!^abc");
264        assert_eq!(format!("{x}"), "(Exact|!(?i)^abc)");
265
266        let x = exact_or_fuzzy.create_engine("!abc$");
267        assert_eq!(format!("{x}"), "(Exact|!(?i)abc$)");
268
269        let x = exact_or_fuzzy.create_engine("!^abc$");
270        assert_eq!(format!("{x}"), "(Exact|!(?i)^abc$)");
271
272        let regex_factory = RegexEngineFactory::builder();
273        let and_or_factory = AndOrEngineFactory::new(exact_or_fuzzy);
274
275        let x = and_or_factory.create_engine("'abc | def ^gh ij | kl mn");
276        assert_eq!(
277            format!("{x}"),
278            "(Or: (And: (Exact|(?i)abc)), (And: (Fuzzy: def), (Exact|(?i)^gh), (Fuzzy: ij)), (And: (Fuzzy: kl), (Fuzzy: mn)))"
279        );
280
281        let x = regex_factory.create_engine("'abc | def ^gh ij | kl mn");
282        assert_eq!(format!("{x}"), "(Regex: 'abc | def ^gh ij | kl mn)");
283    }
284}