/rust/registry/src/index.crates.io-1949cf8c6b5b557f/fuzzy-matcher-0.3.7/src/clangd.rs
Line | Count | Source |
1 | | ///! The fuzzy matching algorithm used in clangd. |
2 | | ///! https://github.com/llvm-mirror/clang-tools-extra/blob/master/clangd/FuzzyMatch.cpp |
3 | | ///! |
4 | | ///! # Example: |
5 | | ///! ```edition2018 |
6 | | ///! use fuzzy_matcher::FuzzyMatcher; |
7 | | ///! use fuzzy_matcher::clangd::ClangdMatcher; |
8 | | ///! |
9 | | ///! let matcher = ClangdMatcher::default(); |
10 | | ///! |
11 | | ///! assert_eq!(None, matcher.fuzzy_match("abc", "abx")); |
12 | | ///! assert!(matcher.fuzzy_match("axbycz", "abc").is_some()); |
13 | | ///! assert!(matcher.fuzzy_match("axbycz", "xyz").is_some()); |
14 | | ///! |
15 | | ///! let (score, indices) = matcher.fuzzy_indices("axbycz", "abc").unwrap(); |
16 | | ///! assert_eq!(indices, [0, 2, 4]); |
17 | | ///! |
18 | | ///! ``` |
19 | | ///! |
20 | | ///! Algorithm modified from |
21 | | ///! https://github.com/llvm-mirror/clang-tools-extra/blob/master/clangd/FuzzyMatch.cpp |
22 | | ///! Also check: https://github.com/lewang/flx/issues/98 |
23 | | use crate::util::*; |
24 | | use crate::{FuzzyMatcher, IndexType, ScoreType}; |
25 | | use std::cell::RefCell; |
26 | | use std::cmp::max; |
27 | | use thread_local::CachedThreadLocal; |
28 | | |
29 | | #[derive(Eq, PartialEq, Debug, Copy, Clone)] |
30 | | enum CaseMatching { |
31 | | Respect, |
32 | | Ignore, |
33 | | Smart, |
34 | | } |
35 | | |
36 | | pub struct ClangdMatcher { |
37 | | case: CaseMatching, |
38 | | |
39 | | use_cache: bool, |
40 | | |
41 | | c_cache: CachedThreadLocal<RefCell<Vec<char>>>, // vector to store the characters of choice |
42 | | p_cache: CachedThreadLocal<RefCell<Vec<char>>>, // vector to store the characters of pattern |
43 | | } |
44 | | |
45 | | impl Default for ClangdMatcher { |
46 | 0 | fn default() -> Self { |
47 | 0 | Self { |
48 | 0 | case: CaseMatching::Ignore, |
49 | 0 | use_cache: true, |
50 | 0 | c_cache: CachedThreadLocal::new(), |
51 | 0 | p_cache: CachedThreadLocal::new(), |
52 | 0 | } |
53 | 0 | } |
54 | | } |
55 | | |
56 | | impl ClangdMatcher { |
57 | 0 | pub fn ignore_case(mut self) -> Self { |
58 | 0 | self.case = CaseMatching::Ignore; |
59 | 0 | self |
60 | 0 | } |
61 | | |
62 | 0 | pub fn smart_case(mut self) -> Self { |
63 | 0 | self.case = CaseMatching::Smart; |
64 | 0 | self |
65 | 0 | } |
66 | | |
67 | 0 | pub fn respect_case(mut self) -> Self { |
68 | 0 | self.case = CaseMatching::Respect; |
69 | 0 | self |
70 | 0 | } |
71 | | |
72 | 0 | pub fn use_cache(mut self, use_cache: bool) -> Self { |
73 | 0 | self.use_cache = use_cache; |
74 | 0 | self |
75 | 0 | } |
76 | | |
77 | 0 | fn contains_upper(&self, string: &str) -> bool { |
78 | 0 | for ch in string.chars() { |
79 | 0 | if ch.is_ascii_uppercase() { |
80 | 0 | return true; |
81 | 0 | } |
82 | | } |
83 | | |
84 | 0 | false |
85 | 0 | } |
86 | | |
87 | 0 | fn is_case_sensitive(&self, pattern: &str) -> bool { |
88 | 0 | match self.case { |
89 | 0 | CaseMatching::Respect => true, |
90 | 0 | CaseMatching::Ignore => false, |
91 | 0 | CaseMatching::Smart => self.contains_upper(pattern), |
92 | | } |
93 | 0 | } |
94 | | } |
95 | | |
96 | | impl FuzzyMatcher for ClangdMatcher { |
97 | 0 | fn fuzzy_indices(&self, choice: &str, pattern: &str) -> Option<(ScoreType, Vec<IndexType>)> { |
98 | 0 | let case_sensitive = self.is_case_sensitive(pattern); |
99 | | |
100 | 0 | let mut choice_chars = self |
101 | 0 | .c_cache |
102 | 0 | .get_or(|| RefCell::new(Vec::new())) |
103 | 0 | .borrow_mut(); |
104 | 0 | let mut pattern_chars = self |
105 | 0 | .p_cache |
106 | 0 | .get_or(|| RefCell::new(Vec::new())) |
107 | 0 | .borrow_mut(); |
108 | | |
109 | 0 | choice_chars.clear(); |
110 | 0 | for char in choice.chars() { |
111 | 0 | choice_chars.push(char); |
112 | 0 | } |
113 | | |
114 | 0 | pattern_chars.clear(); |
115 | 0 | for char in pattern.chars() { |
116 | 0 | pattern_chars.push(char); |
117 | 0 | } |
118 | | |
119 | 0 | if cheap_matches(&choice_chars, &pattern_chars, case_sensitive).is_none() { |
120 | 0 | return None; |
121 | 0 | } |
122 | | |
123 | 0 | let num_pattern_chars = pattern_chars.len(); |
124 | 0 | let num_choice_chars = choice_chars.len(); |
125 | | |
126 | 0 | let dp = build_graph(&choice_chars, &pattern_chars, false, case_sensitive); |
127 | | |
128 | | // search backwards for the matched indices |
129 | 0 | let mut indices_reverse = Vec::with_capacity(num_pattern_chars); |
130 | 0 | let cell = dp[num_pattern_chars][num_choice_chars]; |
131 | | |
132 | 0 | let (mut last_action, score) = if cell.match_score > cell.miss_score { |
133 | 0 | (Action::Match, cell.match_score) |
134 | | } else { |
135 | 0 | (Action::Miss, cell.miss_score) |
136 | | }; |
137 | | |
138 | 0 | let mut row = num_pattern_chars; |
139 | 0 | let mut col = num_choice_chars; |
140 | | |
141 | 0 | while row > 0 || col > 0 { |
142 | 0 | if last_action == Action::Match { |
143 | 0 | indices_reverse.push((col - 1) as IndexType); |
144 | 0 | } |
145 | | |
146 | 0 | let cell = &dp[row][col]; |
147 | 0 | if last_action == Action::Match { |
148 | 0 | last_action = cell.last_action_match; |
149 | 0 | row -= 1; |
150 | 0 | col -= 1; |
151 | 0 | } else { |
152 | 0 | last_action = cell.last_action_miss; |
153 | 0 | col -= 1; |
154 | 0 | } |
155 | | } |
156 | | |
157 | 0 | if !self.use_cache { |
158 | | // drop the allocated memory |
159 | 0 | self.c_cache.get().map(|cell| cell.replace(vec![])); |
160 | 0 | self.p_cache.get().map(|cell| cell.replace(vec![])); |
161 | 0 | } |
162 | | |
163 | 0 | indices_reverse.reverse(); |
164 | 0 | Some((adjust_score(score, num_choice_chars), indices_reverse)) |
165 | 0 | } |
166 | | |
167 | 0 | fn fuzzy_match(&self, choice: &str, pattern: &str) -> Option<ScoreType> { |
168 | 0 | let case_sensitive = self.is_case_sensitive(pattern); |
169 | | |
170 | 0 | let mut choice_chars = self |
171 | 0 | .c_cache |
172 | 0 | .get_or(|| RefCell::new(Vec::new())) |
173 | 0 | .borrow_mut(); |
174 | 0 | let mut pattern_chars = self |
175 | 0 | .p_cache |
176 | 0 | .get_or(|| RefCell::new(Vec::new())) |
177 | 0 | .borrow_mut(); |
178 | | |
179 | 0 | choice_chars.clear(); |
180 | 0 | for char in choice.chars() { |
181 | 0 | choice_chars.push(char); |
182 | 0 | } |
183 | | |
184 | 0 | pattern_chars.clear(); |
185 | 0 | for char in pattern.chars() { |
186 | 0 | pattern_chars.push(char); |
187 | 0 | } |
188 | | |
189 | 0 | if cheap_matches(&choice_chars, &pattern_chars, case_sensitive).is_none() { |
190 | 0 | return None; |
191 | 0 | } |
192 | | |
193 | 0 | let num_pattern_chars = pattern_chars.len(); |
194 | 0 | let num_choice_chars = choice_chars.len(); |
195 | | |
196 | 0 | let dp = build_graph(&choice_chars, &pattern_chars, true, case_sensitive); |
197 | | |
198 | 0 | let cell = dp[num_pattern_chars & 1][num_choice_chars]; |
199 | 0 | let score = max(cell.match_score, cell.miss_score); |
200 | | |
201 | 0 | if !self.use_cache { |
202 | | // drop the allocated memory |
203 | 0 | self.c_cache.get().map(|cell| cell.replace(vec![])); |
204 | 0 | self.p_cache.get().map(|cell| cell.replace(vec![])); |
205 | 0 | } |
206 | | |
207 | 0 | Some(adjust_score(score, num_choice_chars)) |
208 | 0 | } |
209 | | } |
210 | | |
211 | | /// fuzzy match `line` with `pattern`, returning the score and indices of matches |
212 | 0 | pub fn fuzzy_indices(line: &str, pattern: &str) -> Option<(ScoreType, Vec<IndexType>)> { |
213 | 0 | ClangdMatcher::default() |
214 | 0 | .ignore_case() |
215 | 0 | .fuzzy_indices(line, pattern) |
216 | 0 | } |
217 | | |
218 | | /// fuzzy match `line` with `pattern`, returning the score(the larger the better) on match |
219 | 0 | pub fn fuzzy_match(line: &str, pattern: &str) -> Option<ScoreType> { |
220 | 0 | ClangdMatcher::default() |
221 | 0 | .ignore_case() |
222 | 0 | .fuzzy_match(line, pattern) |
223 | 0 | } |
224 | | |
225 | | // checkout https://github.com/llvm-mirror/clang-tools-extra/blob/master/clangd/FuzzyMatch.cpp |
226 | | // for the description |
227 | 0 | fn build_graph( |
228 | 0 | line: &[char], |
229 | 0 | pattern: &[char], |
230 | 0 | compressed: bool, |
231 | 0 | case_sensitive: bool, |
232 | 0 | ) -> Vec<Vec<Score>> { |
233 | 0 | let num_line_chars = line.len(); |
234 | 0 | let num_pattern_chars = pattern.len(); |
235 | 0 | let max_rows = if compressed { 2 } else { num_pattern_chars + 1 }; |
236 | | |
237 | 0 | let mut dp: Vec<Vec<Score>> = Vec::with_capacity(max_rows); |
238 | | |
239 | 0 | for _ in 0..max_rows { |
240 | 0 | dp.push(vec![Score::default(); num_line_chars + 1]); |
241 | 0 | } |
242 | | |
243 | 0 | dp[0][0].miss_score = 0; |
244 | | |
245 | | // first line |
246 | 0 | for (idx, &ch) in line.iter().enumerate() { |
247 | 0 | dp[0][idx + 1] = Score { |
248 | 0 | miss_score: dp[0][idx].miss_score - skip_penalty(idx, ch, Action::Miss), |
249 | 0 | last_action_miss: Action::Miss, |
250 | 0 | match_score: AWFUL_SCORE, |
251 | 0 | last_action_match: Action::Miss, |
252 | 0 | }; |
253 | 0 | } |
254 | | |
255 | | // build the matrix |
256 | 0 | let mut pat_prev_ch = '\0'; |
257 | 0 | for (pat_idx, &pat_ch) in pattern.iter().enumerate() { |
258 | 0 | let current_row_idx = if compressed { |
259 | 0 | (pat_idx + 1) & 1 |
260 | | } else { |
261 | 0 | pat_idx + 1 |
262 | | }; |
263 | 0 | let prev_row_idx = if compressed { pat_idx & 1 } else { pat_idx }; |
264 | | |
265 | 0 | let mut line_prev_ch = '\0'; |
266 | 0 | for (line_idx, &line_ch) in line.iter().enumerate() { |
267 | 0 | if line_idx < pat_idx { |
268 | 0 | line_prev_ch = line_ch; |
269 | 0 | continue; |
270 | 0 | } |
271 | | |
272 | | // what if we skip current line character? |
273 | | // we need to calculate the cases where the pre line character is matched/missed |
274 | 0 | let pre_miss = &dp[current_row_idx][line_idx]; |
275 | 0 | let mut match_miss_score = pre_miss.match_score; |
276 | 0 | let mut miss_miss_score = pre_miss.miss_score; |
277 | 0 | if pat_idx < num_pattern_chars - 1 { |
278 | 0 | match_miss_score -= skip_penalty(line_idx, line_ch, Action::Match); |
279 | 0 | miss_miss_score -= skip_penalty(line_idx, line_ch, Action::Miss); |
280 | 0 | } |
281 | | |
282 | 0 | let (miss_score, last_action_miss) = if match_miss_score > miss_miss_score { |
283 | 0 | (match_miss_score, Action::Match) |
284 | | } else { |
285 | 0 | (miss_miss_score, Action::Miss) |
286 | | }; |
287 | | |
288 | | // what if we want to match current line character? |
289 | | // so we need to calculate the cases where the pre pattern character is matched/missed |
290 | 0 | let pre_match = &dp[prev_row_idx][line_idx]; |
291 | 0 | let match_match_score = if allow_match(pat_ch, line_ch, case_sensitive) { |
292 | 0 | pre_match.match_score |
293 | 0 | + match_bonus( |
294 | 0 | pat_idx, |
295 | 0 | pat_ch, |
296 | 0 | pat_prev_ch, |
297 | 0 | line_idx, |
298 | 0 | line_ch, |
299 | 0 | line_prev_ch, |
300 | 0 | Action::Match, |
301 | 0 | ) |
302 | | } else { |
303 | 0 | AWFUL_SCORE |
304 | | }; |
305 | | |
306 | 0 | let miss_match_score = if allow_match(pat_ch, line_ch, case_sensitive) { |
307 | 0 | pre_match.miss_score |
308 | 0 | + match_bonus( |
309 | 0 | pat_idx, |
310 | 0 | pat_ch, |
311 | 0 | pat_prev_ch, |
312 | 0 | line_idx, |
313 | 0 | line_ch, |
314 | 0 | line_prev_ch, |
315 | 0 | Action::Match, |
316 | 0 | ) |
317 | | } else { |
318 | 0 | AWFUL_SCORE |
319 | | }; |
320 | | |
321 | 0 | let (match_score, last_action_match) = if match_match_score > miss_match_score { |
322 | 0 | (match_match_score, Action::Match) |
323 | | } else { |
324 | 0 | (miss_match_score, Action::Miss) |
325 | | }; |
326 | | |
327 | 0 | dp[current_row_idx][line_idx + 1] = Score { |
328 | 0 | miss_score, |
329 | 0 | last_action_miss, |
330 | 0 | match_score, |
331 | 0 | last_action_match, |
332 | 0 | }; |
333 | | |
334 | 0 | line_prev_ch = line_ch; |
335 | | } |
336 | | |
337 | 0 | pat_prev_ch = pat_ch; |
338 | | } |
339 | | |
340 | 0 | dp |
341 | 0 | } |
342 | | |
343 | 0 | fn adjust_score(score: ScoreType, num_line_chars: usize) -> ScoreType { |
344 | | // line width will affect 10 scores |
345 | 0 | score - (((num_line_chars + 1) as f64).ln().floor() as ScoreType) |
346 | 0 | } |
347 | | |
348 | | const AWFUL_SCORE: ScoreType = -(1 << 30); |
349 | | |
350 | | #[derive(Debug, PartialEq, Clone, Copy)] |
351 | | enum Action { |
352 | | Miss, |
353 | | Match, |
354 | | } |
355 | | |
356 | | #[derive(Debug, Clone, Copy)] |
357 | | struct Score { |
358 | | pub last_action_miss: Action, |
359 | | pub last_action_match: Action, |
360 | | pub miss_score: ScoreType, |
361 | | pub match_score: ScoreType, |
362 | | } |
363 | | |
364 | | impl Default for Score { |
365 | 0 | fn default() -> Self { |
366 | 0 | Self { |
367 | 0 | last_action_miss: Action::Miss, |
368 | 0 | last_action_match: Action::Miss, |
369 | 0 | miss_score: AWFUL_SCORE, |
370 | 0 | match_score: AWFUL_SCORE, |
371 | 0 | } |
372 | 0 | } |
373 | | } |
374 | | |
375 | 0 | fn skip_penalty(_ch_idx: usize, ch: char, last_action: Action) -> ScoreType { |
376 | 0 | let mut score = 1; |
377 | 0 | if last_action == Action::Match { |
378 | 0 | // Non-consecutive match. |
379 | 0 | score += 3; |
380 | 0 | } |
381 | | |
382 | 0 | if char_type_of(ch) == CharType::NonWord { |
383 | 0 | // skip separator |
384 | 0 | score += 6; |
385 | 0 | } |
386 | | |
387 | 0 | score |
388 | 0 | } |
389 | | |
390 | 0 | fn allow_match(pat_ch: char, line_ch: char, case_sensitive: bool) -> bool { |
391 | 0 | char_equal(pat_ch, line_ch, case_sensitive) |
392 | 0 | } |
393 | | |
394 | 0 | fn match_bonus( |
395 | 0 | pat_idx: usize, |
396 | 0 | pat_ch: char, |
397 | 0 | pat_prev_ch: char, |
398 | 0 | line_idx: usize, |
399 | 0 | line_ch: char, |
400 | 0 | line_prev_ch: char, |
401 | 0 | last_action: Action, |
402 | 0 | ) -> ScoreType { |
403 | 0 | let mut score = 10; |
404 | 0 | let pat_role = char_role(pat_prev_ch, pat_ch); |
405 | 0 | let line_role = char_role(line_prev_ch, line_ch); |
406 | | |
407 | | // Bonus: pattern so far is a (case-insensitive) prefix of the word. |
408 | 0 | if pat_idx == line_idx { |
409 | 0 | score += 10; |
410 | 0 | } |
411 | | |
412 | | // Bonus: case match |
413 | 0 | if pat_ch == line_ch { |
414 | 0 | score += 8; |
415 | 0 | } |
416 | | |
417 | | // Bonus: match header |
418 | 0 | if line_role == CharRole::Head { |
419 | 0 | score += 9; |
420 | 0 | } |
421 | | |
422 | | // Bonus: a Head in the pattern aligns with one in the word. |
423 | 0 | if pat_role == CharRole::Head && line_role == CharRole::Head { |
424 | 0 | score += 10; |
425 | 0 | } |
426 | | |
427 | | // Penalty: matching inside a segment (and previous char wasn't matched). |
428 | 0 | if line_role == CharRole::Tail && pat_idx > 0 && last_action == Action::Miss { |
429 | 0 | score -= 30; |
430 | 0 | } |
431 | | |
432 | | // Penalty: a Head in the pattern matches in the middle of a word segment. |
433 | 0 | if pat_role == CharRole::Head && line_role == CharRole::Tail { |
434 | 0 | score -= 10; |
435 | 0 | } |
436 | | |
437 | | // Penalty: matching the first pattern character in the middle of a segment. |
438 | 0 | if pat_idx == 0 && line_role == CharRole::Tail { |
439 | 0 | score -= 40; |
440 | 0 | } |
441 | | |
442 | 0 | score |
443 | 0 | } |
444 | | |
445 | | #[cfg(test)] |
446 | | mod tests { |
447 | | use super::*; |
448 | | use crate::util::{assert_order, wrap_matches}; |
449 | | |
450 | | fn wrap_fuzzy_match(line: &str, pattern: &str) -> Option<String> { |
451 | | let (_score, indices) = fuzzy_indices(line, pattern)?; |
452 | | Some(wrap_matches(line, &indices)) |
453 | | } |
454 | | |
455 | | #[test] |
456 | | fn test_match_or_not() { |
457 | | assert_eq!(None, fuzzy_match("abcdefaghi", "中")); |
458 | | assert_eq!(None, fuzzy_match("abc", "abx")); |
459 | | assert!(fuzzy_match("axbycz", "abc").is_some()); |
460 | | assert!(fuzzy_match("axbycz", "xyz").is_some()); |
461 | | |
462 | | assert_eq!("[a]x[b]y[c]z", &wrap_fuzzy_match("axbycz", "abc").unwrap()); |
463 | | assert_eq!("a[x]b[y]c[z]", &wrap_fuzzy_match("axbycz", "xyz").unwrap()); |
464 | | assert_eq!( |
465 | | "[H]ello, [世]界", |
466 | | &wrap_fuzzy_match("Hello, 世界", "H世").unwrap() |
467 | | ); |
468 | | } |
469 | | |
470 | | #[test] |
471 | | fn test_match_quality() { |
472 | | let matcher = ClangdMatcher::default(); |
473 | | // case |
474 | | assert_order(&matcher, "monad", &["monad", "Monad", "mONAD"]); |
475 | | |
476 | | // initials |
477 | | assert_order(&matcher, "ab", &["ab", "aoo_boo", "acb"]); |
478 | | assert_order(&matcher, "CC", &["CamelCase", "camelCase", "camelcase"]); |
479 | | assert_order(&matcher, "cC", &["camelCase", "CamelCase", "camelcase"]); |
480 | | assert_order( |
481 | | &matcher, |
482 | | "cc", |
483 | | &[ |
484 | | "camel case", |
485 | | "camelCase", |
486 | | "camelcase", |
487 | | "CamelCase", |
488 | | "camel ace", |
489 | | ], |
490 | | ); |
491 | | assert_order( |
492 | | &matcher, |
493 | | "Da.Te", |
494 | | &["Data.Text", "Data.Text.Lazy", "Data.Aeson.Encoding.text"], |
495 | | ); |
496 | | assert_order(&matcher, "foobar.h", &["foobar.h", "foo/bar.h"]); |
497 | | // prefix |
498 | | assert_order(&matcher, "is", &["isIEEE", "inSuf"]); |
499 | | // shorter |
500 | | assert_order(&matcher, "ma", &["map", "many", "maximum"]); |
501 | | assert_order(&matcher, "print", &["printf", "sprintf"]); |
502 | | // score(PRINT) = kMinScore |
503 | | assert_order(&matcher, "ast", &["ast", "AST", "INT_FAST16_MAX"]); |
504 | | // score(PRINT) > kMinScore |
505 | | assert_order(&matcher, "Int", &["int", "INT", "PRINT"]); |
506 | | } |
507 | | } |
508 | | |
509 | | #[allow(dead_code)] |
510 | 0 | fn print_dp(line: &str, pattern: &str, dp: &[Vec<Score>]) { |
511 | 0 | let num_line_chars = line.chars().count(); |
512 | 0 | let num_pattern_chars = pattern.chars().count(); |
513 | | |
514 | 0 | print!("\t"); |
515 | 0 | for (idx, ch) in line.chars().enumerate() { |
516 | 0 | print!("\t\t{}/{}", idx + 1, ch); |
517 | 0 | } |
518 | | |
519 | 0 | for (row_num, row) in dp.iter().enumerate().take(num_pattern_chars + 1) { |
520 | 0 | print!("\n{}\t", row_num); |
521 | 0 | for cell in row.iter().take(num_line_chars + 1) { |
522 | 0 | print!( |
523 | | "({},{})/({},{})\t", |
524 | | cell.miss_score, |
525 | 0 | if cell.last_action_miss == Action::Miss { |
526 | 0 | 'X' |
527 | | } else { |
528 | 0 | 'O' |
529 | | }, |
530 | | cell.match_score, |
531 | 0 | if cell.last_action_match == Action::Miss { |
532 | 0 | 'X' |
533 | | } else { |
534 | 0 | 'O' |
535 | | } |
536 | | ); |
537 | | } |
538 | | } |
539 | 0 | } |