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 explicit DSL `%quick` searches.
45///
46/// `%quick` is the opt-in "be clever" path, so it intentionally accepts a
47/// broader set of typo-like matches than shell completion does.
48///
49/// # Examples
50///
51/// ```
52/// use osp_cli::core::fuzzy::search_fuzzy_matcher;
53/// use skim::fuzzy_matcher::FuzzyMatcher;
54///
55/// assert!(search_fuzzy_matcher()
56///     .fuzzy_match("doctor --mreg", "doctr mreg")
57///     .is_some());
58/// ```
59pub fn search_fuzzy_matcher() -> &'static ArinaeMatcher {
60    static MATCHER: OnceLock<ArinaeMatcher> = OnceLock::new();
61    MATCHER.get_or_init(|| ArinaeMatcher::new(CaseMatching::Smart, true, false))
62}