Skip to main content

osp_cli/core/
fuzzy.rs

1use skim::CaseMatching;
2use skim::fuzzy_matcher::arinae::ArinaeMatcher;
3use std::sync::OnceLock;
4
5/// Lowercases text using Unicode case folding semantics.
6///
7/// This is stricter than ASCII-only lowercasing, so it is safe to use for
8/// case-insensitive matching on user-facing text.
9///
10/// # Examples
11///
12/// ```
13/// use osp_cli::core::fuzzy::fold_case;
14///
15/// assert_eq!(fold_case("LDAP"), "ldap");
16/// assert_eq!(fold_case("ÅSE"), "åse");
17/// ```
18pub fn fold_case(text: &str) -> String {
19    text.chars().flat_map(char::to_lowercase).collect()
20}
21
22/// Conservative fuzzy matcher for completion suggestions.
23///
24/// Completion should rescue near-misses like `lap -> ldap`, but it should not
25/// spill short stubs like `ld` into unrelated commands. Arinae with typo mode
26/// disabled keeps that balance while still handling subsequence-style fuzzy
27/// matches well.
28///
29/// # Examples
30///
31/// ```
32/// use osp_cli::core::fuzzy::completion_fuzzy_matcher;
33/// use skim::fuzzy_matcher::FuzzyMatcher;
34///
35/// assert!(completion_fuzzy_matcher()
36///     .fuzzy_match("ldap", "lap")
37///     .is_some());
38/// ```
39pub fn completion_fuzzy_matcher() -> &'static ArinaeMatcher {
40    static MATCHER: OnceLock<ArinaeMatcher> = OnceLock::new();
41    MATCHER.get_or_init(|| ArinaeMatcher::new(CaseMatching::Smart, false, false))
42}
43
44/// Typo-tolerant fuzzy matcher for config-key recovery suggestions.
45///
46/// Config lookup failures should help with misspellings like
47/// `ui.formt -> ui.format`, but they should still stay narrower than broad
48/// search-oriented matching. Callers are expected to pair this matcher with
49/// explicit ranking such as same-namespace and last-segment preference.
50///
51/// # Examples
52///
53/// ```
54/// use osp_cli::core::fuzzy::config_fuzzy_matcher;
55/// use skim::fuzzy_matcher::FuzzyMatcher;
56///
57/// assert!(config_fuzzy_matcher()
58///     .fuzzy_match("ui.format", "ui.formt")
59///     .is_some());
60/// ```
61pub fn config_fuzzy_matcher() -> &'static ArinaeMatcher {
62    static MATCHER: OnceLock<ArinaeMatcher> = OnceLock::new();
63    MATCHER.get_or_init(|| ArinaeMatcher::new(CaseMatching::Smart, true, false))
64}
65
66/// Typo-tolerant fuzzy matcher for explicit DSL `%quick` searches.
67///
68/// `%quick` is the opt-in "be clever" path, so it intentionally accepts a
69/// broader set of typo-like matches than shell completion does.
70///
71/// # Examples
72///
73/// ```
74/// use osp_cli::core::fuzzy::search_fuzzy_matcher;
75/// use skim::fuzzy_matcher::FuzzyMatcher;
76///
77/// assert!(search_fuzzy_matcher()
78///     .fuzzy_match("doctor --mreg", "doctr mreg")
79///     .is_some());
80/// ```
81pub fn search_fuzzy_matcher() -> &'static ArinaeMatcher {
82    static MATCHER: OnceLock<ArinaeMatcher> = OnceLock::new();
83    MATCHER.get_or_init(|| ArinaeMatcher::new(CaseMatching::Smart, true, false))
84}