1mod matcher;
2mod paths;
3mod strings;
4
5use fuzzy::CharBag;
6use nucleo::pattern::{AtomKind, CaseMatching, Normalization, Pattern};
7
8pub use paths::{
9 PathMatch, PathMatchCandidate, PathMatchCandidateSet, match_fixed_path_set, match_path_sets,
10};
11pub use strings::{StringMatch, StringMatchCandidate, match_strings, match_strings_async};
12
13pub(crate) struct Cancelled;
14
15#[derive(Copy, Clone, Debug, PartialEq, Eq)]
16pub enum Case {
17 Smart,
18 Ignore,
19}
20
21impl Case {
22 pub fn smart_if_uppercase_in(query: &str) -> Self {
23 if query.chars().any(|c| c.is_uppercase()) {
24 Self::Smart
25 } else {
26 Self::Ignore
27 }
28 }
29
30 pub fn is_smart(self) -> bool {
31 matches!(self, Self::Smart)
32 }
33}
34
35#[derive(Copy, Clone, Debug, PartialEq, Eq)]
36pub enum LengthPenalty {
37 On,
38 Off,
39}
40
41impl LengthPenalty {
42 pub fn from_bool(on: bool) -> Self {
43 if on { Self::On } else { Self::Off }
44 }
45
46 pub fn is_on(self) -> bool {
47 matches!(self, Self::On)
48 }
49}
50
51// Matching is always case-insensitive at the nucleo level — using
52// `CaseMatching::Smart` there would *reject* candidates whose capitalization
53// doesn't match the query, breaking pickers like the command palette
54// (`"Editor: Backspace"` against the action named `"editor: backspace"`).
55// `Case::Smart` is honored as a *scoring hint* instead: when the query
56// contains uppercase, candidates whose matched characters disagree in case
57// are downranked by a per-mismatch penalty rather than dropped.
58pub(crate) struct Query {
59 pub(crate) pattern: Pattern,
60 /// Non-whitespace query chars in input order, populated only when a smart-case
61 /// penalty will actually be charged. Aligns 1:1 with the indices appended by
62 /// `Pattern::indices` (atom-order, needle-order within each atom).
63 pub(crate) query_chars: Option<Vec<char>>,
64 pub(crate) char_bag: CharBag,
65}
66
67impl Query {
68 pub(crate) fn build(query: &str, case: Case) -> Option<Self> {
69 if query.chars().all(char::is_whitespace) {
70 return None;
71 }
72 let normalized = query.split_whitespace().collect::<Vec<_>>().join(" ");
73 let pattern = Pattern::new(
74 &normalized,
75 CaseMatching::Ignore,
76 Normalization::Smart,
77 AtomKind::Fuzzy,
78 );
79 let wants_case_penalty = case.is_smart() && query.chars().any(|c| c.is_uppercase());
80 let query_chars =
81 wants_case_penalty.then(|| query.chars().filter(|c| !c.is_whitespace()).collect());
82 Some(Query {
83 pattern,
84 query_chars,
85 char_bag: CharBag::from(query),
86 })
87 }
88}
89
90#[inline]
91pub(crate) fn count_case_mismatches(
92 query_chars: Option<&[char]>,
93 matched_chars: &[u32],
94 candidate: &str,
95 candidate_chars: &mut Vec<char>,
96) -> u32 {
97 let Some(query_chars) = query_chars else {
98 return 0;
99 };
100 if query_chars.len() != matched_chars.len() {
101 return 0;
102 }
103 candidate_chars.clear();
104 candidate_chars.extend(candidate.chars());
105 let mut mismatches: u32 = 0;
106 for (&query_char, &pos) in query_chars.iter().zip(matched_chars) {
107 if let Some(&candidate_char) = candidate_chars.get(pos as usize)
108 && candidate_char != query_char
109 && candidate_char.eq_ignore_ascii_case(&query_char)
110 {
111 mismatches += 1;
112 }
113 }
114 mismatches
115}
116
117const SMART_CASE_PENALTY_PER_MISMATCH: f64 = 0.9;
118
119#[inline]
120pub(crate) fn case_penalty(mismatches: u32) -> f64 {
121 if mismatches == 0 {
122 1.0
123 } else {
124 SMART_CASE_PENALTY_PER_MISMATCH.powi(mismatches as i32)
125 }
126}
127
128/// Reconstruct byte-offset match positions from a list of matched char offsets
129/// that is already sorted ascending and deduplicated.
130pub(crate) fn positions_from_sorted(s: &str, sorted_char_indices: &[u32]) -> Vec<usize> {
131 let mut iter = sorted_char_indices.iter().copied().peekable();
132 let mut out = Vec::with_capacity(sorted_char_indices.len());
133 for (char_offset, (byte_offset, _)) in s.char_indices().enumerate() {
134 if iter.peek().is_none() {
135 break;
136 }
137 if iter.next_if(|&m| m == char_offset as u32).is_some() {
138 out.push(byte_offset);
139 }
140 }
141 out
142}