editor: Use quantize score for code completions sort + Add code completions tests (#29182)

Smit Barmase created

Closes #27994, #29050, #27352, #27616

This PR implements new logic for code completions, which improve cases
where local variables, etc LSP based hints are not shown on top of code
completion menu. The new logic is explained in comment of code.

This new sort is similar to VSCode's completions sort where order of
sort is like:

Fuzzy > Snippet > LSP sort_key > LSP sort_text 

whenever two items have same value, it proceeds to use next one as tie
breaker. Changing fuzzy score from float to int based makes it possible
for two items two have same fuzzy int score, making them get sorted by
next criteria.

Release Notes:

- Improved code completions to prioritize LSP hints, such as local
variables, so they appear at the top of the list.

Change summary

crates/editor/src/code_completion_tests.rs | 1005 ++++++++++++++++++++++++
crates/editor/src/code_context_menus.rs    |  175 ++-
crates/editor/src/editor.rs                |    2 
crates/editor/src/editor_tests.rs          |   74 -
crates/project/src/project.rs              |    3 
5 files changed, 1,111 insertions(+), 148 deletions(-)

Detailed changes

crates/editor/src/code_completion_tests.rs 🔗

@@ -0,0 +1,1005 @@
+use crate::code_context_menus::{CompletionsMenu, SortableMatch};
+use fuzzy::StringMatch;
+use gpui::TestAppContext;
+
+#[gpui::test]
+fn test_sort_matches_local_variable_over_global_variable(_cx: &mut TestAppContext) {
+    // Case 1: "foo"
+    let query: Option<&str> = Some("foo");
+    let mut matches: Vec<SortableMatch<'_>> = vec![
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.2727272727272727,
+                positions: vec![],
+                string: "foo_bar_baz".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("7fffffff"),
+            sort_key: (2, "foo_bar_baz"),
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.2727272727272727,
+                positions: vec![],
+                string: "foo_bar_qux".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("7ffffffe"),
+            sort_key: (1, "foo_bar_qux"),
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.22499999999999998,
+                positions: vec![],
+                string: "floorf64".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("80000000"),
+            sort_key: (2, "floorf64"),
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.22499999999999998,
+                positions: vec![],
+                string: "floorf32".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("80000000"),
+            sort_key: (2, "floorf32"),
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.22499999999999998,
+                positions: vec![],
+                string: "floorf16".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("80000000"),
+            sort_key: (2, "floorf16"),
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.2,
+                positions: vec![],
+                string: "floorf128".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("80000000"),
+            sort_key: (2, "floorf128"),
+        },
+    ];
+    CompletionsMenu::sort_matches(&mut matches, query);
+    assert_eq!(
+        matches[0].string_match.string.as_str(),
+        "foo_bar_qux",
+        "Match order not expected"
+    );
+    assert_eq!(
+        matches[1].string_match.string.as_str(),
+        "foo_bar_baz",
+        "Match order not expected"
+    );
+    assert_eq!(
+        matches[2].string_match.string.as_str(),
+        "floorf128",
+        "Match order not expected"
+    );
+    assert_eq!(
+        matches[3].string_match.string.as_str(),
+        "floorf16",
+        "Match order not expected"
+    );
+
+    // Case 2: "foobar"
+    let query: Option<&str> = Some("foobar");
+    let mut matches: Vec<SortableMatch<'_>> = vec![
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.4363636363636364,
+                positions: vec![],
+                string: "foo_bar_baz".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("7fffffff"),
+            sort_key: (2, "foo_bar_baz"),
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.4363636363636364,
+                positions: vec![],
+                string: "foo_bar_qux".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("7ffffffe"),
+            sort_key: (1, "foo_bar_qux"),
+        },
+    ];
+    CompletionsMenu::sort_matches(&mut matches, query);
+    assert_eq!(
+        matches[0].string_match.string.as_str(),
+        "foo_bar_qux",
+        "Match order not expected"
+    );
+    assert_eq!(
+        matches[1].string_match.string.as_str(),
+        "foo_bar_baz",
+        "Match order not expected"
+    );
+}
+
+#[gpui::test]
+fn test_sort_matches_local_variable_over_global_enum(_cx: &mut TestAppContext) {
+    // Case 1: "ele"
+    let query: Option<&str> = Some("ele");
+    let mut matches: Vec<SortableMatch<'_>> = vec![
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.2727272727272727,
+                positions: vec![],
+                string: "ElementType".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("7fffffff"),
+            sort_key: (2, "ElementType"),
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.25,
+                positions: vec![],
+                string: "element_type".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("7ffffffe"),
+            sort_key: (1, "element_type"),
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.16363636363636364,
+                positions: vec![],
+                string: "simd_select".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("80000000"),
+            sort_key: (2, "simd_select"),
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.16,
+                positions: vec![],
+                string: "while let".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("7fffffff"),
+            sort_key: (0, "while let"),
+        },
+    ];
+    CompletionsMenu::sort_matches(&mut matches, query);
+    assert_eq!(
+        matches[0].string_match.string.as_str(),
+        "element_type",
+        "Match order not expected"
+    );
+    assert_eq!(
+        matches[1].string_match.string.as_str(),
+        "ElementType",
+        "Match order not expected"
+    );
+
+    // Case 2: "eleme"
+    let query: Option<&str> = Some("eleme");
+    let mut matches: Vec<SortableMatch<'_>> = vec![
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.4545454545454546,
+                positions: vec![],
+                string: "ElementType".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("7fffffff"),
+            sort_key: (2, "ElementType"),
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.41666666666666663,
+                positions: vec![],
+                string: "element_type".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("7ffffffe"),
+            sort_key: (1, "element_type"),
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.04714285714285713,
+                positions: vec![],
+                string: "REPLACEMENT_CHARACTER".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("80000000"),
+            sort_key: (2, "REPLACEMENT_CHARACTER"),
+        },
+    ];
+    CompletionsMenu::sort_matches(&mut matches, query);
+    assert_eq!(
+        matches[0].string_match.string.as_str(),
+        "element_type",
+        "Match order not expected"
+    );
+    assert_eq!(
+        matches[1].string_match.string.as_str(),
+        "ElementType",
+        "Match order not expected"
+    );
+
+    // Case 3: "Elem"
+    let query: Option<&str> = Some("Elem");
+    let mut matches: Vec<SortableMatch<'_>> = vec![
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.36363636363636365,
+                positions: vec![],
+                string: "ElementType".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("7fffffff"),
+            sort_key: (2, "ElementType"),
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.0003333333333333333,
+                positions: vec![],
+                string: "element_type".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("7ffffffe"),
+            sort_key: (1, "element_type"),
+        },
+    ];
+    CompletionsMenu::sort_matches(&mut matches, query);
+    assert_eq!(
+        matches[0].string_match.string.as_str(),
+        "ElementType",
+        "Match order not expected"
+    );
+    assert_eq!(
+        matches[1].string_match.string.as_str(),
+        "element_type",
+        "Match order not expected"
+    );
+}
+
+#[gpui::test]
+fn test_sort_matches_for_unreachable(_cx: &mut TestAppContext) {
+    // Case 1: "unre"
+    let query: Option<&str> = Some("unre");
+    let mut matches: Vec<SortableMatch<'_>> = vec![
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.36363636363636365,
+                positions: vec![],
+                string: "unreachable".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("80000000"),
+            sort_key: (2, "unreachable"),
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.26666666666666666,
+                positions: vec![],
+                string: "unreachable!(…)".to_string(),
+            },
+            is_snippet: true,
+            sort_text: Some("7fffffff"),
+            sort_key: (2, "unreachable!(…)"),
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.24615384615384617,
+                positions: vec![],
+                string: "unchecked_rem".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("80000000"),
+            sort_key: (2, "unchecked_rem"),
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.19047619047619047,
+                positions: vec![],
+                string: "unreachable_unchecked".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("80000000"),
+            sort_key: (2, "unreachable_unchecked"),
+        },
+    ];
+    CompletionsMenu::sort_matches(&mut matches, query);
+    assert_eq!(
+        matches[0].string_match.string.as_str(),
+        "unreachable!(…)",
+        "Match order not expected"
+    );
+
+    // Case 2: "unrea"
+    let query: Option<&str> = Some("unrea");
+    let mut matches: Vec<SortableMatch<'_>> = vec![
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.4545454545454546,
+                positions: vec![],
+                string: "unreachable".to_string(),
+            },
+            is_snippet: true,
+            sort_text: Some("80000000"),
+            sort_key: (3, "unreachable"),
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.3333333333333333,
+                positions: vec![],
+                string: "unreachable!(…)".to_string(),
+            },
+            is_snippet: true,
+            sort_text: Some("7fffffff"),
+            sort_key: (3, "unreachable!(…)"),
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.23809523809523808,
+                positions: vec![],
+                string: "unreachable_unchecked".to_string(),
+            },
+            is_snippet: true,
+            sort_text: Some("80000000"),
+            sort_key: (3, "unreachable_unchecked"),
+        },
+    ];
+    CompletionsMenu::sort_matches(&mut matches, query);
+    assert_eq!(
+        matches[0].string_match.string.as_str(),
+        "unreachable!(…)",
+        "Match order not expected"
+    );
+
+    // Case 3: "unreach"
+    let query: Option<&str> = Some("unreach");
+    let mut matches: Vec<SortableMatch<'_>> = vec![
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.6363636363636364,
+                positions: vec![],
+                string: "unreachable".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("80000000"),
+            sort_key: (2, "unreachable"),
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.4666666666666667,
+                positions: vec![],
+                string: "unreachable!(…)".to_string(),
+            },
+            is_snippet: true,
+            sort_text: Some("7fffffff"),
+            sort_key: (2, "unreachable!(…)"),
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.3333333333333333,
+                positions: vec![],
+                string: "unreachable_unchecked".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("80000000"),
+            sort_key: (2, "unreachable_unchecked"),
+        },
+    ];
+    CompletionsMenu::sort_matches(&mut matches, query);
+    assert_eq!(
+        matches[0].string_match.string.as_str(),
+        "unreachable!(…)",
+        "Match order not expected"
+    );
+
+    // Case 4: "unreachable"
+    let query: Option<&str> = Some("unreachable");
+    let mut matches: Vec<SortableMatch<'_>> = vec![
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 1.0,
+                positions: vec![],
+                string: "unreachable".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("80000000"),
+            sort_key: (2, "unreachable"),
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.7333333333333333,
+                positions: vec![],
+                string: "unreachable!(…)".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("7fffffff"),
+            sort_key: (2, "unreachable!(…)"),
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.5238095238095237,
+                positions: vec![],
+                string: "unreachable_unchecked".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("80000000"),
+            sort_key: (2, "unreachable_unchecked"),
+        },
+    ];
+    CompletionsMenu::sort_matches(&mut matches, query);
+    assert_eq!(
+        matches[0].string_match.string.as_str(),
+        "unreachable!(…)",
+        "Match order not expected"
+    );
+}
+
+#[gpui::test]
+fn test_sort_matches_variable_and_constants_over_function(_cx: &mut TestAppContext) {
+    // Case 1: "var" as variable
+    let query: Option<&str> = Some("var");
+    let mut matches: Vec<SortableMatch<'_>> = vec![
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 1.0,
+                positions: vec![],
+                string: "var".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("7fffffff"),
+            sort_key: (3, "var"), // function
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 1,
+                score: 1.0,
+                positions: vec![],
+                string: "var".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("7fffffff"),
+            sort_key: (1, "var"), // variable
+        },
+    ];
+    CompletionsMenu::sort_matches(&mut matches, query);
+    assert_eq!(
+        matches[0].string_match.candidate_id, 1,
+        "Match order not expected"
+    );
+    assert_eq!(
+        matches[1].string_match.candidate_id, 0,
+        "Match order not expected"
+    );
+
+    // Case 2:  "var" as constant
+    let query: Option<&str> = Some("var");
+    let mut matches: Vec<SortableMatch<'_>> = vec![
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 1.0,
+                positions: vec![],
+                string: "var".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("7fffffff"),
+            sort_key: (3, "var"), // function
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 1,
+                score: 1.0,
+                positions: vec![],
+                string: "var".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("7fffffff"),
+            sort_key: (2, "var"), // constant
+        },
+    ];
+    CompletionsMenu::sort_matches(&mut matches, query);
+    assert_eq!(
+        matches[0].string_match.candidate_id, 1,
+        "Match order not expected"
+    );
+    assert_eq!(
+        matches[1].string_match.candidate_id, 0,
+        "Match order not expected"
+    );
+}
+
+#[gpui::test]
+fn test_sort_matches_jsx_event_handler(_cx: &mut TestAppContext) {
+    // Case 1: "on"
+    let query: Option<&str> = Some("on");
+    let mut matches: Vec<SortableMatch<'_>> = vec![
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.3333333333333333,
+                positions: vec![],
+                string: "onCut?".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("12"),
+            sort_key: (3, "onCut?"),
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.2857142857142857,
+                positions: vec![],
+                string: "onPlay?".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("12"),
+            sort_key: (3, "onPlay?"),
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.25,
+                positions: vec![],
+                string: "color?".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("12"),
+            sort_key: (3, "color?"),
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.25,
+                positions: vec![],
+                string: "defaultValue?".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("12"),
+            sort_key: (3, "defaultValue?"),
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.25,
+                positions: vec![],
+                string: "style?".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("12"),
+            sort_key: (3, "style?"),
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.20,
+                positions: vec![],
+                string: "className?".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("12"),
+            sort_key: (3, "className?"),
+        },
+    ];
+    CompletionsMenu::sort_matches(&mut matches, query);
+    assert_eq!(
+        matches[0].string_match.string, "onCut?",
+        "Match order not expected"
+    );
+    assert_eq!(
+        matches[1].string_match.string, "onPlay?",
+        "Match order not expected"
+    );
+
+    // Case 2: "ona"
+    let query: Option<&str> = Some("ona");
+    let mut matches: Vec<SortableMatch<'_>> = vec![
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.375,
+                positions: vec![],
+                string: "onAbort?".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("12"),
+            sort_key: (3, "onAbort?"),
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.2727272727272727,
+                positions: vec![],
+                string: "onAuxClick?".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("12"),
+            sort_key: (3, "onAuxClick?"),
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.23571428571428565,
+                positions: vec![],
+                string: "onPlay?".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("12"),
+            sort_key: (3, "onPlay?"),
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.23571428571428565,
+                positions: vec![],
+                string: "onLoad?".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("12"),
+            sort_key: (3, "onLoad?"),
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.23571428571428565,
+                positions: vec![],
+                string: "onDrag?".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("12"),
+            sort_key: (3, "onDrag?"),
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.22499999999999998,
+                positions: vec![],
+                string: "onPause?".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("12"),
+            sort_key: (3, "onPause?"),
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.22499999999999998,
+                positions: vec![],
+                string: "onPaste?".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("12"),
+            sort_key: (3, "onPaste?"),
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.2,
+                positions: vec![],
+                string: "onAnimationEnd?".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("12"),
+            sort_key: (3, "onAnimationEnd?"),
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.2,
+                positions: vec![],
+                string: "onAbortCapture?".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("12"),
+            sort_key: (3, "onAbortCapture?"),
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.1833333333333333,
+                positions: vec![],
+                string: "onChange?".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("12"),
+            sort_key: (3, "onChange?"),
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.18,
+                positions: vec![],
+                string: "onWaiting?".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("12"),
+            sort_key: (3, "onWaiting?"),
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.18,
+                positions: vec![],
+                string: "onCanPlay?".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("12"),
+            sort_key: (3, "onCanPlay?"),
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.1764705882352941,
+                positions: vec![],
+                string: "onAnimationStart?".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("12"),
+            sort_key: (3, "onAnimationStart?"),
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.16666666666666666,
+                positions: vec![],
+                string: "onAuxClickCapture?".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("12"),
+            sort_key: (3, "onAuxClickCapture?"),
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.16499999999999998,
+                positions: vec![],
+                string: "onStalled?".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("12"),
+            sort_key: (3, "onStalled?"),
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.16499999999999998,
+                positions: vec![],
+                string: "onPlaying?".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("12"),
+            sort_key: (3, "onPlaying?"),
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.16499999999999998,
+                positions: vec![],
+                string: "onDragEnd?".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("12"),
+            sort_key: (3, "onDragEnd?"),
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.15000000000000002,
+                positions: vec![],
+                string: "onInvalid?".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("12"),
+            sort_key: (3, "onInvalid?"),
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.15,
+                positions: vec![],
+                string: "onDragOver?".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("12"),
+            sort_key: (3, "onDragOver?"),
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.15,
+                positions: vec![],
+                string: "onDragExit?".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("12"),
+            sort_key: (3, "onDragExit?"),
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.14285714285714285,
+                positions: vec![],
+                string: "onAnimationIteration?".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("12"),
+            sort_key: (3, "onAnimationIteration?"),
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.13846153846153847,
+                positions: vec![],
+                string: "onRateChange?".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("12"),
+            sort_key: (3, "onRateChange?"),
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.13749999999999996,
+                positions: vec![],
+                string: "onLoadStart?".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("12"),
+            sort_key: (3, "onLoadStart?"),
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.13749999999999996,
+                positions: vec![],
+                string: "onDragStart?".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("12"),
+            sort_key: (3, "onDragStart?"),
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.13749999999999996,
+                positions: vec![],
+                string: "onDragLeave?".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("12"),
+            sort_key: (3, "onDragLeave?"),
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.13749999999999996,
+                positions: vec![],
+                string: "onDragEnter?".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("12"),
+            sort_key: (3, "onDragEnter?"),
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.13636363636363635,
+                positions: vec![],
+                string: "onAnimationEndCapture?".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("12"),
+            sort_key: (3, "onAnimationEndCapture?"),
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.12692307692307692,
+                positions: vec![],
+                string: "onLoadedData?".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("12"),
+            sort_key: (3, "onLoadedData?"),
+        },
+    ];
+    CompletionsMenu::sort_matches(&mut matches, query);
+    assert_eq!(
+        matches
+            .iter()
+            .take(12)
+            .map(|m| m.string_match.string.as_str())
+            .collect::<Vec<&str>>(),
+        vec![
+            "onAbort?",
+            "onAbortCapture?",
+            "onAnimationEnd?",
+            "onAnimationEndCapture?",
+            "onAnimationIteration?",
+            "onAnimationStart?",
+            "onAuxClick?",
+            "onAuxClickCapture?",
+            "onCanPlay?",
+            "onChange?",
+            "onDrag?",
+            "onDragEnd?",
+        ]
+    );
+}
+
+#[gpui::test]
+fn test_sort_matches_for_snippets(_cx: &mut TestAppContext) {
+    // Case 1: "prin"
+    let query: Option<&str> = Some("prin");
+    let mut matches: Vec<SortableMatch<'_>> = vec![
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.2,
+                positions: vec![],
+                string: "println".to_string(),
+            },
+            is_snippet: false,
+            sort_text: Some("80000000"),
+            sort_key: (2, "unreachable"),
+        },
+        SortableMatch {
+            string_match: StringMatch {
+                candidate_id: 0,
+                score: 0.2,
+                positions: vec![],
+                string: "println!(…)".to_string(),
+            },
+            is_snippet: true,
+            sort_text: Some("80000000"),
+            sort_key: (2, "println!(…)"),
+        },
+    ];
+    CompletionsMenu::sort_matches(&mut matches, query);
+    assert_eq!(
+        matches[0].string_match.string.as_str(),
+        "println!(…)",
+        "Match order not expected"
+    );
+}

