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
20
    pub fn best_match(&self, choices: &[&str], pattern: &str) -> Option<usize> {
11
20
        choices
12
20
            .iter()
13
20
            .enumerate()
14
20
            // Calculate fuzzy score for each choice and store it as Option<f64>.
15
82
            .map(|(index, &choice)| {
16
72
                (
17
72
                    index,
18
72
                    self.matcher
19
72
                        .fuzzy(choice, pattern, false)
20
72
                        .map(|(score, _)| score),
21
72
                )
22
82
            })
23
20
            // Filter out None scores and map Some(score) to (index, score) tuple.
24
82
            .filter_map(|(index, score)| score.map(|s| (index, s)))
25
21
            .max_by(|(_, score1), (_, score2)| score1.partial_cmp(score2).unwrap())
26
28
            .map(|(index, _)| index)
27
20
    }
28
}
29

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

            
39
#[cfg(test)]
40
macro_rules! best_match {
41
    ($($name: ident: $value:expr,)*) => {
42
    $(
43
        #[test]
44
        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
8

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

            
52
8
            // 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
8
    best_match! {
64
8
        best_match_exact: (["Atrox", "Opifex", "Solitus"], "Atrox", Some(0)),
65
8
        best_match_partial: (["Atrox", "Opifex", "Solitus"], "Opi", Some(1)),
66
8
        best_match_fuzzy: (["Atrox", "Opifex", "Solitus"], "soli", Some(2)),
67
8
        best_match_none: (["Atrox", "Opifex", "Solitus"], "Nanomage", None),
68
8
    }
69
}