@@ -4,41 +4,209 @@ use fuzzy_matcher::FuzzyMatcher;
44#[cfg(test)]
55mod test;
66
7+struct QueryAtom {
8+ kind: QueryAtomKind,
9+ atom: String,
10+ ignore_case: bool,
11+ inverse: bool,
12+}
13+impl QueryAtom {
14+ fn new(atom: &str) -> Option<QueryAtom> {
15+ let mut atom = atom.to_string();
16+ let inverse = atom.starts_with('!');
17+ if inverse {
18+ atom.remove(0);
19+ }
20+
21+ let mut kind = match atom.chars().next() {
22+ Some('^') => QueryAtomKind::Prefix,
23+ Some('\'') => QueryAtomKind::Substring,
24+ _ if inverse => QueryAtomKind::Substring,
25+ _ => QueryAtomKind::Fuzzy,
26+ };
27+
28+ if atom.starts_with(&['^', '\'']) {
29+ atom.remove(0);
30+ }
31+
32+ if atom.is_empty() {
33+ return None;
34+ }
35+
36+ if atom.ends_with('$') && !atom.ends_with("\\$") {
37+ atom.pop();
38+ kind = if kind == QueryAtomKind::Prefix {
39+ QueryAtomKind::Exact
40+ } else {
41+ QueryAtomKind::Postfix
42+ }
43+ }
44+
45+ Some(QueryAtom {
46+ kind,
47+ atom: atom.replace('\\', ""),
48+ // not ideal but fuzzy_matches only knows ascii uppercase so more consistent
49+ // to behave the same
50+ ignore_case: kind != QueryAtomKind::Fuzzy
51+ && atom.chars().all(|c| c.is_ascii_lowercase()),
52+ inverse,
53+ })
54+ }
55+
56+ fn indices(&self, matcher: &Matcher, item: &str, indices: &mut Vec<usize>) -> bool {
57+ // for inverse there are no indicies to return
58+ // just return whether we matched
59+ if self.inverse {
60+ return self.matches(matcher, item);
61+ }
62+ let buf;
63+ let item = if self.ignore_case {
64+ buf = item.to_ascii_lowercase();
65+ &buf
66+ } else {
67+ item
68+ };
69+ let off = match self.kind {
70+ QueryAtomKind::Fuzzy => {
71+ if let Some((_, fuzzy_indices)) = matcher.fuzzy_indices(item, &self.atom) {
72+ indices.extend_from_slice(&fuzzy_indices);
73+ return true;
74+ } else {
75+ return false;
76+ }
77+ }
78+ QueryAtomKind::Substring => {
79+ if let Some(off) = item.find(&self.atom) {
80+ off
81+ } else {
82+ return false;
83+ }
84+ }
85+ QueryAtomKind::Prefix if item.starts_with(&self.atom) => 0,
86+ QueryAtomKind::Postfix if item.ends_with(&self.atom) => item.len() - self.atom.len(),
87+ QueryAtomKind::Exact if item == self.atom => 0,
88+ _ => return false,
89+ };
90+
91+ indices.extend(off..(off + self.atom.len()));
92+ true
93+ }
94+
95+ fn matches(&self, matcher: &Matcher, item: &str) -> bool {
96+ let buf;
97+ let item = if self.ignore_case {
98+ buf = item.to_ascii_lowercase();
99+ &buf
100+ } else {
101+ item
102+ };
103+ let mut res = match self.kind {
104+ QueryAtomKind::Fuzzy => matcher.fuzzy_match(item, &self.atom).is_some(),
105+ QueryAtomKind::Substring => item.contains(&self.atom),
106+ QueryAtomKind::Prefix => item.starts_with(&self.atom),
107+ QueryAtomKind::Postfix => item.ends_with(&self.atom),
108+ QueryAtomKind::Exact => item == self.atom,
109+ };
110+ if self.inverse {
111+ res = !res;
112+ }
113+ res
114+ }
115+}
116+
117+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
118+enum QueryAtomKind {
119+ /// Item is a fuzzy match of this behaviour
120+ ///
121+ /// Usage: `foo`
122+ Fuzzy,
123+ /// Item contains query atom as a continous substring
124+ ///
125+ /// Usage `'foo`
126+ Substring,
127+ /// Item starts with query atom
128+ ///
129+ /// Usage: `^foo`
130+ Prefix,
131+ /// Item ends with query atom
132+ ///
133+ /// Usage: `foo$`
134+ Postfix,
135+ /// Item is equal to query atom
136+ ///
137+ /// Usage `^foo$`
138+ Exact,
139+}
140+
141+#[derive(Default)]
7142pub struct FuzzyQuery {
8− queries: Vec<String>,
143+ first_fuzzy_atom: Option<String>,
144+ query_atoms: Vec<QueryAtom>,
145+}
146+
147+fn query_atoms(query: &str) -> impl Iterator<Item = &str> + '_ {
148+ let mut saw_backslash = false;
149+ query.split(move |c| {
150+ saw_backslash = match c {
151+ ' ' if !saw_backslash => return true,
152+ '\\' => true,
153+ _ => false,
154+ };
155+ false
156+ })
9157}
10158
11159impl FuzzyQuery {
160+ pub fn refine(&self, query: &str, old_query: &str) -> (FuzzyQuery, bool) {
161+ // TODO: we could be a lot smarter about this
162+ let new_query = Self::new(query);
163+ let mut is_refinement = query.starts_with(old_query);
164+
165+ // if the last atom is an inverse atom adding more text to it
166+ // will actually increase the number of matches and we can not refine
167+ // the matches.
168+ if is_refinement && !self.query_atoms.is_empty() {
169+ let last_idx = self.query_atoms.len() - 1;
170+ if self.query_atoms[last_idx].inverse
171+ && self.query_atoms[last_idx].atom != new_query.query_atoms[last_idx].atom
172+ {
173+ is_refinement = false;
174+ }
175+ }
176+
177+ (new_query, is_refinement)
178+ }
179+
12180 pub fn new(query: &str) -> FuzzyQuery {
13− let mut saw_backslash = false;
14− let queries = query
15− .split(|c| {
16− saw_backslash = match c {
17− ' ' if !saw_backslash => return true,
18− '\\' => true,
19− _ => false,
20− };
21− false
22− })
23− .filter_map(|query| {
24− if query.is_empty() {
181+ let mut first_fuzzy_query = None;
182+ let query_atoms = query_atoms(query)
183+ .filter_map(|atom| {
184+ let atom = QueryAtom::new(atom)?;
185+ if atom.kind == QueryAtomKind::Fuzzy && first_fuzzy_query.is_none() {
186+ first_fuzzy_query = Some(atom.atom);
25187 None
26188 } else {
27− Some(query.replace("\\ ", " "))
189+ Some(atom)
28190 }
29191 })
30192 .collect();
31− FuzzyQuery { queries }
193+ FuzzyQuery {
194+ first_fuzzy_atom: first_fuzzy_query,
195+ query_atoms,
196+ }
32197 }
33198
34199 pub fn fuzzy_match(&self, item: &str, matcher: &Matcher) -> Option<i64> {
35− // use the rank of the first query for the rank, because merging ranks is not really possible
200+ // use the rank of the first fuzzzy query for the rank, because merging ranks is not really possible
36201 // this behaviour matches fzf and skim
37− let score = matcher.fuzzy_match(item, self.queries.get(0)?)?;
202+ let score = self
203+ .first_fuzzy_atom
204+ .as_ref()
205+ .map_or(Some(0), |atom| matcher.fuzzy_match(item, atom))?;
38206 if self
39− .queries
207+ .query_atoms
40208 .iter()
41− .any(|query| matcher.fuzzy_match(item, query).is_none())
209+ .any(|atom| !atom.matches(matcher, item))
42210 {
43211 return None;
44212 }
@@ -46,29 +214,26 @@ impl FuzzyQuery {
46214 }
47215
48216 pub fn fuzzy_indicies(&self, item: &str, matcher: &Matcher) -> Option<(i64, Vec<usize>)> {
49− if self.queries.len() == 1 {
50− return matcher.fuzzy_indices(item, &self.queries[0]);
51− }
52−
53− // use the rank of the first query for the rank, because merging ranks is not really possible
54− // this behaviour matches fzf and skim
55− let (score, mut indicies) = matcher.fuzzy_indices(item, self.queries.get(0)?)?;
217+ let (score, mut indices) = self.first_fuzzy_atom.as_ref().map_or_else(
218+ || Some((0, Vec::new())),
219+ |atom| matcher.fuzzy_indices(item, atom),
220+ )?;
56221
57− // fast path for the common case of not using a space
58− // during matching this branch should be free thanks to branch prediction
59− if self.queries.len() == 1 {
60− return Some((score, indicies));
222+ // fast path for the common case of just a single atom
223+ if self.query_atoms.is_empty() {
224+ return Some((score, indices));
61225 }
62226
63− for query in &self.queries[1..] {
64− let (_, matched_indicies) = matcher.fuzzy_indices(item, query)?;
65− indicies.extend_from_slice(&matched_indicies);
227+ for atom in &self.query_atoms {
228+ if !atom.indices(matcher, item, &mut indices) {
229+ return None;
230+ }
66231 }
67232
68233 // deadup and remove duplicate matches
69− indicies.sort_unstable();
70− indicies.dedup();
234+ indices.sort_unstable();
235+ indices.dedup();
71236
72− Some((score, indicies))
237+ Some((score, indices))
73238 }
74239}