Improve fuzzy match performance and fix corner case that omits results (#22524)

* Removes `max_results` from the matcher interface as this is better
dealt with in consumers once all results are known. The current
implementation was quite inefficient as it was using binary search to
find insertion points and then doing an insert which copies the entire
suffix each time.

* There was a corner case where if the binary search found a match
candidate with the same score, it was dropped. Now fixed.

* Uses of `util::extend_sorted` when merging results from worker threads
also repeatedly uses binary search and insertion which copies the entire
suffix. A followup will remove that and its usage.

* Adds `util::truncate_to_bottom_n_sorted_by` which uses quickselect +
sort to efficiently get a sorted count limited result.

* Improves interface of Matcher::match_candidates by providing the match
positions to the build function. This allows for removal of the `Match`
trait. It also fixes a bug where the Match's own Ord wasn't being used,
which seems relevant to PathMatch for cases where scores are the same.

Release Notes:

- N/A
This commit is contained in:
Michael Sloan 2024-12-31 13:56:23 -07:00 committed by GitHub
parent f912c545e7
commit 6ef5d8f748
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 50 additions and 96 deletions

View file

@ -1,5 +1,5 @@
use crate::{
matcher::{Match, MatchCandidate, Matcher},
matcher::{MatchCandidate, Matcher},
CharBag,
};
use gpui::BackgroundExecutor;
@ -46,16 +46,6 @@ pub struct StringMatch {
pub string: String,
}
impl Match for StringMatch {
fn score(&self) -> f64 {
self.score
}
fn set_positions(&mut self, positions: Vec<usize>) {
self.positions = positions;
}
}
impl StringMatch {
pub fn ranges(&self) -> impl '_ + Iterator<Item = Range<usize>> {
let mut positions = self.positions.iter().peekable();
@ -167,13 +157,8 @@ pub async fn match_strings(
scope.spawn(async move {
let segment_start = cmp::min(segment_idx * segment_size, candidates.len());
let segment_end = cmp::min(segment_start + segment_size, candidates.len());
let mut matcher = Matcher::new(
query,
lowercase_query,
query_char_bag,
smart_case,
max_results,
);
let mut matcher =
Matcher::new(query, lowercase_query, query_char_bag, smart_case);
matcher.match_candidates(
&[],
@ -181,10 +166,10 @@ pub async fn match_strings(
candidates[segment_start..segment_end].iter(),
results,
cancel_flag,
|candidate, score| StringMatch {
|candidate, score, positions| StringMatch {
candidate_id: candidate.id,
score,
positions: Vec::new(),
positions: positions.clone(),
string: candidate.string.to_string(),
},
);
@ -193,13 +178,7 @@ pub async fn match_strings(
})
.await;
let mut results = Vec::new();
for segment_result in segment_results {
if results.is_empty() {
results = segment_result;
} else {
util::extend_sorted(&mut results, segment_result, max_results, |a, b| b.cmp(a));
}
}
let mut results = segment_results.concat();
util::truncate_to_bottom_n_sorted_by(&mut results, max_results, &|a, b| b.cmp(a));
results
}