crates/editor/src/code_context_menus.rs 🔗

@@ -27,8 +27,8 @@ use util::ResultExt;
 
 use crate::hover_popover::{hover_markdown_style, open_markdown_url};
 use crate::{
-    CodeActionProvider, CompletionId, CompletionProvider, DisplayRow, Editor, EditorStyle,
-    ResolvedTasks,
+    CodeActionProvider, CompletionId, CompletionItemKind, CompletionProvider, DisplayRow, Editor,
+    EditorStyle, ResolvedTasks,
     actions::{ConfirmCodeAction, ConfirmCompletion},
     split_words, styled_runs_for_code_label,
 };
@@ -657,6 +657,63 @@ impl CompletionsMenu {
         )
     }
 
+    pub fn sort_matches(matches: &mut Vec<SortableMatch<'_>>, query: Option<&str>) {
+        #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
+        enum MatchTier<'a> {
+            WordStartMatch {
+                sort_score_int: Reverse<i32>,
+                sort_snippet: Reverse<i32>,
+                sort_text: Option<&'a str>,
+                sort_key: (usize, &'a str),
+            },
+            OtherMatch {
+                sort_score: Reverse<OrderedFloat<f64>>,
+            },
+        }
+
+        // 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
+        // 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())
+            .and_then(|c| c.to_lowercase().next());
+
+        matches.sort_unstable_by_key(|mat| {
+            let score = mat.string_match.score;
+
+            let is_other_match = query_start_lower
+                .map(|query_char| {
+                    !split_words(&mat.string_match.string).any(|word| {
+                        word.chars()
+                            .next()
+                            .and_then(|c| c.to_lowercase().next())
+                            .map_or(false, |word_char| word_char == query_char)
+                    })
+                })
+                .unwrap_or(false);
+
+            if is_other_match {
+                let sort_score = Reverse(OrderedFloat(score));
+                MatchTier::OtherMatch { sort_score }
+            } else {
+                let sort_score_int = Reverse(if score >= FUZZY_THRESHOLD { 1 } else { 0 });
+                let sort_snippet = Reverse(if mat.is_snippet { 1 } else { 0 });
+                MatchTier::WordStartMatch {
+                    sort_score_int,
+                    sort_snippet,
+                    sort_text: mat.sort_text,
+                    sort_key: mat.sort_key,
+                }
+            }
+        });
+    }
+
     pub async fn filter(&mut self, query: Option<&str>, executor: BackgroundExecutor) {
         let mut matches = if let Some(query) = query {
             fuzzy::match_strings(
@@ -681,85 +738,45 @@ impl CompletionsMenu {
                 .collect()
         };
 
-        let mut additional_matches = Vec::new();
-        // Deprioritize all candidates where the query's start does not match the start of any word in the candidate
-        if let Some(query) = query {
-            if let Some(query_start) = query.chars().next() {
-                let (primary, secondary) = matches.into_iter().partition(|string_match| {
-                    split_words(&string_match.string).any(|word| {
-                        // Check that the first codepoint of the word as lowercase matches the first
-                        // codepoint of the query as lowercase
-                        word.chars()
-                            .flat_map(|codepoint| codepoint.to_lowercase())
-                            .zip(query_start.to_lowercase())
-                            .all(|(word_cp, query_cp)| word_cp == query_cp)
-                    })
-                });
-                matches = primary;
-                additional_matches = secondary;
-            }
-        }
-
-        let completions = self.completions.borrow_mut();
         if self.sort_completions {
-            matches.sort_unstable_by_key(|mat| {
-                // We do want to strike a balance here between what the language server tells us
-                // to sort by (the sort_text) and what are "obvious" good matches (i.e. when you type
-                // `Creat` and there is a local variable called `CreateComponent`).
-                // So what we do is: we bucket all matches into two buckets
-                // - Strong matches
-                // - Weak matches
-                // Strong matches are the ones with a high fuzzy-matcher score (the "obvious" matches)
-                // and the Weak matches are the rest.
-                //
-                // For the strong matches, we sort by our fuzzy-finder score first and for the weak
-                // matches, we prefer language-server sort_text first.
-                //
-                // The thinking behind that: we want to show strong matches first in order of relevance(fuzzy score).
-                // Rest of the matches(weak) can be sorted as language-server expects.
-
-                #[derive(PartialEq, Eq, PartialOrd, Ord)]
-                enum MatchScore<'a> {
-                    Strong {
-                        score: Reverse<OrderedFloat<f64>>,
-                        sort_text: Option<&'a str>,
-                        sort_key: (usize, &'a str),
-                    },
-                    Weak {
-                        sort_text: Option<&'a str>,
-                        score: Reverse<OrderedFloat<f64>>,
-                        sort_key: (usize, &'a str),
-                    },
-                }
+            let completions = self.completions.borrow();
+
+            let mut sortable_items: Vec<SortableMatch<'_>> = matches
+                .into_iter()
+                .map(|string_match| {
+                    let completion = &completions[string_match.candidate_id];
+
+                    let is_snippet = matches!(
+                        &completion.source,
+                        CompletionSource::Lsp { lsp_completion, .. }
+                        if lsp_completion.kind == Some(CompletionItemKind::SNIPPET)
+                    );
+
+                    let sort_text =
+                        if let CompletionSource::Lsp { lsp_completion, .. } = &completion.source {
+                            lsp_completion.sort_text.as_deref()
+                        } else {
+                            None
+                        };
 
-                let completion = &completions[mat.candidate_id];
-                let sort_key = completion.sort_key();
-                let sort_text =
-                    if let CompletionSource::Lsp { lsp_completion, .. } = &completion.source {
-                        lsp_completion.sort_text.as_deref()
-                    } else {
-                        None
-                    };
-                let score = Reverse(OrderedFloat(mat.score));
-
-                if mat.score >= 0.2 {
-                    MatchScore::Strong {
-                        score,
-                        sort_text,
-                        sort_key,
-                    }
-                } else {
-                    MatchScore::Weak {
+                    let sort_key = completion.sort_key();
+
+                    SortableMatch {
+                        string_match,
+                        is_snippet,
                         sort_text,
-                        score,
                         sort_key,
                     }
-                }
-            });
-        }
-        drop(completions);
+                })
+                .collect();
+
+            Self::sort_matches(&mut sortable_items, query);
 
-        matches.extend(additional_matches);
+            matches = sortable_items
+                .into_iter()
+                .map(|sortable| sortable.string_match)
+                .collect();
+        }
 
         *self.entries.borrow_mut() = matches;
         self.selected_item = 0;
@@ -768,6 +785,14 @@ impl CompletionsMenu {
     }
 }
 
+#[derive(Debug)]
+pub struct SortableMatch<'a> {
+    pub string_match: StringMatch,
+    pub is_snippet: bool,
+    pub sort_text: Option<&'a str>,
+    pub sort_key: (usize, &'a str),
+}
+
 #[derive(Clone)]
 pub struct AvailableCodeAction {
     pub excerpt_id: ExcerptId,

crates/editor/src/editor.rs 🔗

@@ -39,6 +39,8 @@ pub mod scroll;
 mod selections_collection;
 pub mod tasks;
 
+#[cfg(test)]
+mod code_completion_tests;
 #[cfg(test)]
 mod editor_tests;
 #[cfg(test)]

crates/editor/src/editor_tests.rs 🔗

@@ -10704,7 +10704,7 @@ async fn test_completion(cx: &mut TestAppContext) {
             .confirm_completion(&ConfirmCompletion::default(), window, cx)
             .unwrap()
     });
-    cx.assert_editor_state("editor.closeˇ");
+    cx.assert_editor_state("editor.clobberˇ");
     handle_resolve_completion_request(&mut cx, None).await;
     apply_additional_edits.await.unwrap();
 }
@@ -11266,76 +11266,6 @@ async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
     });
 }
 
