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