1use core::fmt::Write;
2
3#[derive(Clone, Copy)]
8pub struct FzfQuery<'a> {
9 pub(super) search_mode: SearchMode<'a>,
10}
11
12#[derive(Clone, Copy)]
14pub(super) enum SearchMode<'a> {
15 Extended(&'a [Condition<'a>]),
17
18 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 #[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 #[inline]
50 pub(super) fn new_extended(conditions: &'a [Condition<'a>]) -> Self {
51 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 #[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#[derive(Default, Clone, Copy)]
81pub(super) struct Condition<'a> {
82 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#[derive(Default, Clone, Copy)]
135#[cfg_attr(test, derive(PartialEq))]
136pub(super) struct Pattern<'a> {
137 text: &'a [char],
139
140 pub(super) has_uppercase: bool,
142
143 pub(super) match_type: MatchType,
145
146 pub(super) is_inverse: bool,
148
149 pub(super) leading_spaces: usize,
151
152 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 #[inline(always)]
166 pub(super) fn char_len(&self) -> usize {
167 self.text.len()
168 }
169
170 #[inline(always)]
172 pub(super) fn char(&self, idx: usize) -> char {
173 self.text[idx]
174 }
175
176 #[inline]
178 pub(crate) fn chars(
179 &self,
180 ) -> impl Iterator<Item = char> + DoubleEndedIterator + '_ {
181 self.text.iter().copied()
182 }
183
184 #[inline]
186 pub(super) fn is_empty(&self) -> bool {
187 self.text.is_empty()
188 }
189
190 #[inline]
192 pub(super) fn into_string(self) -> String {
193 self.text.iter().collect::<String>()
194 }
195
196 #[inline(always)]
198 pub(super) fn leading_spaces(&self) -> usize {
199 self.leading_spaces
200 }
201
202 #[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 #[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 #[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#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
297pub(super) enum MatchType {
298 #[default]
300 Fuzzy,
301
302 Exact,
304
305 PrefixExact,
307
308 SuffixExact,
310
311 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}