Krolyxon
Commit 6794220

Branches

Tags

Refs →

remagesrcstatefuzzy.rs

77 lines · 2.4 KB · Rust

1//! Fuzzy matching for the palette and filename search (nucleo, smart case).
2
3use nucleo_matcher::pattern::{CaseMatching, Normalization, Pattern};
4use nucleo_matcher::{Config, Matcher, Utf32Str};
5
6pub struct Fuzzy {
7 matcher: Matcher,
8 pattern: Pattern,
9 buf: Vec<char>,
10}
11
12impl std::fmt::Debug for Fuzzy {
13 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
14 f.write_str("Fuzzy")
15 }
16}
17
18impl Fuzzy {
19 pub fn new(query: &str) -> Self {
20 Self {
21 matcher: Matcher::new(Config::DEFAULT),
22 pattern: Pattern::parse(query, CaseMatching::Smart, Normalization::Smart),
23 buf: Vec::new(),
24 }
25 }
26
27 /// Like [`Fuzzy::new`], tuned for paths (separators score as word boundaries).
28 pub fn for_paths(query: &str) -> Self {
29 let mut config = Config::DEFAULT;
30 config.set_match_paths();
31 Self {
32 matcher: Matcher::new(config),
33 ..Self::new(query)
34 }
35 }
36
37 /// The match score, higher is better; `None` if it doesn't match. An empty query
38 /// matches everything with score 0.
39 pub fn score(&mut self, haystack: &str) -> Option<u32> {
40 let h = Utf32Str::new(haystack, &mut self.buf);
41 self.pattern.score(h, &mut self.matcher)
42 }
43}
44
45/// Indices of `items` matching `query`, best first. Ties go to the shorter item, then keep
46/// their original order (an empty query keeps every item in place).
47pub fn rank<'a>(query: &str, items: impl IntoIterator<Item = &'a str>) -> Vec<usize> {
48 let mut f = Fuzzy::new(query);
49 let empty = query.trim().is_empty();
50 let mut scored: Vec<(u32, usize, usize)> = items
51 .into_iter()
52 .enumerate()
53 .filter_map(|(i, s)| {
54 let len = if empty { 0 } else { s.chars().count() };
55 f.score(s).map(|sc| (sc, len, i))
56 })
57 .collect();
58 scored.sort_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1)).then(a.2.cmp(&b.2)));
59 scored.into_iter().map(|(_, _, i)| i).collect()
60}
61
62#[cfg(test)]
63mod tests {
64 use super::*;
65
66 #[test]
67 fn ranks_best_first() {
68 let items = ["next-mark", "next", "reload-config", "help"];
69 let r = rank("nx", items);
70 assert_eq!(r[0], 1, "{r:?}");
71 assert!(r.contains(&0));
72 assert!(!r.contains(&3));
73 assert_eq!(rank("", items), [0, 1, 2, 3]);
74 // Smart case: an upper-case letter makes it case-sensitive.
75 assert_eq!(rank("HELP", items), Vec::<usize>::new());
76 }
77}