1use crate::{
2 matcher::{Match, MatchCandidate, Matcher},
3 CharBag,
4};
5use gpui::BackgroundExecutor;
6use std::{
7 borrow::Cow,
8 cmp::{self, Ordering},
9 iter,
10 ops::Range,
11 sync::atomic::AtomicBool,
12};
13
14#[derive(Clone, Debug)]
15pub struct StringMatchCandidate {
16 pub id: usize,
17 pub string: String,
18 pub char_bag: CharBag,
19}
20
21impl Match for StringMatch {
22 fn score(&self) -> f64 {
23 self.score
24 }
25
26 fn set_positions(&mut self, positions: Vec<usize>) {
27 self.positions = positions;
28 }
29}
30
31impl StringMatchCandidate {
32 pub fn new(id: usize, string: String) -> Self {
33 Self {
34 id,
35 char_bag: CharBag::from(string.as_str()),
36 string,
37 }
38 }
39}
40
41impl<'a> MatchCandidate for &'a StringMatchCandidate {
42 fn has_chars(&self, bag: CharBag) -> bool {
43 self.char_bag.is_superset(bag)
44 }
45
46 fn to_string(&self) -> Cow<'a, str> {
47 self.string.as_str().into()
48 }
49}
50
51#[derive(Clone, Debug)]
52pub struct StringMatch {
53 pub candidate_id: usize,
54 pub score: f64,
55 pub positions: Vec<usize>,
56 pub string: String,
57}
58
59impl StringMatch {
60 pub fn ranges(&self) -> impl '_ + Iterator<Item = Range<usize>> {
61 let mut positions = self.positions.iter().peekable();
62 iter::from_fn(move || {
63 if let Some(start) = positions.next().copied() {
64 if start >= self.string.len() {
65 log::error!(
66 "Invariant violation: Index {start} out of range in string {:?}",
67 self.string
68 );
69 return None;
70 }
71 let mut end = start + self.char_len_at_index(start);
72 while let Some(next_start) = positions.peek() {
73 if end == **next_start {
74 if end >= self.string.len() {
75 log::error!(
76 "Invariant violation: Index {end} out of range in string {:?}",
77 self.string
78 );
79 return None;
80 }
81 end += self.char_len_at_index(end);
82 positions.next();
83 } else {
84 break;
85 }
86 }
87
88 return Some(start..end);
89 }
90 None
91 })
92 }
93
94 fn char_len_at_index(&self, ix: usize) -> usize {
95 self.string[ix..].chars().next().unwrap().len_utf8()
96 }
97}
98
99impl PartialEq for StringMatch {
100 fn eq(&self, other: &Self) -> bool {
101 self.cmp(other).is_eq()
102 }
103}
104
105impl Eq for StringMatch {}
106
107impl PartialOrd for StringMatch {
108 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
109 Some(self.cmp(other))
110 }
111}
112
113impl Ord for StringMatch {
114 fn cmp(&self, other: &Self) -> Ordering {
115 self.score
116 .partial_cmp(&other.score)
117 .unwrap_or(Ordering::Equal)
118 .then_with(|| self.candidate_id.cmp(&other.candidate_id))
119 }
120}
121
122pub async fn match_strings(
123 candidates: &[StringMatchCandidate],
124 query: &str,
125 smart_case: bool,
126 max_results: usize,
127 cancel_flag: &AtomicBool,
128 executor: BackgroundExecutor,
129) -> Vec<StringMatch> {
130 if candidates.is_empty() || max_results == 0 {
131 return Default::default();
132 }
133
134 if query.is_empty() {
135 return candidates
136 .iter()
137 .map(|candidate| StringMatch {
138 candidate_id: candidate.id,
139 score: 0.,
140 positions: Default::default(),
141 string: candidate.string.clone(),
142 })
143 .collect();
144 }
145
146 let lowercase_query = query.to_lowercase().chars().collect::<Vec<_>>();
147 let query = query.chars().collect::<Vec<_>>();
148
149 let lowercase_query = &lowercase_query;
150 let query = &query;
151 let query_char_bag = CharBag::from(&lowercase_query[..]);
152
153 let num_cpus = executor.num_cpus().min(candidates.len());
154 let segment_size = (candidates.len() + num_cpus - 1) / num_cpus;
155 let mut segment_results = (0..num_cpus)
156 .map(|_| Vec::with_capacity(max_results.min(candidates.len())))
157 .collect::<Vec<_>>();
158
159 executor
160 .scoped(|scope| {
161 for (segment_idx, results) in segment_results.iter_mut().enumerate() {
162 let cancel_flag = &cancel_flag;
163 scope.spawn(async move {
164 let segment_start = cmp::min(segment_idx * segment_size, candidates.len());
165 let segment_end = cmp::min(segment_start + segment_size, candidates.len());
166 let mut matcher = Matcher::new(
167 query,
168 lowercase_query,
169 query_char_bag,
170 smart_case,
171 max_results,
172 );
173
174 matcher.match_candidates(
175 &[],
176 &[],
177 candidates[segment_start..segment_end].iter(),
178 results,
179 cancel_flag,
180 |candidate, score| StringMatch {
181 candidate_id: candidate.id,
182 score,
183 positions: Vec::new(),
184 string: candidate.string.to_string(),
185 },
186 );
187 });
188 }
189 })
190 .await;
191
192 let mut results = Vec::new();
193 for segment_result in segment_results {
194 if results.is_empty() {
195 results = segment_result;
196 } else {
197 util::extend_sorted(&mut results, segment_result, max_results, |a, b| b.cmp(a));
198 }
199 }
200 results
201}