1use std::{
2 borrow::Borrow,
3 cmp::Ordering,
4 iter,
5 ops::Range,
6 sync::atomic::{self, AtomicBool},
7};
8
9use gpui::{BackgroundExecutor, SharedString};
10use nucleo::Utf32Str;
11use nucleo::pattern::{Atom, AtomKind, CaseMatching, Normalization};
12
13use crate::{
14 Cancelled, Case, LengthPenalty,
15 matcher::{self, LENGTH_PENALTY},
16 positions_from_sorted,
17};
18use fuzzy::CharBag;
19
20// String matching is always case-insensitive at the nucleo level — using
21// `CaseMatching::Smart` there would reject queries whose capitalization
22// doesn't match the candidate, breaking pickers like the command palette
23// (`"Editor: Backspace"` against the action named `"editor: backspace"`).
24// `Case::Smart` is still honored as a *scoring hint*: when the query
25// contains uppercase, candidates whose matched characters disagree in case
26// are downranked rather than dropped.
27const SMART_CASE_PENALTY_PER_MISMATCH: f64 = 0.9;
28
29struct Query {
30 atoms: Vec<Atom>,
31 source_words: Option<Vec<Vec<char>>>,
32 char_bag: CharBag,
33}
34
35impl Query {
36 fn build(query: &str, case: Case) -> Option<Self> {
37 let mut atoms = Vec::new();
38 let mut source_words = Vec::new();
39 let wants_case_penalty = case.is_smart() && query.chars().any(|c| c.is_uppercase());
40
41 for word in query.split_whitespace() {
42 atoms.push(Atom::new(
43 word,
44 CaseMatching::Ignore,
45 Normalization::Smart,
46 AtomKind::Fuzzy,
47 false,
48 ));
49 if wants_case_penalty {
50 source_words.push(word.chars().collect());
51 }
52 }
53
54 if atoms.is_empty() {
55 return None;
56 }
57
58 Some(Query {
59 atoms,
60 source_words: wants_case_penalty.then_some(source_words),
61 char_bag: CharBag::from(query),
62 })
63 }
64}
65
66#[derive(Clone, Debug)]
67pub struct StringMatchCandidate {
68 pub id: usize,
69 pub string: SharedString,
70 char_bag: CharBag,
71}
72
73impl StringMatchCandidate {
74 pub fn new(id: usize, string: impl ToString) -> Self {
75 Self::from_shared(id, SharedString::new(string.to_string()))
76 }
77
78 pub fn from_shared(id: usize, string: SharedString) -> Self {
79 let char_bag = CharBag::from(string.as_ref());
80 Self {
81 id,
82 string,
83 char_bag,
84 }
85 }
86}
87
88#[derive(Clone, Debug)]
89pub struct StringMatch {
90 pub candidate_id: usize,
91 pub score: f64,
92 pub positions: Vec<usize>,
93 pub string: SharedString,
94}
95
96impl StringMatch {
97 pub fn ranges(&self) -> impl '_ + Iterator<Item = Range<usize>> {
98 let mut positions = self.positions.iter().peekable();
99 iter::from_fn(move || {
100 let start = *positions.next()?;
101 let char_len = self.char_len_at_index(start)?;
102 let mut end = start + char_len;
103 while let Some(next_start) = positions.peek() {
104 if end == **next_start {
105 let Some(char_len) = self.char_len_at_index(end) else {
106 break;
107 };
108 end += char_len;
109 positions.next();
110 } else {
111 break;
112 }
113 }
114 Some(start..end)
115 })
116 }
117
118 fn char_len_at_index(&self, ix: usize) -> Option<usize> {
119 self.string
120 .get(ix..)
121 .and_then(|slice| slice.chars().next().map(|c| c.len_utf8()))
122 }
123}
124
125impl PartialEq for StringMatch {
126 fn eq(&self, other: &Self) -> bool {
127 self.cmp(other).is_eq()
128 }
129}
130
131impl Eq for StringMatch {}
132
133impl PartialOrd for StringMatch {
134 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
135 Some(self.cmp(other))
136 }
137}
138
139impl Ord for StringMatch {
140 fn cmp(&self, other: &Self) -> Ordering {
141 self.score
142 .total_cmp(&other.score)
143 .then_with(|| self.candidate_id.cmp(&other.candidate_id))
144 }
145}
146
147pub async fn match_strings_async<T>(
148 candidates: &[T],
149 query: &str,
150 case: Case,
151 length_penalty: LengthPenalty,
152 max_results: usize,
153 cancel_flag: &AtomicBool,
154 executor: BackgroundExecutor,
155) -> Vec<StringMatch>
156where
157 T: Borrow<StringMatchCandidate> + Sync,
158{
159 if candidates.is_empty() || max_results == 0 {
160 return Vec::new();
161 }
162
163 let Some(query) = Query::build(query, case) else {
164 return empty_query_results(candidates, max_results);
165 };
166
167 let num_cpus = executor.num_cpus().min(candidates.len());
168 let base_size = candidates.len() / num_cpus;
169 let remainder = candidates.len() % num_cpus;
170 let mut segment_results = (0..num_cpus)
171 .map(|_| Vec::with_capacity(max_results.min(candidates.len())))
172 .collect::<Vec<_>>();
173
174 let config = nucleo::Config::DEFAULT;
175 let mut matchers = matcher::get_matchers(num_cpus, config);
176
177 executor
178 .scoped(|scope| {
179 for (segment_idx, (results, matcher)) in segment_results
180 .iter_mut()
181 .zip(matchers.iter_mut())
182 .enumerate()
183 {
184 let query = &query;
185 scope.spawn(async move {
186 let segment_start = segment_idx * base_size + segment_idx.min(remainder);
187 let segment_end =
188 (segment_idx + 1) * base_size + (segment_idx + 1).min(remainder);
189
190 match_string_helper(
191 &candidates[segment_start..segment_end],
192 query,
193 matcher,
194 length_penalty,
195 results,
196 cancel_flag,
197 )
198 .ok();
199 });
200 }
201 })
202 .await;
203
204 matcher::return_matchers(matchers);
205
206 if cancel_flag.load(atomic::Ordering::Acquire) {
207 return Vec::new();
208 }
209
210 let mut results = segment_results.concat();
211 util::truncate_to_bottom_n_sorted_by(&mut results, max_results, &|a, b| b.cmp(a));
212 results
213}
214
215pub fn match_strings<T>(
216 candidates: &[T],
217 query: &str,
218 case: Case,
219 length_penalty: LengthPenalty,
220 max_results: usize,
221) -> Vec<StringMatch>
222where
223 T: Borrow<StringMatchCandidate>,
224{
225 if candidates.is_empty() || max_results == 0 {
226 return Vec::new();
227 }
228
229 let Some(query) = Query::build(query, case) else {
230 return empty_query_results(candidates, max_results);
231 };
232
233 let config = nucleo::Config::DEFAULT;
234 let mut matcher = matcher::get_matcher(config);
235 let mut results = Vec::with_capacity(max_results.min(candidates.len()));
236
237 match_string_helper(
238 candidates,
239 &query,
240 &mut matcher,
241 length_penalty,
242 &mut results,
243 &AtomicBool::new(false),
244 )
245 .ok();
246
247 matcher::return_matcher(matcher);
248 util::truncate_to_bottom_n_sorted_by(&mut results, max_results, &|a, b| b.cmp(a));
249 results
250}
251
252fn empty_query_results<T: Borrow<StringMatchCandidate>>(
253 candidates: &[T],
254 max_results: usize,
255) -> Vec<StringMatch> {
256 candidates
257 .iter()
258 .take(max_results)
259 .map(|candidate| {
260 let borrowed = candidate.borrow();
261 StringMatch {
262 candidate_id: borrowed.id,
263 score: 0.,
264 positions: Vec::new(),
265 string: borrowed.string.clone(),
266 }
267 })
268 .collect()
269}
270
271fn match_string_helper<T>(
272 candidates: &[T],
273 query: &Query,
274 matcher: &mut nucleo::Matcher,
275 length_penalty: LengthPenalty,
276 results: &mut Vec<StringMatch>,
277 cancel_flag: &AtomicBool,
278) -> Result<(), Cancelled>
279where
280 T: Borrow<StringMatchCandidate>,
281{
282 let mut buf = Vec::new();
283 let mut matched_chars: Vec<u32> = Vec::new();
284 let mut atom_matched_chars = Vec::new();
285 let mut candidate_chars: Vec<char> = Vec::new();
286
287 for candidate in candidates {
288 buf.clear();
289 matched_chars.clear();
290 if cancel_flag.load(atomic::Ordering::Relaxed) {
291 return Err(Cancelled);
292 }
293
294 let borrowed = candidate.borrow();
295
296 if !borrowed.char_bag.is_superset(query.char_bag) {
297 continue;
298 }
299
300 let haystack: Utf32Str = Utf32Str::new(&borrowed.string, &mut buf);
301
302 if query.source_words.is_some() {
303 candidate_chars.clear();
304 candidate_chars.extend(borrowed.string.chars());
305 }
306
307 let mut total_score: u32 = 0;
308 let mut case_mismatches: u32 = 0;
309 let mut all_matched = true;
310
311 for (atom_idx, atom) in query.atoms.iter().enumerate() {
312 atom_matched_chars.clear();
313 let Some(score) = atom.indices(haystack, matcher, &mut atom_matched_chars) else {
314 all_matched = false;
315 break;
316 };
317 total_score = total_score.saturating_add(score as u32);
318 if let Some(source_words) = query.source_words.as_deref() {
319 let query_chars = &source_words[atom_idx];
320 if query_chars.len() == atom_matched_chars.len() {
321 for (&query_char, &pos) in query_chars.iter().zip(&atom_matched_chars) {
322 if let Some(&candidate_char) = candidate_chars.get(pos as usize)
323 && candidate_char != query_char
324 && candidate_char.eq_ignore_ascii_case(&query_char)
325 {
326 case_mismatches += 1;
327 }
328 }
329 }
330 }
331 matched_chars.extend_from_slice(&atom_matched_chars);
332 }
333
334 if all_matched {
335 matched_chars.sort_unstable();
336 matched_chars.dedup();
337
338 let positive = total_score as f64 * case_penalty(case_mismatches);
339 let adjusted_score =
340 positive - length_penalty_for(borrowed.string.as_ref(), length_penalty);
341 let positions = positions_from_sorted(borrowed.string.as_ref(), &matched_chars);
342
343 results.push(StringMatch {
344 candidate_id: borrowed.id,
345 score: adjusted_score,
346 positions,
347 string: borrowed.string.clone(),
348 });
349 }
350 }
351 Ok(())
352}
353
354#[inline]
355fn case_penalty(mismatches: u32) -> f64 {
356 if mismatches == 0 {
357 1.0
358 } else {
359 SMART_CASE_PENALTY_PER_MISMATCH.powi(mismatches as i32)
360 }
361}
362
363#[inline]
364fn length_penalty_for(s: &str, length_penalty: LengthPenalty) -> f64 {
365 if length_penalty.is_on() {
366 s.len() as f64 * LENGTH_PENALTY
367 } else {
368 0.0
369 }
370}
371
372#[cfg(test)]
373mod tests {
374 use super::*;
375 use gpui::BackgroundExecutor;
376
377 fn candidates(strings: &[&str]) -> Vec<StringMatchCandidate> {
378 strings
379 .iter()
380 .enumerate()
381 .map(|(id, s)| StringMatchCandidate::new(id, s))
382 .collect()
383 }
384
385 #[gpui::test]
386 async fn test_basic_match(executor: BackgroundExecutor) {
387 let cs = candidates(&["hello", "world", "help"]);
388 let cancel = AtomicBool::new(false);
389 let results = match_strings_async(
390 &cs,
391 "hel",
392 Case::Ignore,
393 LengthPenalty::Off,
394 10,
395 &cancel,
396 executor,
397 )
398 .await;
399 let matched: Vec<&str> = results.iter().map(|m| m.string.as_ref()).collect();
400 assert!(matched.contains(&"hello"));
401 assert!(matched.contains(&"help"));
402 assert!(!matched.contains(&"world"));
403 }
404
405 #[gpui::test]
406 async fn test_multi_word_query(executor: BackgroundExecutor) {
407 let cs = candidates(&[
408 "src/lib/parser.rs",
409 "src/bin/main.rs",
410 "tests/parser_test.rs",
411 ]);
412 let cancel = AtomicBool::new(false);
413 let results = match_strings_async(
414 &cs,
415 "src parser",
416 Case::Ignore,
417 LengthPenalty::Off,
418 10,
419 &cancel,
420 executor,
421 )
422 .await;
423 assert_eq!(results.len(), 1);
424 assert_eq!(results[0].string, "src/lib/parser.rs");
425 }
426
427 #[gpui::test]
428 async fn test_empty_query_returns_all(executor: BackgroundExecutor) {
429 let cs = candidates(&["alpha", "beta", "gamma"]);
430 let cancel = AtomicBool::new(false);
431 let results = match_strings_async(
432 &cs,
433 "",
434 Case::Ignore,
435 LengthPenalty::Off,
436 10,
437 &cancel,
438 executor,
439 )
440 .await;
441 assert_eq!(results.len(), 3);
442 assert!(results.iter().all(|m| m.score == 0.0));
443 }
444
445 #[gpui::test]
446 async fn test_whitespace_only_query_returns_all(executor: BackgroundExecutor) {
447 let cs = candidates(&["alpha", "beta", "gamma"]);
448 let cancel = AtomicBool::new(false);
449 let results = match_strings_async(
450 &cs,
451 " \t\n",
452 Case::Ignore,
453 LengthPenalty::Off,
454 10,
455 &cancel,
456 executor,
457 )
458 .await;
459 assert_eq!(results.len(), 3);
460 }
461
462 #[gpui::test]
463 async fn test_empty_candidates(executor: BackgroundExecutor) {
464 let cs: Vec<StringMatchCandidate> = vec![];
465 let cancel = AtomicBool::new(false);
466 let results = match_strings_async(
467 &cs,
468 "query",
469 Case::Ignore,
470 LengthPenalty::Off,
471 10,
472 &cancel,
473 executor,
474 )
475 .await;
476 assert!(results.is_empty());
477 }
478
479 #[gpui::test]
480 async fn test_cancellation(executor: BackgroundExecutor) {
481 let cs = candidates(&["hello", "world"]);
482 let cancel = AtomicBool::new(true);
483 let results = match_strings_async(
484 &cs,
485 "hel",
486 Case::Ignore,
487 LengthPenalty::Off,
488 10,
489 &cancel,
490 executor,
491 )
492 .await;
493 assert!(results.is_empty());
494 }
495
496 #[gpui::test]
497 async fn test_max_results_limit(executor: BackgroundExecutor) {
498 let cs = candidates(&["ab", "abc", "abcd", "abcde"]);
499 let cancel = AtomicBool::new(false);
500 let results = match_strings_async(
501 &cs,
502 "ab",
503 Case::Ignore,
504 LengthPenalty::Off,
505 2,
506 &cancel,
507 executor,
508 )
509 .await;
510 assert_eq!(results.len(), 2);
511 }
512
513 #[gpui::test]
514 async fn test_scoring_order(executor: BackgroundExecutor) {
515 let cs = candidates(&[
516 "some_very_long_variable_name_fuzzy",
517 "fuzzy",
518 "a_fuzzy_thing",
519 ]);
520 let cancel = AtomicBool::new(false);
521 let results = match_strings_async(
522 &cs,
523 "fuzzy",
524 Case::Ignore,
525 LengthPenalty::Off,
526 10,
527 &cancel,
528 executor.clone(),
529 )
530 .await;
531
532 let ordered = matches!(
533 (
534 results[0].string.as_ref(),
535 results[1].string.as_ref(),
536 results[2].string.as_ref()
537 ),
538 (
539 "fuzzy",
540 "a_fuzzy_thing",
541 "some_very_long_variable_name_fuzzy"
542 )
543 );
544 assert!(ordered, "matches are not in the proper order.");
545
546 let results_penalty = match_strings_async(
547 &cs,
548 "fuzzy",
549 Case::Ignore,
550 LengthPenalty::On,
551 10,
552 &cancel,
553 executor,
554 )
555 .await;
556 let greater = results[2].score > results_penalty[2].score;
557 assert!(greater, "penalize length not affecting long candidates");
558 }
559
560 #[gpui::test]
561 async fn test_utf8_positions(executor: BackgroundExecutor) {
562 let cs = candidates(&["café"]);
563 let cancel = AtomicBool::new(false);
564 let results = match_strings_async(
565 &cs,
566 "caf",
567 Case::Ignore,
568 LengthPenalty::Off,
569 10,
570 &cancel,
571 executor,
572 )
573 .await;
574 assert_eq!(results.len(), 1);
575 let m = &results[0];
576 assert_eq!(m.positions, vec![0, 1, 2]);
577 for &pos in &m.positions {
578 assert!(m.string.is_char_boundary(pos));
579 }
580 }
581
582 #[gpui::test]
583 async fn test_smart_case(executor: BackgroundExecutor) {
584 let cs = candidates(&["FooBar", "foobar", "FOOBAR"]);
585 let cancel = AtomicBool::new(false);
586
587 let case_insensitive = match_strings_async(
588 &cs,
589 "foobar",
590 Case::Ignore,
591 LengthPenalty::Off,
592 10,
593 &cancel,
594 executor.clone(),
595 )
596 .await;
597 assert_eq!(case_insensitive.len(), 3);
598
599 let smart = match_strings_async(
600 &cs,
601 "FooBar",
602 Case::Smart,
603 LengthPenalty::Off,
604 10,
605 &cancel,
606 executor,
607 )
608 .await;
609 assert!(smart.iter().any(|m| m.string == "FooBar"));
610 let foobar_score = smart.iter().find(|m| m.string == "FooBar").map(|m| m.score);
611 let lower_score = smart.iter().find(|m| m.string == "foobar").map(|m| m.score);
612 if let (Some(exact), Some(lower)) = (foobar_score, lower_score) {
613 assert!(exact >= lower);
614 }
615 }
616
617 #[gpui::test]
618 async fn test_smart_case_does_not_flip_order_when_length_penalty_on(
619 executor: BackgroundExecutor,
620 ) {
621 // Regression for the sign bug: with a length penalty large enough to push
622 // `total_score - length_penalty` negative, case mismatches used to make
623 // scores *better* (less negative). Exact-case match must still rank first.
624 let cs = candidates(&[
625 "aaaaaaaaaaaaaaaaaaaaaaaaaaaa_FooBar",
626 "aaaaaaaaaaaaaaaaaaaaaaaaaaaa_foobar",
627 ]);
628 let cancel = AtomicBool::new(false);
629 let results = match_strings_async(
630 &cs,
631 "FooBar",
632 Case::Smart,
633 LengthPenalty::On,
634 10,
635 &cancel,
636 executor,
637 )
638 .await;
639 let exact = results
640 .iter()
641 .find(|m| m.string.as_ref() == "aaaaaaaaaaaaaaaaaaaaaaaaaaaa_FooBar")
642 .map(|m| m.score)
643 .expect("exact-case candidate should match");
644 let mismatch = results
645 .iter()
646 .find(|m| m.string.as_ref() == "aaaaaaaaaaaaaaaaaaaaaaaaaaaa_foobar")
647 .map(|m| m.score)
648 .expect("mismatch-case candidate should match");
649 assert!(
650 exact >= mismatch,
651 "exact-case score ({exact}) should be >= mismatch-case score ({mismatch})"
652 );
653 }
654
655 #[gpui::test]
656 async fn test_char_bag_prefilter(executor: BackgroundExecutor) {
657 let cs = candidates(&["abcdef", "abc", "def", "aabbcc"]);
658 let cancel = AtomicBool::new(false);
659 let results = match_strings_async(
660 &cs,
661 "abc",
662 Case::Ignore,
663 LengthPenalty::Off,
664 10,
665 &cancel,
666 executor,
667 )
668 .await;
669 let matched: Vec<&str> = results.iter().map(|m| m.string.as_ref()).collect();
670 assert!(matched.contains(&"abcdef"));
671 assert!(matched.contains(&"abc"));
672 assert!(matched.contains(&"aabbcc"));
673 assert!(!matched.contains(&"def"));
674 }
675
676 #[test]
677 fn test_sync_basic_match() {
678 let cs = candidates(&["hello", "world", "help"]);
679 let results = match_strings(&cs, "hel", Case::Ignore, LengthPenalty::Off, 10);
680 let matched: Vec<&str> = results.iter().map(|m| m.string.as_ref()).collect();
681 assert!(matched.contains(&"hello"));
682 assert!(matched.contains(&"help"));
683 assert!(!matched.contains(&"world"));
684 }
685
686 #[test]
687 fn test_sync_empty_query_returns_all() {
688 let cs = candidates(&["alpha", "beta", "gamma"]);
689 let results = match_strings(&cs, "", Case::Ignore, LengthPenalty::Off, 10);
690 assert_eq!(results.len(), 3);
691 }
692
693 #[test]
694 fn test_sync_whitespace_only_query_returns_all() {
695 let cs = candidates(&["alpha", "beta", "gamma"]);
696 let results = match_strings(&cs, " ", Case::Ignore, LengthPenalty::Off, 10);
697 assert_eq!(results.len(), 3);
698 }
699
700 #[test]
701 fn test_sync_max_results() {
702 let cs = candidates(&["ab", "abc", "abcd", "abcde"]);
703 let results = match_strings(&cs, "ab", Case::Ignore, LengthPenalty::Off, 2);
704 assert_eq!(results.len(), 2);
705 }
706
707 #[gpui::test]
708 async fn test_empty_query_respects_max_results(executor: BackgroundExecutor) {
709 let cs = candidates(&["alpha", "beta", "gamma", "delta"]);
710 let cancel = AtomicBool::new(false);
711 let results = match_strings_async(
712 &cs,
713 "",
714 Case::Ignore,
715 LengthPenalty::Off,
716 2,
717 &cancel,
718 executor,
719 )
720 .await;
721 assert_eq!(results.len(), 2);
722 }
723
724 #[gpui::test]
725 async fn test_multi_word_with_nonmatching_word(executor: BackgroundExecutor) {
726 let cs = candidates(&["src/parser.rs", "src/main.rs"]);
727 let cancel = AtomicBool::new(false);
728 let results = match_strings_async(
729 &cs,
730 "src xyzzy",
731 Case::Ignore,
732 LengthPenalty::Off,
733 10,
734 &cancel,
735 executor,
736 )
737 .await;
738 assert!(
739 results.is_empty(),
740 "no candidate contains 'xyzzy', so nothing should match"
741 );
742 }
743
744 #[gpui::test]
745 async fn test_segment_size_not_divisible_by_cpus(executor: BackgroundExecutor) {
746 executor.set_num_cpus(4);
747 let cs = candidates(&["alpha", "beta", "gamma", "delta", "epsilon"]);
748 let cancel = AtomicBool::new(false);
749 let results = match_strings_async(
750 &cs,
751 "a",
752 Case::Ignore,
753 LengthPenalty::Off,
754 10,
755 &cancel,
756 executor,
757 )
758 .await;
759 let matched: Vec<&str> = results.iter().map(|m| m.string.as_ref()).collect();
760 assert!(matched.contains(&"alpha"));
761 assert!(matched.contains(&"gamma"));
762 assert!(matched.contains(&"delta"));
763 }
764
765 #[gpui::test]
766 async fn test_segment_size_with_many_cpus_few_candidates(executor: BackgroundExecutor) {
767 executor.set_num_cpus(16);
768 let cs = candidates(&["one", "two", "three"]);
769 let cancel = AtomicBool::new(false);
770 let results = match_strings_async(
771 &cs,
772 "o",
773 Case::Ignore,
774 LengthPenalty::Off,
775 10,
776 &cancel,
777 executor,
778 )
779 .await;
780 let matched: Vec<&str> = results.iter().map(|m| m.string.as_ref()).collect();
781 assert!(matched.contains(&"one"));
782 assert!(matched.contains(&"two"));
783 }
784
785 #[gpui::test]
786 async fn test_segment_size_single_candidate(executor: BackgroundExecutor) {
787 executor.set_num_cpus(8);
788 let cs = candidates(&["lonely"]);
789 let cancel = AtomicBool::new(false);
790 let results = match_strings_async(
791 &cs,
792 "lone",
793 Case::Ignore,
794 LengthPenalty::Off,
795 10,
796 &cancel,
797 executor,
798 )
799 .await;
800 assert_eq!(results.len(), 1);
801 assert_eq!(results[0].string.as_ref(), "lonely");
802 }
803
804 #[gpui::test]
805 async fn test_segment_size_candidates_equal_cpus(executor: BackgroundExecutor) {
806 executor.set_num_cpus(4);
807 let cs = candidates(&["aaa", "bbb", "ccc", "ddd"]);
808 let cancel = AtomicBool::new(false);
809 let results = match_strings_async(
810 &cs,
811 "a",
812 Case::Ignore,
813 LengthPenalty::Off,
814 10,
815 &cancel,
816 executor,
817 )
818 .await;
819 assert_eq!(results.len(), 1);
820 assert_eq!(results[0].string.as_ref(), "aaa");
821 }
822
823 #[gpui::test]
824 async fn test_segment_size_candidates_one_more_than_cpus(executor: BackgroundExecutor) {
825 executor.set_num_cpus(3);
826 let cs = candidates(&["ant", "ape", "dog", "axe"]);
827 let cancel = AtomicBool::new(false);
828 let results = match_strings_async(
829 &cs,
830 "a",
831 Case::Ignore,
832 LengthPenalty::Off,
833 10,
834 &cancel,
835 executor,
836 )
837 .await;
838 let matched: Vec<&str> = results.iter().map(|m| m.string.as_ref()).collect();
839 assert!(matched.contains(&"ant"));
840 assert!(matched.contains(&"ape"));
841 assert!(matched.contains(&"axe"));
842 assert!(!matched.contains(&"dog"));
843 }
844}