From 3961d87ae04d4e5ed1c79f3a2e8ca11eede9b2c0 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Sat, 26 Apr 2025 02:14:18 +0530 Subject: [PATCH] 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: before After: after Release Notes: - N/A --- crates/editor/src/code_completion_tests.rs | 149 +++++++++++++++++++++ crates/editor/src/code_context_menus.rs | 18 ++- 2 files changed, 160 insertions(+), 7 deletions(-) diff --git a/crates/editor/src/code_completion_tests.rs b/crates/editor/src/code_completion_tests.rs index 866fc4575f..9c6c5dc014 100644 --- a/crates/editor/src/code_completion_tests.rs +++ b/crates/editor/src/code_completion_tests.rs @@ -1006,3 +1006,152 @@ fn test_sort_matches_for_snippets(_cx: &mut TestAppContext) { "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> = 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![ + "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" + ] + ); +} diff --git a/crates/editor/src/code_context_menus.rs b/crates/editor/src/code_context_menus.rs index 28ae2eb771..37e1c9c7cd 100644 --- a/crates/editor/src/code_context_menus.rs +++ b/crates/editor/src/code_context_menus.rs @@ -671,7 +671,7 @@ impl CompletionsMenu { #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] enum MatchTier<'a> { WordStartMatch { - sort_score_int: Reverse, + sort_bucket: Reverse, sort_snippet: Reverse, sort_text: Option<&'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 // 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 - // strong one and weak one. Among these buckets matches are then compared by + // We first primary sort using fuzzy score by putting matches into multiple + // buckets. Among these buckets matches are then compared by // various criteria like snippet, LSP hints, kind, label text etc. - // - const FUZZY_THRESHOLD: f64 = 0.1317; let query_start_lower = query .and_then(|q| q.chars().next()) @@ -712,14 +710,20 @@ impl CompletionsMenu { let sort_score = Reverse(OrderedFloat(score)); MatchTier::OtherMatch { sort_score } } 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 { SnippetSortOrder::Top => Reverse(if mat.is_snippet { 1 } else { 0 }), SnippetSortOrder::Bottom => Reverse(if mat.is_snippet { 0 } else { 1 }), SnippetSortOrder::Inline => Reverse(0), }; MatchTier::WordStartMatch { - sort_score_int, + sort_bucket, sort_snippet, sort_text: mat.sort_text, sort_key: mat.sort_key,