egui_charts/ui_kit/command_palette/
fuzzy.rs1#[derive(Debug, Clone)]
5pub struct FuzzyMatch {
6 pub score: i32,
8 pub matched_indices: Vec<usize>,
10}
11
12pub fn fuzzy_match(query: &str, target: &str) -> Option<FuzzyMatch> {
17 if query.is_empty() {
18 return Some(FuzzyMatch {
19 score: 0,
20 matched_indices: Vec::new(),
21 });
22 }
23
24 let query_lower: Vec<char> = query.to_lowercase().chars().collect();
25 let target_lower: Vec<char> = target.to_lowercase().chars().collect();
26
27 let mut score = 0i32;
28 let mut matched_indices = Vec::new();
29 let mut query_idx = 0;
30 let mut prev_matched = false;
31 let mut prev_was_separator = true; for (target_idx, &target_char) in target_lower.iter().enumerate() {
34 let is_separator = target_char == ' ' || target_char == '_' || target_char == '-';
35
36 if query_idx < query_lower.len() && target_char == query_lower[query_idx] {
37 matched_indices.push(target_idx);
38 query_idx += 1;
39
40 if prev_matched {
42 score += 15;
44 }
45 if prev_was_separator {
46 score += 20;
48 }
49 if target_idx == 0 {
50 score += 25;
52 }
53
54 score += 10;
56 prev_matched = true;
57 } else {
58 prev_matched = false;
59 }
60
61 prev_was_separator = is_separator;
62 }
63
64 if query_idx == query_lower.len() {
66 let unmatched_count = target_lower.len() - matched_indices.len();
68 score -= unmatched_count as i32;
69
70 if matched_indices.len() == target_lower.len() {
72 score += 50;
73 }
74
75 Some(FuzzyMatch {
76 score,
77 matched_indices,
78 })
79 } else {
80 None
81 }
82}
83
84pub fn fuzzy_search<'a, T>(
86 query: &str,
87 items: &'a [T],
88 get_text: impl Fn(&T) -> &str,
89) -> Vec<(&'a T, FuzzyMatch)> {
90 let mut matches: Vec<_> = items
91 .iter()
92 .filter_map(|item| fuzzy_match(query, get_text(item)).map(|m| (item, m)))
93 .collect();
94
95 matches.sort_by_key(|m| std::cmp::Reverse(m.1.score));
97
98 matches
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104
105 #[test]
106 fn matches_subsequences_case_insensitively() {
107 for (query, target, indices) in [
108 ("", "hello", Some(&[][..])),
109 ("hello", "hello", Some(&[0, 1, 2, 3, 4][..])),
110 ("nch", "new chart", Some(&[0, 4, 5][..])),
111 ("xyz", "hello", None),
112 ("HELLO", "hello", Some(&[0, 1, 2, 3, 4][..])),
113 ] {
114 let result = fuzzy_match(query, target);
115 assert_eq!(
116 result.as_ref().map(|m| m.matched_indices.as_slice()),
117 indices,
118 "{query:?} in {target:?}"
119 );
120 if query.is_empty() {
121 assert_eq!(result.unwrap().score, 0);
122 }
123 }
124 }
125
126 #[test]
127 fn test_word_boundary_bonus() {
128 let score_nc = fuzzy_match("nc", "New Chart").unwrap().score;
131 let score_branch = fuzzy_match("nc", "branch").unwrap().score;
132 assert!(score_nc > score_branch);
133 }
134}