diff --git a/crates/editor/src/code_completion_tests.rs b/crates/editor/src/code_completion_tests.rs index 9c6c5dc014..5fd2ddf0fa 100644 --- a/crates/editor/src/code_completion_tests.rs +++ b/crates/editor/src/code_completion_tests.rs @@ -90,12 +90,12 @@ fn test_sort_matches_local_variable_over_global_variable(_cx: &mut TestAppContex ); assert_eq!( matches[2].string_match.string.as_str(), - "floorf128", + "floorf16", "Match order not expected" ); assert_eq!( matches[3].string_match.string.as_str(), - "floorf16", + "floorf32", "Match order not expected" ); @@ -433,7 +433,51 @@ fn test_sort_matches_for_unreachable(_cx: &mut TestAppContext) { "Match order not expected" ); - // Case 4: "unreachable" + // Case 4: "unreachabl" + let query: Option<&str> = Some("unreachable"); + let mut matches: Vec> = vec![ + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.9090909090909092, + positions: vec![], + string: "unreachable".to_string(), + }, + is_snippet: false, + sort_text: Some("80000000"), + sort_key: (3, "unreachable"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.6666666666666666, + positions: vec![], + string: "unreachable!(…)".to_string(), + }, + is_snippet: false, + sort_text: Some("7fffffff"), + sort_key: (3, "unreachable!(…)"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.47619047619047616, + positions: vec![], + string: "unreachable_unchecked".to_string(), + }, + is_snippet: false, + sort_text: Some("80000000"), + sort_key: (3, "unreachable_unchecked"), + }, + ]; + CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); + assert_eq!( + matches[0].string_match.string.as_str(), + "unreachable!(…)", + "Match order not expected" + ); + + // Case 5: "unreachable" let query: Option<&str> = Some("unreachable"); let mut matches: Vec> = vec![ SortableMatch { @@ -956,17 +1000,17 @@ fn test_sort_matches_jsx_event_handler(_cx: &mut TestAppContext) { .collect::>(), vec![ "onAbort?", + "onAuxClick?", "onAbortCapture?", "onAnimationEnd?", - "onAnimationEndCapture?", - "onAnimationIteration?", "onAnimationStart?", - "onAuxClick?", "onAuxClickCapture?", - "onCanPlay?", - "onChange?", + "onAnimationIteration?", + "onAnimationEndCapture?", "onDrag?", - "onDragEnd?", + "onLoad?", + "onPlay?", + "onPaste?", ] ); } @@ -985,7 +1029,7 @@ fn test_sort_matches_for_snippets(_cx: &mut TestAppContext) { }, is_snippet: false, sort_text: Some("80000000"), - sort_key: (2, "unreachable"), + sort_key: (2, "println"), }, SortableMatch { string_match: StringMatch { @@ -1142,16 +1186,148 @@ fn test_sort_matches_for_exact_match(_cx: &mut TestAppContext) { .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", + "set_placeholder_text", + "set_context_menu_options", + "set_custom_context_menu", "select_to_next_word_end", + "select_to_next_subword_end", + "select_to_end_of_excerpt", "select_to_start_of_excerpt", "select_to_start_of_next_excerpt", - "set_custom_context_menu" + "select_to_end_of_previous_excerpt", + ] + ); +} + +#[gpui::test] +fn test_sort_matches_for_prefix_matches(_cx: &mut TestAppContext) { + // Case 1: "set" + let query: Option<&str> = Some("set"); + let mut matches: Vec> = vec![ + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.12631578947368421, + positions: vec![], + string: "select_to_beginning".to_string(), + }, + is_snippet: false, + sort_text: Some("7fffffff"), + sort_key: (3, "select_to_beginning"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.15000000000000002, + positions: vec![], + string: "set_collapse_matches".to_string(), + }, + is_snippet: false, + sort_text: Some("7fffffff"), + sort_key: (3, "set_collapse_matches"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.21428571428571427, + positions: vec![], + string: "set_autoindent".to_string(), + }, + is_snippet: false, + sort_text: Some("7fffffff"), + sort_key: (3, "set_autoindent"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.11538461538461539, + positions: vec![], + string: "set_all_diagnostics_active".to_string(), + }, + is_snippet: false, + sort_text: Some("7fffffff"), + sort_key: (3, "set_all_diagnostics_active"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.1142857142857143, + positions: vec![], + string: "select_to_end_of_line".to_string(), + }, + is_snippet: false, + sort_text: Some("7fffffff"), + sort_key: (3, "select_to_end_of_line"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.15000000000000002, + positions: vec![], + string: "select_all".to_string(), + }, + is_snippet: false, + sort_text: Some("7fffffff"), + sort_key: (3, "select_all"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.13636363636363635, + positions: vec![], + string: "select_line".to_string(), + }, + is_snippet: false, + sort_text: Some("7fffffff"), + sort_key: (3, "select_line"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.13636363636363635, + positions: vec![], + string: "select_left".to_string(), + }, + is_snippet: false, + sort_text: Some("7fffffff"), + sort_key: (3, "select_left"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.13636363636363635, + positions: vec![], + string: "select_down".to_string(), + }, + is_snippet: false, + sort_text: Some("7fffffff"), + sort_key: (3, "select_down"), + }, + ]; + CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top); + println!( + "{:?}", + matches + .iter() + .map(|m| m.string_match.string.as_str()) + .collect::>(), + ); + assert_eq!( + matches + .iter() + .map(|m| m.string_match.string.as_str()) + .collect::>(), + vec![ + "set_autoindent", + "set_collapse_matches", + "set_all_diagnostics_active", + "select_all", + "select_down", + "select_left", + "select_line", + "select_to_beginning", + "select_to_end_of_line", ] ); } diff --git a/crates/editor/src/code_context_menus.rs b/crates/editor/src/code_context_menus.rs index e47d783638..e85297e1e6 100644 --- a/crates/editor/src/code_context_menus.rs +++ b/crates/editor/src/code_context_menus.rs @@ -673,9 +673,10 @@ impl CompletionsMenu { #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] enum MatchTier<'a> { WordStartMatch { - sort_bucket: Reverse, + sort_prefix: Reverse, sort_snippet: Reverse, sort_text: Option<&'a str>, + sort_score: Reverse>, sort_key: (usize, &'a str), }, OtherMatch { @@ -685,10 +686,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 - // - // 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. let query_start_lower = query .and_then(|q| q.chars().next()) @@ -696,8 +693,9 @@ impl CompletionsMenu { matches.sort_unstable_by_key(|mat| { let score = mat.string_match.score; + let sort_score = Reverse(OrderedFloat(score)); - let is_other_match = query_start_lower + let query_start_doesnt_match_split_words = query_start_lower .map(|query_char| { !split_words(&mat.string_match.string).any(|word| { word.chars() @@ -708,26 +706,38 @@ impl CompletionsMenu { }) .unwrap_or(false); - if is_other_match { - let sort_score = Reverse(OrderedFloat(score)); + if query_start_doesnt_match_split_words { MatchTier::OtherMatch { sort_score } } else { - // 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), }; + let mixed_case_prefix_length = Reverse( + query + .map(|q| { + q.chars() + .zip(mat.string_match.string.chars()) + .enumerate() + .take_while(|(i, (q_char, match_char))| { + if *i == 0 { + // Case-sensitive comparison for first character + q_char == match_char + } else { + // Case-insensitive comparison for other characters + q_char.to_lowercase().eq(match_char.to_lowercase()) + } + }) + .count() + }) + .unwrap_or(0), + ); MatchTier::WordStartMatch { - sort_bucket, + sort_prefix: mixed_case_prefix_length, sort_snippet, sort_text: mat.sort_text, + sort_score, sort_key: mat.sort_key, } } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 07bcbb40e1..44db047ded 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -10732,7 +10732,7 @@ async fn test_completion(cx: &mut TestAppContext) { .confirm_completion(&ConfirmCompletion::default(), window, cx) .unwrap() }); - cx.assert_editor_state("editor.clobberˇ"); + cx.assert_editor_state("editor.closeˇ"); handle_resolve_completion_request(&mut cx, None).await; apply_additional_edits.await.unwrap(); } @@ -13982,7 +13982,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestA { assert_eq!( completion_menu_entries(&menu), - &["bg-blue", "bg-red", "bg-yellow"] + &["bg-red", "bg-blue", "bg-yellow"] ); } else { panic!("expected completion menu to be open");