norm/metrics/fzf/
query.rs

1use core::fmt::Write;
2
3/// A parsed fzf query.
4///
5/// This struct is created by parsing a query string via the
6/// [`FzfParser`](super::FzfParser). See its documentation for more.
7#[derive(Clone, Copy)]
8pub struct FzfQuery<'a> {
9    pub(super) search_mode: SearchMode<'a>,
10}
11
12/// TODO: docs
13#[derive(Clone, Copy)]
14pub(super) enum SearchMode<'a> {
15    /// TODO: docs
16    Extended(&'a [Condition<'a>]),
17
18    /// TODO: docs
19    NotExtended(Pattern<'a>),
20}
21
22impl core::fmt::Debug for FzfQuery<'_> {
23    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
24        let s = match self.search_mode {
25            SearchMode::Extended(conditions) => conditions
26                .iter()
27                .map(|condition| format!("{:?}", condition))
28                .collect::<Vec<_>>()
29                .join(" && "),
30
31            SearchMode::NotExtended(pattern) => pattern.into_string(),
32        };
33
34        f.debug_tuple("FzfQuery").field(&s).finish()
35    }
36}
37
38impl<'a> FzfQuery<'a> {
39    /// TODO: docs
40    #[inline]
41    pub(super) fn is_empty(&self) -> bool {
42        match self.search_mode {
43            SearchMode::Extended(conditions) => conditions.is_empty(),
44            SearchMode::NotExtended(pattern) => pattern.is_empty(),
45        }
46    }
47
48    /// TODO: docs
49    #[inline]
50    pub(super) fn new_extended(conditions: &'a [Condition<'a>]) -> Self {
51        // If there's only one condition with a single pattern, and that
52        // pattern is fuzzy, then we can use the non-extended search mode.
53        if conditions.len() == 1 {
54            let mut patterns = conditions[0].iter();
55
56            let first_pattern = patterns
57                .next()
58                .expect("conditions always have at least one pattern");
59
60            if patterns.next().is_none()
61                && matches!(first_pattern.match_type, MatchType::Fuzzy)
62            {
63                return Self {
64                    search_mode: SearchMode::NotExtended(first_pattern),
65                };
66            }
67        }
68
69        Self { search_mode: SearchMode::Extended(conditions) }
70    }
71
72    /// TODO: docs
73    #[inline]
74    pub(super) fn new_not_extended(chars: &'a [char]) -> Self {
75        Self { search_mode: SearchMode::NotExtended(Pattern::raw(chars)) }
76    }
77}
78
79/// TODO: docs
80#[derive(Default, Clone, Copy)]
81pub(super) struct Condition<'a> {
82    /// TODO: docs
83    pub(super) or_patterns: &'a [Pattern<'a>],
84}
85
86impl core::fmt::Debug for Condition<'_> {
87    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
88        match self.or_patterns {
89            [] => Ok(()),
90
91            [pattern] => pattern.into_string().fmt(f),
92
93            _ => {
94                f.write_char('(')?;
95
96                let len = self.or_patterns.len();
97
98                for (idx, pattern) in self.iter().enumerate() {
99                    let is_last = idx + 1 == len;
100
101                    pattern.into_string().fmt(f)?;
102
103                    if !is_last {
104                        f.write_str(" || ")?;
105                    }
106                }
107
108                f.write_char(')')
109            },
110        }
111    }
112}
113
114impl<'a> Condition<'a> {
115    #[cfg(test)]
116    pub(super) fn or_patterns(&self) -> &'a [Pattern<'a>] {
117        self.or_patterns
118    }
119
120    #[inline]
121    pub(super) fn iter(
122        &self,
123    ) -> impl Iterator<Item = Pattern<'a>> + ExactSizeIterator + '_ {
124        self.or_patterns.iter().copied()
125    }
126
127    #[inline]
128    pub(super) fn new(or_patterns: &'a [Pattern<'a>]) -> Self {
129        Self { or_patterns }
130    }
131}
132
133/// TODO: docs
134#[derive(Default, Clone, Copy)]
135#[cfg_attr(test, derive(PartialEq))]
136pub(super) struct Pattern<'a> {
137    /// TODO: docs
138    text: &'a [char],
139
140    /// Whether any of the characters in [`Self::text`] are uppercase.
141    pub(super) has_uppercase: bool,
142
143    /// TODO: docs
144    pub(super) match_type: MatchType,
145
146    /// TODO: docs
147    pub(super) is_inverse: bool,
148
149    /// TODO: docs
150    pub(super) leading_spaces: usize,
151
152    /// TODO: docs
153    pub(super) trailing_spaces: usize,
154}
155
156impl core::fmt::Debug for Pattern<'_> {
157    #[inline]
158    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
159        self.into_string().fmt(f)
160    }
161}
162
163impl<'a> Pattern<'a> {
164    /// TODO: docs
165    #[inline(always)]
166    pub(super) fn char_len(&self) -> usize {
167        self.text.len()
168    }
169
170    /// TODO: docs
171    #[inline(always)]
172    pub(super) fn char(&self, idx: usize) -> char {
173        self.text[idx]
174    }
175
176    /// TODO: docs
177    #[inline]
178    pub(crate) fn chars(
179        &self,
180    ) -> impl Iterator<Item = char> + DoubleEndedIterator + '_ {
181        self.text.iter().copied()
182    }
183
184    /// TODO: docs
185    #[inline]
186    pub(super) fn is_empty(&self) -> bool {
187        self.text.is_empty()
188    }
189
190    /// TODO: docs
191    #[inline]
192    pub(super) fn into_string(self) -> String {
193        self.text.iter().collect::<String>()
194    }
195
196    /// TODO: docs
197    #[inline(always)]
198    pub(super) fn leading_spaces(&self) -> usize {
199        self.leading_spaces
200    }
201
202    /// TODO: docs
203    #[inline]
204    fn raw(text: &'a [char]) -> Self {
205        let leading_spaces = text.iter().take_while(|&&c| c == ' ').count();
206
207        let trailing_spaces =
208            text.iter().rev().take_while(|&&c| c == ' ').count();
209
210        Self {
211            leading_spaces,
212            trailing_spaces,
213            has_uppercase: text.iter().copied().any(char::is_uppercase),
214            text,
215            match_type: MatchType::Fuzzy,
216            is_inverse: false,
217        }
218    }
219
220    /// TODO: docs
221    #[inline]
222    pub(super) fn parse(mut text: &'a [char]) -> Option<Self> {
223        debug_assert!(!text.is_empty());
224
225        let mut is_inverse = false;
226
227        let mut match_type = MatchType::Fuzzy;
228
229        if starts_with(text, '!') {
230            is_inverse = true;
231            match_type = MatchType::Exact;
232            text = &text[1..];
233        }
234
235        if ends_with(text, '$') && text.len() > 1 {
236            match_type = MatchType::SuffixExact;
237            text = &text[..text.len() - 1];
238        }
239
240        if starts_with(text, '\'') {
241            match_type =
242                if !is_inverse { MatchType::Exact } else { MatchType::Fuzzy };
243
244            text = &text[1..];
245        } else if starts_with(text, '^') {
246            match_type = if match_type == MatchType::SuffixExact {
247                MatchType::EqualExact
248            } else {
249                MatchType::PrefixExact
250            };
251
252            text = &text[1..];
253        }
254
255        if text.is_empty() {
256            return None;
257        }
258
259        let has_uppercase = text.iter().copied().any(char::is_uppercase);
260
261        let leading_spaces = text.iter().take_while(|&&c| c == ' ').count();
262
263        let trailing_spaces =
264            text.iter().rev().take_while(|&&c| c == ' ').count();
265
266        let this = Self {
267            is_inverse,
268            match_type,
269            text,
270            has_uppercase,
271            leading_spaces,
272            trailing_spaces,
273        };
274
275        Some(this)
276    }
277
278    /// TODO: docs
279    #[inline(always)]
280    pub(super) fn trailing_spaces(&self) -> usize {
281        self.trailing_spaces
282    }
283}
284
285#[inline(always)]
286fn ends_with(haystack: &[char], needle: char) -> bool {
287    haystack.last().copied() == Some(needle)
288}
289
290#[inline(always)]
291fn starts_with(haystack: &[char], needle: char) -> bool {
292    haystack.first().copied() == Some(needle)
293}
294
295/// TODO: docs
296#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
297pub(super) enum MatchType {
298    /// TODO: docs
299    #[default]
300    Fuzzy,
301
302    /// TODO: docs
303    Exact,
304
305    /// TODO: docs
306    PrefixExact,
307
308    /// TODO: docs
309    SuffixExact,
310
311    /// TODO: docs
312    EqualExact,
313}
314
315#[cfg(test)]
316mod tests {
317    use super::*;
318
319    #[test]
320    fn pattern_parse_specials_1() {
321        assert!(Pattern::parse(&['\'']).is_none());
322        assert!(Pattern::parse(&['^']).is_none());
323        assert!(Pattern::parse(&['!']).is_none());
324
325        let pattern = Pattern::parse(&['$']).unwrap();
326        assert_eq!(pattern.into_string(), "$");
327        assert_eq!(pattern.match_type, MatchType::Fuzzy);
328    }
329
330    #[test]
331    fn pattern_parse_specials_2() {
332        assert!(Pattern::parse(&['!', '\'']).is_none());
333        assert!(Pattern::parse(&['!', '^']).is_none());
334        assert!(Pattern::parse(&['\'', '$']).is_none());
335        assert!(Pattern::parse(&['^', '$']).is_none());
336
337        let pattern = Pattern::parse(&['\'', '^']).unwrap();
338        assert_eq!(pattern.into_string(), "^");
339        assert_eq!(pattern.match_type, MatchType::Exact);
340
341        let pattern = Pattern::parse(&['!', '$']).unwrap();
342        assert_eq!(pattern.into_string(), "$");
343        assert_eq!(pattern.match_type, MatchType::Exact);
344        assert!(pattern.is_inverse);
345
346        let pattern = Pattern::parse(&['!', '!']).unwrap();
347        assert_eq!(pattern.into_string(), "!");
348        assert_eq!(pattern.match_type, MatchType::Exact);
349        assert!(pattern.is_inverse);
350
351        let pattern = Pattern::parse(&['$', '$']).unwrap();
352        assert_eq!(pattern.into_string(), "$");
353        assert_eq!(pattern.match_type, MatchType::SuffixExact);
354    }
355
356    #[test]
357    fn pattern_parse_specials_3() {
358        assert!(Pattern::parse(&['!', '^', '$']).is_none());
359
360        let pattern = Pattern::parse(&['\'', '^', '$']).unwrap();
361        assert_eq!(pattern.into_string(), "^");
362        assert_eq!(pattern.match_type, MatchType::Exact);
363
364        let pattern = Pattern::parse(&['\'', '!', '$']).unwrap();
365        assert_eq!(pattern.into_string(), "!");
366        assert_eq!(pattern.match_type, MatchType::Exact);
367    }
368
369    #[test]
370    fn pattern_parse_specials_4() {
371        let pattern = Pattern::parse(&['\'', '^', '$', '$']).unwrap();
372        assert_eq!(pattern.into_string(), "^$");
373        assert_eq!(pattern.match_type, MatchType::Exact);
374    }
375}