editor: Improve fuzzy match bucket logic for code completions (#29442)
Add new test and improve fuzzy match bucket logic which results into far better balance between LSP and fuzzy search. Before: <img width="500" alt="before" src="https://github.com/user-attachments/assets/3e8900a6-c0ff-4f37-b88e-b0e3783b7e9a" /> After: <img width="500" alt="after" src="https://github.com/user-attachments/assets/738c074c-d446-4697-aac6-9814362e88db" /> Release Notes: - N/A
This commit is contained in:
parent
8b910e1cd9
commit
3961d87ae0
2 changed files with 160 additions and 7 deletions
|
@ -1006,3 +1006,152 @@ fn test_sort_matches_for_snippets(_cx: &mut TestAppContext) {
|
||||||
"Match order not expected"
|
"Match order not expected"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn test_sort_matches_for_exact_match(_cx: &mut TestAppContext) {
|
||||||
|
// Case 1: "set_text"
|
||||||
|
let query: Option<&str> = Some("set_text");
|
||||||
|
let mut matches: Vec<SortableMatch<'_>> = vec![
|
||||||
|
SortableMatch {
|
||||||
|
string_match: StringMatch {
|
||||||
|
candidate_id: 0,
|
||||||
|
score: 1.0,
|
||||||
|
positions: vec![],
|
||||||
|
string: "set_text".to_string(),
|
||||||
|
},
|
||||||
|
is_snippet: false,
|
||||||
|
sort_text: Some("7fffffff"),
|
||||||
|
sort_key: (3, "set_text"),
|
||||||
|
},
|
||||||
|
SortableMatch {
|
||||||
|
string_match: StringMatch {
|
||||||
|
candidate_id: 0,
|
||||||
|
score: 0.32000000000000006,
|
||||||
|
positions: vec![],
|
||||||
|
string: "set_placeholder_text".to_string(),
|
||||||
|
},
|
||||||
|
is_snippet: false,
|
||||||
|
sort_text: Some("7fffffff"),
|
||||||
|
sort_key: (3, "set_placeholder_text"),
|
||||||
|
},
|
||||||
|
SortableMatch {
|
||||||
|
string_match: StringMatch {
|
||||||
|
candidate_id: 0,
|
||||||
|
score: 0.32,
|
||||||
|
positions: vec![],
|
||||||
|
string: "set_text_style_refinement".to_string(),
|
||||||
|
},
|
||||||
|
is_snippet: false,
|
||||||
|
sort_text: Some("7fffffff"),
|
||||||
|
sort_key: (3, "set_text_style_refinement"),
|
||||||
|
},
|
||||||
|
SortableMatch {
|
||||||
|
string_match: StringMatch {
|
||||||
|
candidate_id: 0,
|
||||||
|
score: 0.16666666666666666,
|
||||||
|
positions: vec![],
|
||||||
|
string: "set_context_menu_options".to_string(),
|
||||||
|
},
|
||||||
|
is_snippet: false,
|
||||||
|
sort_text: Some("7fffffff"),
|
||||||
|
sort_key: (3, "set_context_menu_options"),
|
||||||
|
},
|
||||||
|
SortableMatch {
|
||||||
|
string_match: StringMatch {
|
||||||
|
candidate_id: 0,
|
||||||
|
score: 0.08695652173913043,
|
||||||
|
positions: vec![],
|
||||||
|
string: "select_to_next_word_end".to_string(),
|
||||||
|
},
|
||||||
|
is_snippet: false,
|
||||||
|
sort_text: Some("7fffffff"),
|
||||||
|
sort_key: (3, "select_to_next_word_end"),
|
||||||
|
},
|
||||||
|
SortableMatch {
|
||||||
|
string_match: StringMatch {
|
||||||
|
candidate_id: 0,
|
||||||
|
score: 0.07692307692307693,
|
||||||
|
positions: vec![],
|
||||||
|
string: "select_to_next_subword_end".to_string(),
|
||||||
|
},
|
||||||
|
is_snippet: false,
|
||||||
|
sort_text: Some("7fffffff"),
|
||||||
|
sort_key: (3, "select_to_next_subword_end"),
|
||||||
|
},
|
||||||
|
SortableMatch {
|
||||||
|
string_match: StringMatch {
|
||||||
|
candidate_id: 0,
|
||||||
|
score: 0.06956521739130435,
|
||||||
|
positions: vec![],
|
||||||
|
string: "set_custom_context_menu".to_string(),
|
||||||
|
},
|
||||||
|
is_snippet: false,
|
||||||
|
sort_text: Some("7fffffff"),
|
||||||
|
sort_key: (3, "set_custom_context_menu"),
|
||||||
|
},
|
||||||
|
SortableMatch {
|
||||||
|
string_match: StringMatch {
|
||||||
|
candidate_id: 0,
|
||||||
|
score: 0.06,
|
||||||
|
positions: vec![],
|
||||||
|
string: "select_to_end_of_excerpt".to_string(),
|
||||||
|
},
|
||||||
|
is_snippet: false,
|
||||||
|
sort_text: Some("7fffffff"),
|
||||||
|
sort_key: (3, "select_to_end_of_excerpt"),
|
||||||
|
},
|
||||||
|
SortableMatch {
|
||||||
|
string_match: StringMatch {
|
||||||
|
candidate_id: 0,
|
||||||
|
score: 0.055384615384615386,
|
||||||
|
positions: vec![],
|
||||||
|
string: "select_to_start_of_excerpt".to_string(),
|
||||||
|
},
|
||||||
|
is_snippet: false,
|
||||||
|
sort_text: Some("7fffffff"),
|
||||||
|
sort_key: (3, "select_to_start_of_excerpt"),
|
||||||
|
},
|
||||||
|
SortableMatch {
|
||||||
|
string_match: StringMatch {
|
||||||
|
candidate_id: 0,
|
||||||
|
score: 0.0464516129032258,
|
||||||
|
positions: vec![],
|
||||||
|
string: "select_to_start_of_next_excerpt".to_string(),
|
||||||
|
},
|
||||||
|
is_snippet: false,
|
||||||
|
sort_text: Some("7fffffff"),
|
||||||
|
sort_key: (3, "select_to_start_of_next_excerpt"),
|
||||||
|
},
|
||||||
|
SortableMatch {
|
||||||
|
string_match: StringMatch {
|
||||||
|
candidate_id: 0,
|
||||||
|
score: 0.04363636363636363,
|
||||||
|
positions: vec![],
|
||||||
|
string: "select_to_end_of_previous_excerpt".to_string(),
|
||||||
|
},
|
||||||
|
is_snippet: false,
|
||||||
|
sort_text: Some("7fffffff"),
|
||||||
|
sort_key: (3, "select_to_end_of_previous_excerpt"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top);
|
||||||
|
assert_eq!(
|
||||||
|
matches
|
||||||
|
.iter()
|
||||||
|
.map(|m| m.string_match.string.as_str())
|
||||||
|
.collect::<Vec<&str>>(),
|
||||||
|
vec![
|
||||||
|
"set_text",
|
||||||
|
"set_context_menu_options",
|
||||||
|
"set_placeholder_text",
|
||||||
|
"set_text_style_refinement",
|
||||||
|
"select_to_end_of_excerpt",
|
||||||
|
"select_to_end_of_previous_excerpt",
|
||||||
|
"select_to_next_subword_end",
|
||||||
|
"select_to_next_word_end",
|
||||||
|
"select_to_start_of_excerpt",
|
||||||
|
"select_to_start_of_next_excerpt",
|
||||||
|
"set_custom_context_menu"
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -671,7 +671,7 @@ impl CompletionsMenu {
|
||||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
enum MatchTier<'a> {
|
enum MatchTier<'a> {
|
||||||
WordStartMatch {
|
WordStartMatch {
|
||||||
sort_score_int: Reverse<i32>,
|
sort_bucket: Reverse<i32>,
|
||||||
sort_snippet: Reverse<i32>,
|
sort_snippet: Reverse<i32>,
|
||||||
sort_text: Option<&'a str>,
|
sort_text: Option<&'a str>,
|
||||||
sort_key: (usize, &'a str),
|
sort_key: (usize, &'a str),
|
||||||
|
@ -684,11 +684,9 @@ impl CompletionsMenu {
|
||||||
// Our goal here is to intelligently sort completion suggestions. We want to
|
// Our goal here is to intelligently sort completion suggestions. We want to
|
||||||
// balance the raw fuzzy match score with hints from the language server
|
// balance the raw fuzzy match score with hints from the language server
|
||||||
//
|
//
|
||||||
// We first primary sort using fuzzy score by putting matches into two buckets
|
// We first primary sort using fuzzy score by putting matches into multiple
|
||||||
// strong one and weak one. Among these buckets matches are then compared by
|
// buckets. Among these buckets matches are then compared by
|
||||||
// various criteria like snippet, LSP hints, kind, label text etc.
|
// various criteria like snippet, LSP hints, kind, label text etc.
|
||||||
//
|
|
||||||
const FUZZY_THRESHOLD: f64 = 0.1317;
|
|
||||||
|
|
||||||
let query_start_lower = query
|
let query_start_lower = query
|
||||||
.and_then(|q| q.chars().next())
|
.and_then(|q| q.chars().next())
|
||||||
|
@ -712,14 +710,20 @@ impl CompletionsMenu {
|
||||||
let sort_score = Reverse(OrderedFloat(score));
|
let sort_score = Reverse(OrderedFloat(score));
|
||||||
MatchTier::OtherMatch { sort_score }
|
MatchTier::OtherMatch { sort_score }
|
||||||
} else {
|
} else {
|
||||||
let sort_score_int = Reverse(if score >= FUZZY_THRESHOLD { 1 } else { 0 });
|
// Convert fuzzy match score (0.0-1.0) to a priority bucket (0-3)
|
||||||
|
let sort_bucket = Reverse(match (score * 10.0).floor() as i32 {
|
||||||
|
s if s >= 7 => 3,
|
||||||
|
s if s >= 1 => 2,
|
||||||
|
s if s > 0 => 1,
|
||||||
|
_ => 0,
|
||||||
|
});
|
||||||
let sort_snippet = match snippet_sort_order {
|
let sort_snippet = match snippet_sort_order {
|
||||||
SnippetSortOrder::Top => Reverse(if mat.is_snippet { 1 } else { 0 }),
|
SnippetSortOrder::Top => Reverse(if mat.is_snippet { 1 } else { 0 }),
|
||||||
SnippetSortOrder::Bottom => Reverse(if mat.is_snippet { 0 } else { 1 }),
|
SnippetSortOrder::Bottom => Reverse(if mat.is_snippet { 0 } else { 1 }),
|
||||||
SnippetSortOrder::Inline => Reverse(0),
|
SnippetSortOrder::Inline => Reverse(0),
|
||||||
};
|
};
|
||||||
MatchTier::WordStartMatch {
|
MatchTier::WordStartMatch {
|
||||||
sort_score_int,
|
sort_bucket,
|
||||||
sort_snippet,
|
sort_snippet,
|
||||||
sort_text: mat.sort_text,
|
sort_text: mat.sort_text,
|
||||||
sort_key: mat.sort_key,
|
sort_key: mat.sort_key,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue