1use nucleo_matcher::{Config, Matcher};
4
5use crate::config::CaseMatching;
6use crate::models::{AliasEntry, SearchResult};
7
8#[derive(Debug, Clone)]
10pub struct SearchOptions {
11 pub case_matching: CaseMatching,
13 pub normalize: bool,
15}
16
17impl Default for SearchOptions {
18 fn default() -> Self {
19 Self {
20 case_matching: CaseMatching::Smart,
21 normalize: true,
22 }
23 }
24}
25
26pub fn fuzzy_search(
36 entries: &[AliasEntry],
37 query: &str,
38 opts: &SearchOptions,
39) -> Vec<SearchResult> {
40 if query.is_empty() {
41 return entries
43 .iter()
44 .map(|entry| SearchResult {
45 entry: entry.clone(),
46 score: 0,
47 })
48 .collect();
49 }
50
51 let mut matcher = create_matcher(opts);
52 let mut results = Vec::new();
53
54 let query_haystack = nucleo_matcher::Utf32Str::Ascii(query.as_bytes());
56
57 for entry in entries {
58 let name_haystack = nucleo_matcher::Utf32Str::Ascii(entry.name.as_bytes());
60 let name_score = matcher.fuzzy_match(name_haystack, query_haystack);
61
62 let comment_score = entry.comments.as_ref().and_then(|comments| {
63 let comment_text = comments.join(" ");
65 let comment_haystack = nucleo_matcher::Utf32Str::Ascii(comment_text.as_bytes());
66 matcher.fuzzy_match(comment_haystack, query_haystack)
67 });
68
69 if let Some(score) = name_score.or(comment_score) {
71 results.push(SearchResult {
72 entry: entry.clone(),
73 score: score as u32,
74 });
75 }
76 }
77
78 results.sort_by_key(|r| std::cmp::Reverse(r.score));
79
80 results
81}
82
83fn create_matcher(_opts: &SearchOptions) -> Matcher {
85 Matcher::new(Config::DEFAULT)
89}
90
91#[cfg(test)]
92mod search_tests;