-#[gpui::test]
-async fn test_completion_sort(cx: &mut TestAppContext) {
-    init_test(cx, |_| {});
-    let mut cx = EditorLspTestContext::new_rust(
-        lsp::ServerCapabilities {
-            completion_provider: Some(lsp::CompletionOptions {
-                trigger_characters: Some(vec![".".to_string()]),
-                ..Default::default()
-            }),
-            ..Default::default()
-        },
-        cx,
-    )
-    .await;
-    cx.lsp
-        .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
-            Ok(Some(lsp::CompletionResponse::Array(vec![
-                lsp::CompletionItem {
-                    label: "Range".into(),
-                    sort_text: Some("a".into()),
-                    ..Default::default()
-                },
-                lsp::CompletionItem {
-                    label: "r".into(),
-                    sort_text: Some("b".into()),
-                    ..Default::default()
-                },
-                lsp::CompletionItem {
-                    label: "ret".into(),
-                    sort_text: Some("c".into()),
-                    ..Default::default()
-                },
-                lsp::CompletionItem {
-                    label: "return".into(),
-                    sort_text: Some("d".into()),
-                    ..Default::default()
-                },
-                lsp::CompletionItem {
-                    label: "slice".into(),
-                    sort_text: Some("d".into()),
-                    ..Default::default()
-                },
-            ])))
-        });
-    cx.set_state("rˇ");
-    cx.executor().run_until_parked();
-    cx.update_editor(|editor, window, cx| {
-        editor.show_completions(
-            &ShowCompletions {
-                trigger: Some("r".into()),
-            },
-            window,
-            cx,
-        );
-    });
-    cx.executor().run_until_parked();
-
-    cx.update_editor(|editor, _, _| {
-        if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
-        {
-            assert_eq!(
-                completion_menu_entries(&menu),
-                &["r", "ret", "Range", "return"]
-            );
-        } else {
-            panic!("expected completion menu to be open");
-        }
-    });
-}
-
 #[gpui::test]
 async fn test_as_is_completions(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
@@ -14061,7 +13991,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestA
         {
             assert_eq!(
                 completion_menu_entries(&menu),
-                &["bg-red", "bg-blue", "bg-yellow"]
+                &["bg-blue", "bg-red", "bg-yellow"]
             );
         } else {
             panic!("expected completion menu to be open");

crates/project/src/project.rs 🔗

@@ -5006,7 +5006,7 @@ impl Completion {
     /// A key that can be used to sort completions when displaying
     /// them to the user.
     pub fn sort_key(&self) -> (usize, &str) {
-        const DEFAULT_KIND_KEY: usize = 2;
+        const DEFAULT_KIND_KEY: usize = 3;
         let kind_key = self
             .source
             // `lsp::CompletionListItemDefaults` has no `kind` field
@@ -5015,6 +5015,7 @@ impl Completion {
             .and_then(|lsp_completion_kind| match lsp_completion_kind {
                 lsp::CompletionItemKind::KEYWORD => Some(0),
                 lsp::CompletionItemKind::VARIABLE => Some(1),
+                lsp::CompletionItemKind::CONSTANT => Some(2),
                 _ => None,
             })
             .unwrap_or(DEFAULT_KIND_KEY);