ncp_engine/
pattern.rs

1//! Patterns to prescribe matching behaviour.
2pub use ncp_matcher::pattern::{Atom, AtomKind, CaseMatching, Normalization, Pattern};
3use ncp_matcher::{Matcher, Utf32String};
4
5#[cfg(test)]
6mod tests;
7
8#[derive(Debug, PartialEq, Eq, Clone, Copy, PartialOrd, Ord, Default)]
9pub(crate) enum Status {
10    #[default]
11    Unchanged,
12    Update,
13    Rescore,
14}
15
16/// A list of patterns corresponding to the columns of a [`Nucleo`](crate::Nucleo) instance.
17#[derive(Debug)]
18pub struct MultiPattern {
19    cols: Vec<(Pattern, Status)>,
20}
21
22impl Clone for MultiPattern {
23    fn clone(&self) -> Self {
24        Self {
25            cols: self.cols.clone(),
26        }
27    }
28
29    fn clone_from(&mut self, source: &Self) {
30        self.cols.clone_from(&source.cols);
31    }
32}
33
34impl MultiPattern {
35    /// Creates a new multi-pattern with `columns` empty column patterns.
36    pub fn new(columns: usize) -> Self {
37        Self {
38            cols: vec![Default::default(); columns],
39        }
40    }
41
42    /// Reparses a column. By specifying `append` the caller promises that text passed
43    /// to the previous `reparse` invocation is a prefix of `new_text`. This enables
44    /// additional optimizations but can lead to missing matches if an incorrect value
45    /// is passed.
46    #[allow(clippy::unnecessary_map_or)]
47    pub fn reparse(
48        &mut self,
49        column: usize,
50        new_text: &str,
51        case_matching: CaseMatching,
52        normalization: Normalization,
53        append: bool,
54    ) {
55        let old_status = self.cols[column].1;
56        if append
57            && old_status != Status::Rescore
58                // must be rescored if the atom is negative or if there is an unescaped
59                // trailing `\`
60            && self.cols[column].0.atoms.last().map_or(true, |last| {
61                !last.negative
62                    && last
63                        .needle_text()
64                        .chars()
65                        .rev()
66                        .take_while(|c| *c == '\\')
67                        .count()
68                        % 2
69                        == 0
70            })
71        {
72            self.cols[column].1 = Status::Update;
73        } else {
74            self.cols[column].1 = Status::Rescore;
75        }
76        self.cols[column]
77            .0
78            .reparse(new_text, case_matching, normalization);
79    }
80
81    /// Returns the pattern corresponding to the provided column.
82    pub fn column_pattern(&self, column: usize) -> &Pattern {
83        &self.cols[column].0
84    }
85
86    pub(crate) fn status(&self) -> Status {
87        self.cols
88            .iter()
89            .map(|&(_, status)| status)
90            .max()
91            .unwrap_or(Status::Unchanged)
92    }
93
94    pub(crate) fn reset_status(&mut self) {
95        for (_, status) in &mut self.cols {
96            *status = Status::Unchanged;
97        }
98    }
99
100    /// Returns the score of the haystack corresponding to the pattern.
101    pub fn score(&self, haystack: &[Utf32String], matcher: &mut Matcher) -> Option<u32> {
102        // TODO: weight columns?
103        let mut score = 0;
104        for ((pattern, _), haystack) in self.cols.iter().zip(haystack) {
105            score += pattern.score(haystack.slice(..), matcher)?;
106        }
107        Some(score)
108    }
109
110    /// Returns whether or not all of the patterns are empty.
111    pub fn is_empty(&self) -> bool {
112        self.cols.iter().all(|(pat, _)| pat.atoms.is_empty())
113    }
114}