editor: Improve code completion filtering to provide fewer and more accurate suggestions (#32928)

Closes #32756

- Uses `filter_text` from LSP source to filter items in completion list.
This fixes noisy lists like on typing `await` in Rust, it would suggest
`await.or`, `await.and`, etc., which are bad suggestions. Fallbacks to
label.
- Add `penalize_length` flag to fuzzy matcher, which was the default
behavior across. Now, this flag is set to `false` just for code
completion fuzzy matching. This fixes the case where if the query is
`unreac` and the completion items are `unreachable` and
`unreachable!()`, the item with a shorter length would have a larger
score than the other one, which is not right in the case of
auto-complete context. Now these two items will have the same fuzzy
score, and LSP `sort_text` will take over in finalizing its ranking.
- Updated test to be more utility based rather than example based. This
will help to iterate/verify logic faster on what's going on.

Before/After:

await: 
<img width="600" alt="before-await"
src="https://github.com/user-attachments/assets/384138dd-a90d-4942-a430-6ae15df37268"
/>
<img width="600" alt="after-await"
src="https://github.com/user-attachments/assets/d05a10fa-bae5-49bd-9fe7-9933ff215f29"
/>

iter:
<img width="600" alt="before-iter"
src="https://github.com/user-attachments/assets/6e57ffe9-007d-4b17-9cc2-d48fc0176c8e"
/>
<img width="600" alt="after-iter"
src="https://github.com/user-attachments/assets/a8577a9f-dcc8-4fd6-9ba0-b7590584ec31"
/>

opt:
<img width="600" alt="opt-before"
src="https://github.com/user-attachments/assets/d45b6c52-c9ee-4bf3-8552-d5e3fdbecbff"
/>
<img width="600" alt="opt-after"
src="https://github.com/user-attachments/assets/daac11a8-9699-48f8-b441-19fe9803848d"
/>

Release Notes:

- Improved code completion filtering to provide fewer and more accurate
suggestions.
This commit is contained in:
Smit Barmase 2025-06-18 16:01:28 +05:30 committed by GitHub
parent 65067dad9e
commit 131f2857a5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
45 changed files with 303 additions and 430 deletions

View file

@ -260,7 +260,7 @@ impl CompletionsMenu {
let match_candidates = completions
.iter()
.enumerate()
.map(|(id, completion)| StringMatchCandidate::new(id, &completion.label.filter_text()))
.map(|(id, completion)| StringMatchCandidate::new(id, completion.filter_text()))
.collect();
let completions_menu = Self {
@ -979,7 +979,8 @@ impl CompletionsMenu {
&match_candidates,
&query,
query.chars().any(|c| c.is_uppercase()),
100,
false,
1000,
&cancel_filter,
background_executor,
)
@ -1055,13 +1056,12 @@ impl CompletionsMenu {
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
enum MatchTier<'a> {
WordStartMatch {
sort_capitalize: Reverse<usize>,
sort_exact: Reverse<i32>,
sort_positions: Vec<usize>,
sort_snippet: Reverse<i32>,
sort_kind: usize,
sort_fuzzy_bracket: Reverse<usize>,
sort_text: Option<&'a str>,
sort_score: Reverse<OrderedFloat<f64>>,
sort_text: Option<&'a str>,
sort_kind: usize,
sort_label: &'a str,
},
OtherMatch {
@ -1069,14 +1069,6 @@ impl CompletionsMenu {
},
}
// Our goal here is to intelligently sort completion suggestions. We want to
// balance the raw fuzzy match score with hints from the language server
// In a fuzzy bracket, matches with a score of 1.0 are prioritized.
// The remaining matches are partitioned into two groups at 3/5 of the max_score.
let max_score = matches.iter().map(|mat| mat.score).fold(0.0, f64::max);
let fuzzy_bracket_threshold = max_score * (3.0 / 5.0);
let query_start_lower = query
.as_ref()
.and_then(|q| q.chars().next())
@ -1117,34 +1109,25 @@ impl CompletionsMenu {
if query_start_doesnt_match_split_words {
MatchTier::OtherMatch { sort_score }
} else {
let sort_fuzzy_bracket = Reverse(if score >= fuzzy_bracket_threshold {
1
} else {
0
});
let sort_snippet = match snippet_sort_order {
SnippetSortOrder::Top => Reverse(if is_snippet { 1 } else { 0 }),
SnippetSortOrder::Bottom => Reverse(if is_snippet { 0 } else { 1 }),
SnippetSortOrder::Inline => Reverse(0),
};
let sort_capitalize = Reverse(
query
.as_ref()
.and_then(|q| q.chars().next())
.zip(string_match.string.chars().next())
.map(|(q_char, s_char)| if q_char == s_char { 1 } else { 0 })
.unwrap_or(0),
);
let sort_positions = string_match.positions.clone();
let sort_exact = Reverse(if Some(completion.filter_text()) == query {
1
} else {
0
});
MatchTier::WordStartMatch {
sort_capitalize,
sort_exact,
sort_positions,
sort_snippet,
sort_kind,
sort_fuzzy_bracket,
sort_text,
sort_score,
sort_text,
sort_kind,
sort_label,
}
}