norm/metrics/fzf/
fzf.rs

1use core::ops::Range;
2
3use super::{query::*, *};
4use crate::utils::CharEq;
5use crate::*;
6
7/// TODO: docs
8pub(super) trait Fzf {
9    /// TODO: docs
10    fn alloc_chars<'a>(&mut self, candidate: &str) -> &'a [char];
11
12    /// TODO: docs
13    fn char_eq(&self, pattern: Pattern) -> CharEq;
14
15    /// TODO: docs
16    fn scheme(&self) -> &Scheme;
17
18    /// TODO: docs
19    fn fuzzy<const RANGES: bool>(
20        &mut self,
21        pattern: Pattern,
22        candidate: Candidate,
23        ranges: &mut MatchedRanges,
24    ) -> Option<Score>;
25
26    /// TODO: docs
27    fn score<const RANGES: bool>(
28        &mut self,
29        pattern: Pattern,
30        candidate: Candidate,
31        ranges: &mut MatchedRanges,
32    ) -> Option<Score> {
33        let score = match pattern.match_type {
34            MatchType::Fuzzy => {
35                if pattern.is_inverse {
36                    self.fuzzy::<false>(pattern, candidate, ranges)
37                } else {
38                    self.fuzzy::<RANGES>(pattern, candidate, ranges)
39                }
40            },
41
42            MatchType::Exact => {
43                let char_eq = self.char_eq(pattern);
44
45                if pattern.is_inverse {
46                    exact_match::<false>(
47                        pattern,
48                        candidate,
49                        char_eq,
50                        self.scheme(),
51                        ranges,
52                    )
53                } else {
54                    exact_match::<RANGES>(
55                        pattern,
56                        candidate,
57                        char_eq,
58                        self.scheme(),
59                        ranges,
60                    )
61                }
62            },
63
64            MatchType::PrefixExact => {
65                let char_eq = self.char_eq(pattern);
66
67                if pattern.is_inverse {
68                    prefix_match::<false>(
69                        pattern,
70                        candidate,
71                        char_eq,
72                        self.scheme(),
73                        ranges,
74                    )
75                } else {
76                    prefix_match::<RANGES>(
77                        pattern,
78                        candidate,
79                        char_eq,
80                        self.scheme(),
81                        ranges,
82                    )
83                }
84            },
85
86            MatchType::SuffixExact => {
87                let char_eq = self.char_eq(pattern);
88
89                if pattern.is_inverse {
90                    suffix_match::<false>(
91                        pattern,
92                        candidate,
93                        char_eq,
94                        self.scheme(),
95                        ranges,
96                    )
97                } else {
98                    suffix_match::<RANGES>(
99                        pattern,
100                        candidate,
101                        char_eq,
102                        self.scheme(),
103                        ranges,
104                    )
105                }
106            },
107
108            MatchType::EqualExact => {
109                let char_eq = self.char_eq(pattern);
110
111                if pattern.is_inverse {
112                    equal_match::<false>(
113                        pattern,
114                        candidate,
115                        char_eq,
116                        self.scheme(),
117                        ranges,
118                    )
119                } else {
120                    equal_match::<RANGES>(
121                        pattern,
122                        candidate,
123                        char_eq,
124                        self.scheme(),
125                        ranges,
126                    )
127                }
128            },
129        };
130
131        match (score.is_some(), pattern.is_inverse) {
132            (true, false) => score,
133            (false, true) => Some(0),
134            _ => None,
135        }
136    }
137
138    /// TODO: docs
139    #[inline(always)]
140    fn distance<const RANGES: bool>(
141        &mut self,
142        query: FzfQuery,
143        candidate: &str,
144        ranges: &mut Vec<Range<usize>>,
145    ) -> Option<FzfDistance> {
146        if query.is_empty() {
147            return Some(FzfDistance::from_score(0));
148        }
149
150        let candidate = if candidate.is_ascii() {
151            Candidate::Ascii(candidate.as_bytes())
152        } else {
153            Candidate::Unicode(self.alloc_chars(candidate))
154        };
155
156        let ranges = &mut ranges.into();
157
158        match query.search_mode {
159            SearchMode::NotExtended(pattern) => self
160                .fuzzy::<RANGES>(pattern, candidate, ranges)
161                .map(FzfDistance::from_score),
162
163            SearchMode::Extended(conditions) => {
164                let mut total_score: Score = 0;
165                for condition in conditions {
166                    total_score += condition.iter().find_map(|pattern| {
167                        self.score::<RANGES>(pattern, candidate, ranges)
168                    })?;
169                }
170                Some(FzfDistance::from_score(total_score))
171            },
172        }
173    }
174}
175
176/// TODO: docs
177#[inline]
178fn exact_match<const RANGES: bool>(
179    pattern: Pattern,
180    candidate: Candidate,
181    char_eq: CharEq,
182    scheme: &Scheme,
183    ranges: &mut MatchedRanges,
184) -> Option<Score> {
185    if pattern.is_empty() {
186        return Some(0);
187    }
188
189    // TODO: docs
190    let mut best_bonus: i64 = -1;
191
192    // TODO: docs
193    let mut best_bonus_char_start = 0;
194
195    // TODO: docs
196    let mut best_bonus_char_end = 0;
197
198    // TODO: docs
199    let mut matched = false;
200
201    let mut prev_class = scheme.initial_char_class;
202
203    let mut start_offset = 0;
204
205    'outer: loop {
206        let current_start_offset = start_offset;
207        let mut bonus_start = 0;
208        let mut current_bonus: Score = 0;
209        let mut pattern_char_idx = 0;
210
211        let mut chars = candidate.chars_from(start_offset).enumerate();
212
213        for (char_offset, candidate_ch) in chars.by_ref() {
214            let pattern_ch = pattern.char(pattern_char_idx);
215
216            let char_class = char_class(candidate_ch, scheme);
217
218            if (char_eq)(pattern_ch, candidate_ch) {
219                if pattern_char_idx == 0 {
220                    bonus_start = current_start_offset + char_offset;
221                    start_offset += char_offset + 1;
222                    current_bonus =
223                        compute_bonus(prev_class, char_class, scheme);
224                }
225
226                pattern_char_idx += 1;
227
228                if pattern_char_idx == pattern.char_len() {
229                    matched = true;
230
231                    if current_bonus as i64 > best_bonus {
232                        best_bonus = current_bonus as _;
233
234                        best_bonus_char_start = bonus_start;
235
236                        best_bonus_char_end =
237                            current_start_offset + char_offset + 1;
238                    }
239
240                    if current_bonus >= bonus::BOUNDARY {
241                        break 'outer;
242                    }
243
244                    break;
245                }
246            } else if pattern_char_idx > 0 {
247                break;
248            }
249
250            prev_class = char_class;
251        }
252
253        if chars.next().is_none() {
254            break;
255        }
256    }
257
258    if !matched {
259        return None;
260    }
261
262    let matched_range = best_bonus_char_start..best_bonus_char_end;
263
264    let score = compute_score::<false>(
265        pattern,
266        candidate,
267        matched_range.clone(),
268        char_eq,
269        scheme,
270        ranges,
271    );
272
273    if RANGES {
274        ranges.insert(candidate.to_byte_range(matched_range));
275    }
276
277    Some(score)
278}
279
280/// TODO: docs
281#[inline]
282fn prefix_match<const RANGES: bool>(
283    pattern: Pattern,
284    candidate: Candidate,
285    char_eq: CharEq,
286    scheme: &Scheme,
287    ranges: &mut MatchedRanges,
288) -> Option<Score> {
289    if pattern.is_empty() {
290        return Some(0);
291    }
292
293    let mut pattern_chars = pattern.chars();
294
295    let ignored_leading_spaces =
296        ignored_candidate_leading_spaces(pattern, candidate)?;
297
298    for (candidate_ch, pattern_ch) in candidate
299        .chars_from(ignored_leading_spaces)
300        .zip(pattern_chars.by_ref())
301    {
302        if !char_eq(pattern_ch, candidate_ch) {
303            return None;
304        }
305    }
306
307    if pattern_chars.next().is_some() {
308        return None;
309    }
310
311    let matched_range = {
312        let start = ignored_leading_spaces;
313        let end = start + pattern.char_len();
314        start..end
315    };
316
317    let score = compute_score::<false>(
318        pattern,
319        candidate,
320        matched_range.clone(),
321        char_eq,
322        scheme,
323        ranges,
324    );
325
326    if RANGES {
327        ranges.insert(candidate.to_byte_range(matched_range));
328    }
329
330    Some(score)
331}
332
333/// TODO: docs
334#[inline]
335fn suffix_match<const RANGES: bool>(
336    pattern: Pattern,
337    candidate: Candidate,
338    char_eq: CharEq,
339    scheme: &Scheme,
340    ranges: &mut MatchedRanges,
341) -> Option<Score> {
342    if pattern.is_empty() {
343        return Some(0);
344    }
345
346    let mut pattern_chars = pattern.chars().rev();
347
348    let chars_up_to_ignored_spaces = candidate.char_len()
349        - ignored_candidate_trailing_spaces(pattern, candidate)?;
350
351    for (candidate_ch, pattern_ch) in candidate
352        .slice(0..chars_up_to_ignored_spaces)
353        .chars()
354        .rev()
355        .zip(pattern_chars.by_ref())
356    {
357        if !char_eq(pattern_ch, candidate_ch) {
358            return None;
359        }
360    }
361
362    if pattern_chars.next().is_some() {
363        return None;
364    }
365
366    let matched_range = {
367        let end = chars_up_to_ignored_spaces;
368        let start = end - pattern.char_len();
369        start..end
370    };
371
372    let score = compute_score::<false>(
373        pattern,
374        candidate,
375        matched_range.clone(),
376        char_eq,
377        scheme,
378        ranges,
379    );
380
381    if RANGES {
382        ranges.insert(candidate.to_byte_range(matched_range));
383    }
384
385    Some(score)
386}
387
388/// TODO: docs
389#[inline]
390fn equal_match<const RANGES: bool>(
391    pattern: Pattern,
392    candidate: Candidate,
393    char_eq: CharEq,
394    scheme: &Scheme,
395    ranges: &mut MatchedRanges,
396) -> Option<Score> {
397    if pattern.is_empty() {
398        return Some(0);
399    }
400
401    let ignored_leading_spaces =
402        ignored_candidate_leading_spaces(pattern, candidate)?;
403
404    // The candidate contains only spaces.
405    if ignored_leading_spaces == candidate.char_len() {
406        return None;
407    }
408
409    let ignored_trailing_spaces =
410        ignored_candidate_trailing_spaces(pattern, candidate)?;
411
412    let matched_char_range =
413        ignored_leading_spaces..candidate.char_len() - ignored_trailing_spaces;
414
415    if matched_char_range.len() < pattern.char_len() {
416        return None;
417    }
418
419    let mut pattern_chars = pattern.chars();
420
421    let mut candidate_chars =
422        candidate.slice(matched_char_range.clone()).chars();
423
424    for (pattern_ch, candidate_ch) in
425        pattern_chars.by_ref().zip(candidate_chars.by_ref())
426    {
427        if !char_eq(pattern_ch, candidate_ch) {
428            return None;
429        }
430    }
431
432    if pattern_chars.next().is_some() || candidate_chars.next().is_some() {
433        return None;
434    }
435
436    let score = compute_score::<false>(
437        pattern,
438        candidate,
439        matched_char_range.clone(),
440        char_eq,
441        scheme,
442        ranges,
443    );
444
445    if RANGES {
446        ranges.insert(candidate.to_byte_range(matched_char_range));
447    }
448
449    Some(score)
450}
451
452/// TODO: docs
453#[inline(always)]
454fn ignored_candidate_leading_spaces(
455    pattern: Pattern,
456    candidate: Candidate,
457) -> Option<usize> {
458    let candidate_leading_spaces = candidate.leading_spaces();
459
460    if pattern.leading_spaces() > candidate_leading_spaces {
461        None
462    } else {
463        Some(candidate_leading_spaces - pattern.leading_spaces())
464    }
465}
466
467/// TODO: docs
468#[inline(always)]
469fn ignored_candidate_trailing_spaces(
470    pattern: Pattern,
471    candidate: Candidate,
472) -> Option<usize> {
473    let candidate_trailing_spaces = candidate.trailing_spaces();
474
475    if pattern.trailing_spaces() > candidate_trailing_spaces {
476        None
477    } else {
478        Some(candidate_trailing_spaces - pattern.trailing_spaces())
479    }
480}
481
482/// TODO: docs
483#[inline]
484pub(super) fn compute_score<const RANGES: bool>(
485    pattern: Pattern,
486    candidate: Candidate,
487    candidate_char_range: Range<usize>,
488    char_eq: CharEq,
489    scheme: &Scheme,
490    ranges: &mut MatchedRanges,
491) -> Score {
492    // TODO: docs
493    let mut is_in_gap = false;
494
495    // TODO: docs
496    let mut is_first_pattern_char = true;
497
498    // TODO: docs
499    let mut first_bonus: Score = 0;
500
501    // TODO: docs
502    let mut consecutive = 0u32;
503
504    let byte_range_start = if RANGES {
505        candidate.to_byte_offset(candidate_char_range.start)
506    } else {
507        0
508    };
509
510    let mut byte_offset = 0;
511
512    let mut prev_class = if candidate_char_range.start == 0 {
513        scheme.initial_char_class
514    } else {
515        char_class(candidate.char(candidate_char_range.start - 1), scheme)
516    };
517
518    let mut pattern_chars = pattern.chars();
519
520    let mut pattern_char = pattern_chars.next().expect("pattern is not empty");
521
522    let mut score: Score = 0;
523
524    for candidate_ch in candidate.slice(candidate_char_range).chars() {
525        let ch_class = char_class(candidate_ch, scheme);
526
527        if char_eq(pattern_char, candidate_ch) {
528            score += bonus::MATCH;
529
530            let mut bonus = compute_bonus(prev_class, ch_class, scheme);
531
532            if consecutive == 0 {
533                first_bonus = bonus;
534            } else {
535                if bonus >= bonus::BOUNDARY && bonus > first_bonus {
536                    first_bonus = bonus
537                }
538                bonus = bonus.max(first_bonus).max(bonus::CONSECUTIVE);
539            }
540
541            score += if is_first_pattern_char {
542                bonus * bonus::FIRST_QUERY_CHAR_MULTIPLIER
543            } else {
544                bonus
545            };
546
547            if RANGES {
548                let start = byte_range_start + byte_offset;
549                let end = start + candidate_ch.len_utf8();
550                ranges.insert(start..end);
551            }
552
553            is_in_gap = false;
554
555            is_first_pattern_char = false;
556
557            consecutive += 1;
558
559            if let Some(next_char) = pattern_chars.next() {
560                pattern_char = next_char;
561            } else {
562                break;
563            };
564        } else {
565            score -= if is_in_gap {
566                penalty::GAP_EXTENSION
567            } else {
568                penalty::GAP_START
569            };
570
571            is_in_gap = true;
572
573            consecutive = 0;
574
575            first_bonus = 0;
576        }
577
578        prev_class = ch_class;
579
580        if RANGES {
581            byte_offset += candidate_ch.len_utf8();
582        }
583    }
584
585    score
586}
587
588#[cfg(test)]
589mod tests {
590    #![allow(clippy::single_range_in_vec_init)]
591
592    use super::*;
593
594    fn candidate(s: &str) -> Candidate {
595        assert!(s.is_ascii());
596        Candidate::Ascii(s.as_bytes())
597    }
598
599    #[test]
600    fn equal_match_1() {
601        let pattern =
602            Pattern::parse("^AbC$".chars().collect::<Vec<_>>().leak())
603                .unwrap();
604
605        let mut ranges_buf = Vec::new();
606
607        assert!(exact_match::<true>(
608            pattern,
609            candidate("ABC"),
610            utils::char_eq(true, false),
611            &Scheme::default(),
612            &mut ((&mut ranges_buf).into())
613        )
614        .is_none());
615
616        {
617            ranges_buf.clear();
618
619            assert!(exact_match::<true>(
620                pattern,
621                candidate("AbC"),
622                utils::char_eq(true, false),
623                &Scheme::default(),
624                &mut ((&mut ranges_buf).into())
625            )
626            .is_some());
627
628            assert_eq!(ranges_buf.as_slice(), [0..3]);
629        }
630
631        {
632            ranges_buf.clear();
633
634            assert!(exact_match::<true>(
635                pattern,
636                candidate("AbC "),
637                utils::char_eq(true, false),
638                &Scheme::default(),
639                &mut ((&mut ranges_buf).into())
640            )
641            .is_some());
642
643            assert_eq!(ranges_buf.as_slice(), [0..3]);
644        }
645
646        {
647            ranges_buf.clear();
648
649            assert!(exact_match::<true>(
650                pattern,
651                candidate(" AbC "),
652                utils::char_eq(true, false),
653                &Scheme::default(),
654                &mut ((&mut ranges_buf).into())
655            )
656            .is_some());
657
658            assert_eq!(ranges_buf.as_slice(), [1..4]);
659        }
660
661        {
662            ranges_buf.clear();
663
664            assert!(exact_match::<true>(
665                pattern,
666                candidate("  AbC"),
667                utils::char_eq(true, false),
668                &Scheme::default(),
669                &mut ((&mut ranges_buf).into())
670            )
671            .is_some());
672
673            assert_eq!(ranges_buf.as_slice(), [2..5]);
674        }
675    }
676
677    #[test]
678    fn exact_match_1() {
679        let pattern =
680            Pattern::parse("abc".chars().collect::<Vec<_>>().leak()).unwrap();
681
682        let mut ranges_buf = Vec::new();
683
684        assert!(exact_match::<true>(
685            pattern,
686            candidate("aabbcc abc"),
687            utils::char_eq(true, false),
688            &Scheme::default(),
689            &mut ((&mut ranges_buf).into())
690        )
691        .is_some());
692
693        assert_eq!(ranges_buf, [7..10]);
694    }
695}