1
use fuzzy_matcher::skim::SkimMatcherV2;
2

            
3
/// Encapsulate SkimMatcherV2 to extend it with custom methods.
4
pub struct Matcher {
5
    matcher: SkimMatcherV2,
6
}
7

            
8
impl Matcher {
9
    /// Find a best match for a pattern from choices and return its index.
10
24
    pub fn best_match(&self, choices: &[&str], pattern: &str) -> Option<usize> {
11
24
        choices
12
24
            .iter()
13
24
            .enumerate()
14
            // Calculate fuzzy score for each choice and store it as Option<f64>.
15
100
            .map(|(index, &choice)| {
16
                (
17
88
                    index,
18
88
                    self.matcher
19
88
                        .fuzzy(choice, pattern, false)
20
88
                        .map(|(score, _)| score),
21
                )
22
88
            })
23
            // Filter out None scores and map Some(score) to (index, score) tuple.
24
100
            .filter_map(|(index, score)| score.map(|s| (index, s)))
25
25
            .max_by(|(_, score1), (_, score2)| score1.partial_cmp(score2).unwrap())
26
24
            .map(|(index, _)| index)
27
24
    }
28
}
29

            
30
impl Default for Matcher {
31
    /// Return the default Matcher.
32
28
    fn default() -> Self {
33
28
        Matcher {
34
28
            matcher: SkimMatcherV2::default(),
35
28
        }
36
28
    }
37
}
38

            
39
#[cfg(test)]
40
macro_rules! best_match {
41
    ($($name: ident: $value:expr,)*) => {
42
    $(
43
        #[test]
44
8
        fn $name() {
45
            // GIVEN an array of choices, search pattern & expected index
46
8
            let (choices, pattern, expected) = $value;
47
8
            let matcher = Matcher::default();
48

            
49
            // WHEN getting the best match for the pattern
50
8
            let actual = matcher.best_match(&choices, &pattern);
51

            
52
            // THEN the returned index should match the expected choice's index
53
8
            assert_eq!(expected, actual);
54
8
        }
55
    )*
56
    }
57
}
58

            
59
#[cfg(test)]
60
mod tests {
61
    use super::*;
62

            
63
    best_match! {
64
        best_match_exact: (["Atrox", "Opifex", "Solitus"], "Atrox", Some(0)),
65
        best_match_partial: (["Atrox", "Opifex", "Solitus"], "Opi", Some(1)),
66
        best_match_fuzzy: (["Atrox", "Opifex", "Solitus"], "soli", Some(2)),
67
        best_match_none: (["Atrox", "Opifex", "Solitus"], "Nanomage", None),
68
    }
69
}