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

Smit Barmase created

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.

Change summary

crates/agent/src/context_picker/completion_provider.rs         |   1 
crates/agent/src/context_picker/symbol_context_picker.rs       |   2 
crates/agent/src/context_picker/thread_context_picker.rs       |   1 
crates/agent/src/thread_history.rs                             |   1 
crates/assistant_context_editor/src/context_store.rs           |   1 
crates/assistant_context_editor/src/language_model_selector.rs |   1 
crates/assistant_context_editor/src/slash_command.rs           |   1 
crates/assistant_slash_commands/src/diagnostics_command.rs     |   1 
crates/assistant_slash_commands/src/tab_command.rs             |   1 
crates/collab_ui/src/chat_panel/message_editor.rs              |   1 
crates/collab_ui/src/collab_panel.rs                           |   8 
crates/collab_ui/src/collab_panel/channel_modal.rs             |   1 
crates/command_palette/src/command_palette.rs                  |   1 
crates/debugger_ui/src/attach_modal.rs                         |   1 
crates/debugger_ui/src/new_process_modal.rs                    |   1 
crates/debugger_ui/src/session/running/console.rs              |   1 
crates/editor/src/code_completion_tests.rs                     | 590 +--
crates/editor/src/code_context_menus.rs                        |  45 
crates/editor/src/editor.rs                                    |   1 
crates/editor/src/editor_tests.rs                              |   4 
crates/extensions_ui/src/extension_version_selector.rs         |   1 
crates/extensions_ui/src/extensions_ui.rs                      |   1 
crates/file_finder/src/open_path_prompt.rs                     |   1 
crates/fuzzy/src/matcher.rs                                    |  13 
crates/fuzzy/src/paths.rs                                      |   4 
crates/fuzzy/src/strings.rs                                    |  10 
crates/git_ui/src/branch_picker.rs                             |   1 
crates/git_ui/src/picker_prompt.rs                             |   1 
crates/indexed_docs/src/store.rs                               |   1 
crates/jj_ui/src/bookmark_picker.rs                            |   1 
crates/language/src/outline.rs                                 |   1 
crates/language_selector/src/language_selector.rs              |   1 
crates/outline_panel/src/outline_panel.rs                      |   1 
crates/project/src/project.rs                                  |  15 
crates/project_symbols/src/project_symbols.rs                  |   3 
crates/prompt_store/src/prompt_store.rs                        |   1 
crates/recent_projects/src/recent_projects.rs                  |   1 
crates/snippets_ui/src/snippets_ui.rs                          |   1 
crates/storybook/src/stories/picker.rs                         |   1 
crates/tab_switcher/src/tab_switcher.rs                        |   1 
crates/tasks_ui/src/modal.rs                                   |   1 
crates/theme_selector/src/icon_theme_selector.rs               |   1 
crates/theme_selector/src/theme_selector.rs                    |   1 
crates/toolchain_selector/src/toolchain_selector.rs            |   1 
crates/welcome/src/base_keymap_picker.rs                       |   1 
45 files changed, 301 insertions(+), 428 deletions(-)

Detailed changes

crates/agent/src/context_picker/symbol_context_picker.rs πŸ”—

@@ -307,6 +307,7 @@ pub(crate) fn search_symbols(
             &visible_match_candidates,
             &query,
             false,
+            true,
             MAX_MATCHES,
             &cancellation_flag,
             cx.background_executor().clone(),
@@ -315,6 +316,7 @@ pub(crate) fn search_symbols(
             &external_match_candidates,
             &query,
             false,
+            true,
             MAX_MATCHES - visible_matches.len().min(MAX_MATCHES),
             &cancellation_flag,
             cx.background_executor().clone(),

crates/collab_ui/src/collab_panel.rs πŸ”—

@@ -499,6 +499,7 @@ impl CollabPanel {
                         &self.match_candidates,
                         &query,
                         true,
+                        true,
                         usize::MAX,
                         &Default::default(),
                         executor.clone(),
@@ -542,6 +543,7 @@ impl CollabPanel {
                     &self.match_candidates,
                     &query,
                     true,
+                    true,
                     usize::MAX,
                     &Default::default(),
                     executor.clone(),
@@ -593,6 +595,7 @@ impl CollabPanel {
                     &self.match_candidates,
                     &query,
                     true,
+                    true,
                     usize::MAX,
                     &Default::default(),
                     executor.clone(),
@@ -623,6 +626,7 @@ impl CollabPanel {
                 &self.match_candidates,
                 &query,
                 true,
+                true,
                 usize::MAX,
                 &Default::default(),
                 executor.clone(),
@@ -699,6 +703,7 @@ impl CollabPanel {
                 &self.match_candidates,
                 &query,
                 true,
+                true,
                 usize::MAX,
                 &Default::default(),
                 executor.clone(),
@@ -734,6 +739,7 @@ impl CollabPanel {
                 &self.match_candidates,
                 &query,
                 true,
+                true,
                 usize::MAX,
                 &Default::default(),
                 executor.clone(),
@@ -758,6 +764,7 @@ impl CollabPanel {
                 &self.match_candidates,
                 &query,
                 true,
+                true,
                 usize::MAX,
                 &Default::default(),
                 executor.clone(),
@@ -791,6 +798,7 @@ impl CollabPanel {
                 &self.match_candidates,
                 &query,
                 true,
+                true,
                 usize::MAX,
                 &Default::default(),
                 executor.clone(),

crates/editor/src/code_completion_tests.rs πŸ”—

@@ -1,5 +1,5 @@
 use crate::{code_context_menus::CompletionsMenu, editor_settings::SnippetSortOrder};
-use fuzzy::StringMatchCandidate;
+use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::TestAppContext;
 use language::CodeLabel;
 use lsp::{CompletionItem, CompletionItemKind, LanguageServerId};
@@ -9,432 +9,258 @@ use std::sync::atomic::AtomicBool;
 use text::Anchor;
 
 #[gpui::test]
-async fn test_sort_matches_local_variable_over_global_variable(cx: &mut TestAppContext) {
-    // Case 1: "foo"
+async fn test_sort_kind(cx: &mut TestAppContext) {
     let completions = vec![
-        CompletionBuilder::constant("foo_bar_baz", "7fffffff"),
-        CompletionBuilder::variable("foo_bar_qux", "7ffffffe"),
-        CompletionBuilder::constant("floorf64", "80000000"),
-        CompletionBuilder::constant("floorf32", "80000000"),
-        CompletionBuilder::constant("floorf16", "80000000"),
-        CompletionBuilder::constant("floorf128", "80000000"),
+        CompletionBuilder::function("floorf128", None, "80000000"),
+        CompletionBuilder::constant("foo_bar_baz", None, "80000000"),
+        CompletionBuilder::variable("foo_bar_qux", None, "80000000"),
     ];
-    let matches = sort_matches("foo", &completions, SnippetSortOrder::default(), cx).await;
-    assert_eq!(matches[0], "foo_bar_qux");
-    assert_eq!(matches[1], "foo_bar_baz");
-    assert_eq!(matches[2], "floorf16");
-    assert_eq!(matches[3], "floorf32");
-
-    // Case 2: "foobar"
-    let completions = vec![
-        CompletionBuilder::constant("foo_bar_baz", "7fffffff"),
-        CompletionBuilder::variable("foo_bar_qux", "7ffffffe"),
-    ];
-    let matches = sort_matches("foobar", &completions, SnippetSortOrder::default(), cx).await;
-    assert_eq!(matches[0], "foo_bar_qux");
-    assert_eq!(matches[1], "foo_bar_baz");
-}
-
-#[gpui::test]
-async fn test_sort_matches_local_variable_over_global_enum(cx: &mut TestAppContext) {
-    // Case 1: "ele"
-    let completions = vec![
-        CompletionBuilder::constant("ElementType", "7fffffff"),
-        CompletionBuilder::variable("element_type", "7ffffffe"),
-        CompletionBuilder::constant("simd_select", "80000000"),
-        CompletionBuilder::keyword("while let", "7fffffff"),
-    ];
-    let matches = sort_matches("ele", &completions, SnippetSortOrder::default(), cx).await;
-    assert_eq!(matches[0], "element_type");
-    assert_eq!(matches[1], "ElementType");
-
-    // Case 2: "eleme"
-    let completions = vec![
-        CompletionBuilder::constant("ElementType", "7fffffff"),
-        CompletionBuilder::variable("element_type", "7ffffffe"),
-        CompletionBuilder::constant("REPLACEMENT_CHARACTER", "80000000"),
-    ];
-    let matches = sort_matches("eleme", &completions, SnippetSortOrder::default(), cx).await;
-    assert_eq!(matches[0], "element_type");
-    assert_eq!(matches[1], "ElementType");
-}
-
-#[gpui::test]
-async fn test_sort_matches_capitalization(cx: &mut TestAppContext) {
-    // Case 1: "Elem"
-    let completions = vec![
-        CompletionBuilder::constant("ElementType", "7fffffff"),
-        CompletionBuilder::variable("element_type", "7ffffffe"),
-    ];
-    let matches = sort_matches("Elem", &completions, SnippetSortOrder::default(), cx).await;
-    assert_eq!(matches[0], "ElementType");
-    assert_eq!(matches[1], "element_type");
-}
-
-#[gpui::test]
-async fn test_sort_matches_for_unreachable(cx: &mut TestAppContext) {
-    // Case 1: "unre"
-    let completions = vec![
-        CompletionBuilder::function("unreachable", "80000000"),
-        CompletionBuilder::function("unreachable!(…)", "7fffffff"),
-        CompletionBuilder::function("unchecked_rem", "80000000"),
-        CompletionBuilder::function("unreachable_unchecked", "80000000"),
-    ];
-    let matches = sort_matches("unre", &completions, SnippetSortOrder::default(), cx).await;
-    assert_eq!(matches[0], "unreachable!(…)");
-
-    // Case 2: "unrea"
-    let completions = vec![
-        CompletionBuilder::function("unreachable", "80000000"),
-        CompletionBuilder::function("unreachable!(…)", "7fffffff"),
-        CompletionBuilder::function("unreachable_unchecked", "80000000"),
-    ];
-    let matches = sort_matches("unrea", &completions, SnippetSortOrder::default(), cx).await;
-    assert_eq!(matches[0], "unreachable!(…)");
-
-    // Case 3: "unreach"
-    let completions = vec![
-        CompletionBuilder::function("unreachable", "80000000"),
-        CompletionBuilder::function("unreachable!(…)", "7fffffff"),
-        CompletionBuilder::function("unreachable_unchecked", "80000000"),
-    ];
-    let matches = sort_matches("unreach", &completions, SnippetSortOrder::default(), cx).await;
-    assert_eq!(matches[0], "unreachable!(…)");
-
-    // Case 4: "unreachabl"
-    let completions = vec![
-        CompletionBuilder::function("unreachable", "80000000"),
-        CompletionBuilder::function("unreachable!(…)", "7fffffff"),
-        CompletionBuilder::function("unreachable_unchecked", "80000000"),
-    ];
-    let matches = sort_matches("unreachable", &completions, SnippetSortOrder::default(), cx).await;
-    assert_eq!(matches[0], "unreachable!(…)");
-
-    // Case 5: "unreachable"
-    let completions = vec![
-        CompletionBuilder::function("unreachable", "80000000"),
-        CompletionBuilder::function("unreachable!(…)", "7fffffff"),
-        CompletionBuilder::function("unreachable_unchecked", "80000000"),
-    ];
-    let matches = sort_matches("unreachable", &completions, SnippetSortOrder::default(), cx).await;
-    assert_eq!(matches[0], "unreachable!(…)");
-}
-
-#[gpui::test]
-async fn test_sort_matches_variable_and_constants_over_function(cx: &mut TestAppContext) {
-    // Case 1: "var" as variable
-    let completions = vec![
-        CompletionBuilder::function("var", "7fffffff"),
-        CompletionBuilder::variable("var", "7fffffff"),
-    ];
-    let matches = sort_matches("var", &completions, SnippetSortOrder::default(), cx).await;
-    assert_eq!(matches[0], "var");
-    assert_eq!(matches[1], "var");
-
-    // Case 2: "var" as constant
-    let completions = vec![
-        CompletionBuilder::function("var", "7fffffff"),
-        CompletionBuilder::constant("var", "7fffffff"),
-    ];
-    let matches = sort_matches("var", &completions, SnippetSortOrder::default(), cx).await;
-    assert_eq!(matches[0], "var");
-    assert_eq!(matches[1], "var");
-}
-
-#[gpui::test]
-async fn test_sort_matches_for_jsx_event_handler(cx: &mut TestAppContext) {
-    // Case 1: "on"
-    let completions = vec![
-        CompletionBuilder::function("onCut?", "12"),
-        CompletionBuilder::function("onPlay?", "12"),
-        CompletionBuilder::function("color?", "12"),
-        CompletionBuilder::function("defaultValue?", "12"),
-        CompletionBuilder::function("style?", "12"),
-        CompletionBuilder::function("className?", "12"),
-    ];
-    let matches = sort_matches("on", &completions, SnippetSortOrder::default(), cx).await;
-    assert_eq!(matches[0], "onCut?");
-    assert_eq!(matches[1], "onPlay?");
-
-    // Case 2: "ona"
-    let completions = vec![
-        CompletionBuilder::function("onAbort?", "12"),
-        CompletionBuilder::function("onAuxClick?", "12"),
-        CompletionBuilder::function("onPlay?", "12"),
-        CompletionBuilder::function("onLoad?", "12"),
-        CompletionBuilder::function("onDrag?", "12"),
-        CompletionBuilder::function("onPause?", "12"),
-        CompletionBuilder::function("onPaste?", "12"),
-        CompletionBuilder::function("onAnimationEnd?", "12"),
-        CompletionBuilder::function("onAbortCapture?", "12"),
-        CompletionBuilder::function("onChange?", "12"),
-        CompletionBuilder::function("onWaiting?", "12"),
-        CompletionBuilder::function("onCanPlay?", "12"),
-    ];
-    let matches = sort_matches("ona", &completions, SnippetSortOrder::default(), cx).await;
-    assert_eq!(matches[0], "onAbort?");
-    assert_eq!(matches[1], "onAuxClick?");
-}
+    let matches =
+        filter_and_sort_matches("foo", &completions, SnippetSortOrder::default(), cx).await;
+
+    // variable takes precedence over constant
+    // constant take precedence over function
+    assert_eq!(
+        matches
+            .iter()
+            .map(|m| m.string.as_str())
+            .collect::<Vec<_>>(),
+        vec!["foo_bar_qux", "foo_bar_baz", "floorf128"]
+    );
 
-#[gpui::test]
-async fn test_sort_matches_for_snippets(cx: &mut TestAppContext) {
-    // Case 1: "prin"
-    let completions = vec![
-        CompletionBuilder::constant("println", "80000000"),
-        CompletionBuilder::snippet("println!(…)", "80000000"),
-    ];
-    let matches = sort_matches("prin", &completions, SnippetSortOrder::Top, cx).await;
-    assert_eq!(matches[0], "println!(…)");
+    // fuzzy score should match for first two items as query is common prefix
+    assert_eq!(matches[0].score, matches[1].score);
 }
 
 #[gpui::test]
-async fn test_sort_matches_for_exact_match(cx: &mut TestAppContext) {
-    // Case 1: "set_text"
-    let completions = vec![
-        CompletionBuilder::function("set_text", "7fffffff"),
-        CompletionBuilder::function("set_placeholder_text", "7fffffff"),
-        CompletionBuilder::function("set_text_style_refinement", "7fffffff"),
-        CompletionBuilder::function("set_context_menu_options", "7fffffff"),
-        CompletionBuilder::function("select_to_next_word_end", "7fffffff"),
-        CompletionBuilder::function("select_to_next_subword_end", "7fffffff"),
-        CompletionBuilder::function("set_custom_context_menu", "7fffffff"),
-        CompletionBuilder::function("select_to_end_of_excerpt", "7fffffff"),
-        CompletionBuilder::function("select_to_start_of_excerpt", "7fffffff"),
-        CompletionBuilder::function("select_to_start_of_next_excerpt", "7fffffff"),
-        CompletionBuilder::function("select_to_end_of_previous_excerpt", "7fffffff"),
-    ];
-    let matches = sort_matches("set_text", &completions, SnippetSortOrder::Top, cx).await;
-    assert_eq!(matches[0], "set_text");
-    assert_eq!(matches[1], "set_text_style_refinement");
-    assert_eq!(matches[2], "set_context_menu_options");
-}
+async fn test_fuzzy_score(cx: &mut TestAppContext) {
+    // first character sensitive over sort_text and sort_kind
+    {
+        let completions = vec![
+            CompletionBuilder::variable("element_type", None, "7ffffffe"),
+            CompletionBuilder::constant("ElementType", None, "7fffffff"),
+        ];
+        let matches =
+            filter_and_sort_matches("Elem", &completions, SnippetSortOrder::default(), cx).await;
+        assert_eq!(
+            matches
+                .iter()
+                .map(|m| m.string.as_str())
+                .collect::<Vec<_>>(),
+            vec!["ElementType", "element_type"]
+        );
+        assert!(matches[0].score > matches[1].score);
+    }
 
-#[gpui::test]
-async fn test_sort_matches_for_prefix_matches(cx: &mut TestAppContext) {
-    // Case 1: "set"
-    let completions = vec![
-        CompletionBuilder::function("select_to_beginning", "7fffffff"),
-        CompletionBuilder::function("set_collapse_matches", "7fffffff"),
-        CompletionBuilder::function("set_autoindent", "7fffffff"),
-        CompletionBuilder::function("set_all_diagnostics_active", "7fffffff"),
-        CompletionBuilder::function("select_to_end_of_line", "7fffffff"),
-        CompletionBuilder::function("select_all", "7fffffff"),
-        CompletionBuilder::function("select_line", "7fffffff"),
-        CompletionBuilder::function("select_left", "7fffffff"),
-        CompletionBuilder::function("select_down", "7fffffff"),
-    ];
-    let matches = sort_matches("set", &completions, SnippetSortOrder::Top, cx).await;
-    assert_eq!(matches[0], "set_autoindent");
-    assert_eq!(matches[1], "set_collapse_matches");
-    assert_eq!(matches[2], "set_all_diagnostics_active");
-}
+    // fuzzy takes over sort_text and sort_kind
+    {
+        let completions = vec![
+            CompletionBuilder::function("onAbort?", None, "12"),
+            CompletionBuilder::function("onAuxClick?", None, "12"),
+            CompletionBuilder::variable("onPlay?", None, "12"),
+            CompletionBuilder::variable("onLoad?", None, "12"),
+            CompletionBuilder::variable("onDrag?", None, "12"),
+            CompletionBuilder::function("onPause?", None, "10"),
+            CompletionBuilder::function("onPaste?", None, "10"),
+            CompletionBuilder::function("onAnimationEnd?", None, "12"),
+            CompletionBuilder::function("onAbortCapture?", None, "12"),
+            CompletionBuilder::constant("onChange?", None, "12"),
+            CompletionBuilder::constant("onWaiting?", None, "12"),
+            CompletionBuilder::function("onCanPlay?", None, "12"),
+        ];
+        let matches =
+            filter_and_sort_matches("ona", &completions, SnippetSortOrder::default(), cx).await;
+        for i in 0..4 {
+            assert!(matches[i].string.to_lowercase().starts_with("ona"));
+        }
+    }
 
-#[gpui::test]
-async fn test_sort_matches_for_await(cx: &mut TestAppContext) {
-    // Case 1: "awa"
-    let completions = vec![
-        CompletionBuilder::keyword("await", "7fffffff"),
-        CompletionBuilder::function("await.ne", "80000010"),
-        CompletionBuilder::function("await.eq", "80000010"),
-        CompletionBuilder::function("await.or", "7ffffff8"),
-        CompletionBuilder::function("await.zip", "80000006"),
-        CompletionBuilder::function("await.xor", "7ffffff8"),
-        CompletionBuilder::function("await.and", "80000006"),
-        CompletionBuilder::function("await.map", "80000006"),
-        CompletionBuilder::function("await.take", "7ffffff8"),
-    ];
-    let matches = sort_matches("awa", &completions, SnippetSortOrder::Top, cx).await;
-    assert_eq!(matches[0], "await");
+    // plain fuzzy prefix match
+    {
+        let completions = vec![
+            CompletionBuilder::function("set_text", None, "7fffffff"),
+            CompletionBuilder::function("set_placeholder_text", None, "7fffffff"),
+            CompletionBuilder::function("set_text_style_refinement", None, "7fffffff"),
+            CompletionBuilder::function("set_context_menu_options", None, "7fffffff"),
+            CompletionBuilder::function("select_to_next_word_end", None, "7fffffff"),
+            CompletionBuilder::function("select_to_next_subword_end", None, "7fffffff"),
+            CompletionBuilder::function("set_custom_context_menu", None, "7fffffff"),
+            CompletionBuilder::function("select_to_end_of_excerpt", None, "7fffffff"),
+            CompletionBuilder::function("select_to_start_of_excerpt", None, "7fffffff"),
+            CompletionBuilder::function("select_to_start_of_next_excerpt", None, "7fffffff"),
+            CompletionBuilder::function("select_to_end_of_previous_excerpt", None, "7fffffff"),
+        ];
+        let matches =
+            filter_and_sort_matches("set_text", &completions, SnippetSortOrder::Top, cx).await;
+        assert_eq!(matches[0].string, "set_text");
+        assert_eq!(matches[1].string, "set_text_style_refinement");
+        assert_eq!(matches[2].string, "set_context_menu_options");
+    }
 
-    // Case 2: "await"
-    let completions = vec![
-        CompletionBuilder::keyword("await", "7fffffff"),
-        CompletionBuilder::function("await.ne", "80000010"),
-        CompletionBuilder::function("await.eq", "80000010"),
-        CompletionBuilder::function("await.or", "7ffffff8"),
-        CompletionBuilder::function("await.zip", "80000006"),
-        CompletionBuilder::function("await.xor", "7ffffff8"),
-        CompletionBuilder::function("await.and", "80000006"),
-        CompletionBuilder::function("await.map", "80000006"),
-        CompletionBuilder::function("await.take", "7ffffff8"),
-    ];
-    let matches = sort_matches("await", &completions, SnippetSortOrder::Top, cx).await;
-    assert_eq!(matches[0], "await");
+    // fuzzy filter text over label, sort_text and sort_kind
+    {
+        // Case 1: "awa"
+        let completions = vec![
+            CompletionBuilder::method("await", Some("await"), "7fffffff"),
+            CompletionBuilder::method("await.ne", Some("ne"), "80000010"),
+            CompletionBuilder::method("await.eq", Some("eq"), "80000010"),
+            CompletionBuilder::method("await.or", Some("or"), "7ffffff8"),
+            CompletionBuilder::method("await.zip", Some("zip"), "80000006"),
+            CompletionBuilder::method("await.xor", Some("xor"), "7ffffff8"),
+            CompletionBuilder::method("await.and", Some("and"), "80000006"),
+            CompletionBuilder::method("await.map", Some("map"), "80000006"),
+        ];
+
+        test_for_each_prefix("await", &completions, cx, |matches| {
+            // for each prefix, first item should always be one with lower sort_text
+            assert_eq!(matches[0].string, "await");
+        })
+        .await;
+    }
 }
 
 #[gpui::test]
-async fn test_sort_matches_for_python_init(cx: &mut TestAppContext) {
-    // Case 1: "__in"
-    let completions = vec![
-        CompletionBuilder::function("__init__", "05.0003.__init__"),
-        CompletionBuilder::function("__init__", "05.0003"),
-        CompletionBuilder::function("__instancecheck__", "05.0005.__instancecheck__"),
-        CompletionBuilder::function("__init_subclass__", "05.0004.__init_subclass__"),
-        CompletionBuilder::function("__instancecheck__", "05.0005"),
-        CompletionBuilder::function("__init_subclass__", "05.0004"),
-    ];
-    let matches = sort_matches("__in", &completions, SnippetSortOrder::Top, cx).await;
-    assert_eq!(matches[0], "__init__");
-    assert_eq!(matches[1], "__init__");
-
-    // Case 2: "__ini"
-    let completions = vec![
-        CompletionBuilder::function("__init__", "05.0004.__init__"),
-        CompletionBuilder::function("__init__", "05.0004"),
-        CompletionBuilder::function("__init_subclass__", "05.0003.__init_subclass__"),
-        CompletionBuilder::function("__init_subclass__", "05.0003"),
-    ];
-    let matches = sort_matches("__ini", &completions, SnippetSortOrder::Top, cx).await;
-    assert_eq!(matches[0], "__init__");
-    assert_eq!(matches[1], "__init__");
-
-    // Case 3: "__init"
-    let completions = vec![
-        CompletionBuilder::function("__init__", "05.0000.__init__"),
-        CompletionBuilder::function("__init__", "05.0000"),
-        CompletionBuilder::function("__init_subclass__", "05.0001.__init_subclass__"),
-        CompletionBuilder::function("__init_subclass__", "05.0001"),
-    ];
-    let matches = sort_matches("__init", &completions, SnippetSortOrder::Top, cx).await;
-    assert_eq!(matches[0], "__init__");
-    assert_eq!(matches[1], "__init__");
-
-    // Case 4: "__init_"
-    let completions = vec![
-        CompletionBuilder::function("__init__", "11.9999.__init__"),
-        CompletionBuilder::function("__init__", "11.9999"),
-        CompletionBuilder::function("__init_subclass__", "05.0000.__init_subclass__"),
-        CompletionBuilder::function("__init_subclass__", "05.0000"),
-    ];
-    let matches = sort_matches("__init_", &completions, SnippetSortOrder::Top, cx).await;
-    assert_eq!(matches[0], "__init__");
-    assert_eq!(matches[1], "__init__");
+async fn test_sort_text(cx: &mut TestAppContext) {
+    // sort text takes precedance over sort_kind, when fuzzy is same
+    {
+        let completions = vec![
+            CompletionBuilder::variable("unreachable", None, "80000000"),
+            CompletionBuilder::function("unreachable!(…)", None, "7fffffff"),
+            CompletionBuilder::function("unchecked_rem", None, "80000010"),
+            CompletionBuilder::function("unreachable_unchecked", None, "80000020"),
+        ];
+
+        test_for_each_prefix("unreachabl", &completions, cx, |matches| {
+            // for each prefix, first item should always be one with lower sort_text
+            assert_eq!(matches[0].string, "unreachable!(…)");
+            assert_eq!(matches[1].string, "unreachable");
+
+            // fuzzy score should match for first two items as query is common prefix
+            assert_eq!(matches[0].score, matches[1].score);
+        })
+        .await;
+
+        let matches =
+            filter_and_sort_matches("unreachable", &completions, SnippetSortOrder::Top, cx).await;
+        // exact match comes first
+        assert_eq!(matches[0].string, "unreachable");
+        assert_eq!(matches[1].string, "unreachable!(…)");
+
+        // fuzzy score should match for first two items as query is common prefix
+        assert_eq!(matches[0].score, matches[1].score);
+    }
 }
 
 #[gpui::test]
-async fn test_sort_matches_for_rust_into(cx: &mut TestAppContext) {
-    // Case 1: "int"
+async fn test_sort_snippet(cx: &mut TestAppContext) {
     let completions = vec![
-        CompletionBuilder::function("into", "80000004"),
-        CompletionBuilder::function("try_into", "80000004"),
-        CompletionBuilder::snippet("println", "80000004"),
-        CompletionBuilder::function("clone_into", "80000004"),
-        CompletionBuilder::function("into_searcher", "80000000"),
-        CompletionBuilder::snippet("eprintln", "80000004"),
+        CompletionBuilder::constant("println", None, "7fffffff"),
+        CompletionBuilder::snippet("println!(…)", None, "80000000"),
     ];
-    let matches = sort_matches("int", &completions, SnippetSortOrder::default(), cx).await;
-    assert_eq!(matches[0], "into");
+    let matches = filter_and_sort_matches("prin", &completions, SnippetSortOrder::Top, cx).await;
 
-    // Case 2: "into"
-    let completions = vec![
-        CompletionBuilder::function("into", "80000004"),
-        CompletionBuilder::function("try_into", "80000004"),
-        CompletionBuilder::function("clone_into", "80000004"),
-        CompletionBuilder::function("into_searcher", "80000000"),
-        CompletionBuilder::function("split_terminator", "7fffffff"),
-        CompletionBuilder::function("rsplit_terminator", "7fffffff"),
-    ];
-    let matches = sort_matches("into", &completions, SnippetSortOrder::default(), cx).await;
-    assert_eq!(matches[0], "into");
+    // snippet take precedence over sort_text and sort_kind
+    assert_eq!(matches[0].string, "println!(…)");
 }
 
 #[gpui::test]
-async fn test_sort_matches_for_variable_over_function(cx: &mut TestAppContext) {
-    // Case 1: "serial"
+async fn test_sort_exact(cx: &mut TestAppContext) {
+    // sort_text takes over if no exact match
     let completions = vec![
-        CompletionBuilder::function("serialize", "80000000"),
-        CompletionBuilder::function("serialize", "80000000"),
-        CompletionBuilder::variable("serialization_key", "7ffffffe"),
-        CompletionBuilder::function("serialize_version", "80000000"),
-        CompletionBuilder::function("deserialize", "80000000"),
+        CompletionBuilder::function("into", None, "80000004"),
+        CompletionBuilder::function("try_into", None, "80000004"),
+        CompletionBuilder::snippet("println", None, "80000004"),
+        CompletionBuilder::function("clone_into", None, "80000004"),
+        CompletionBuilder::function("into_searcher", None, "80000000"),
+        CompletionBuilder::snippet("eprintln", None, "80000004"),
     ];
-    let matches = sort_matches("serial", &completions, SnippetSortOrder::default(), cx).await;
-    assert_eq!(matches[0], "serialization_key");
-    assert_eq!(matches[1], "serialize");
-    assert_eq!(matches[2], "serialize");
-    assert_eq!(matches[3], "serialize_version");
-    assert_eq!(matches[4], "deserialize");
-}
+    let matches =
+        filter_and_sort_matches("int", &completions, SnippetSortOrder::default(), cx).await;
+    assert_eq!(matches[0].string, "into_searcher");
 
-#[gpui::test]
-async fn test_sort_matches_for_local_methods_over_library(cx: &mut TestAppContext) {
-    // Case 1: "setis"
+    // exact match takes over sort_text
     let completions = vec![
-        CompletionBuilder::variable("setISODay", "16"),
-        CompletionBuilder::variable("setISOWeek", "16"),
-        CompletionBuilder::variable("setISOWeekYear", "16"),
-        CompletionBuilder::function("setISOWeekYear", "16"),
-        CompletionBuilder::variable("setIsRefreshing", "11"),
-        CompletionBuilder::function("setFips", "16"),
+        CompletionBuilder::function("into", None, "80000004"),
+        CompletionBuilder::function("try_into", None, "80000004"),
+        CompletionBuilder::function("clone_into", None, "80000004"),
+        CompletionBuilder::function("into_searcher", None, "80000000"),
+        CompletionBuilder::function("split_terminator", None, "7fffffff"),
+        CompletionBuilder::function("rsplit_terminator", None, "7fffffff"),
     ];
-    let matches = sort_matches("setis", &completions, SnippetSortOrder::default(), cx).await;
-    assert_eq!(matches[0], "setIsRefreshing");
-    assert_eq!(matches[1], "setISODay");
-    assert_eq!(matches[2], "setISOWeek");
+    let matches =
+        filter_and_sort_matches("into", &completions, SnippetSortOrder::default(), cx).await;
+    assert_eq!(matches[0].string, "into");
 }
 
 #[gpui::test]
-async fn test_sort_matches_for_prioritize_not_exact_match(cx: &mut TestAppContext) {
-    // Case 1: "item"
+async fn test_sort_positions(cx: &mut TestAppContext) {
+    // positions take precedence over fuzzy score and sort_text
     let completions = vec![
-        CompletionBuilder::function("Item", "16"),
-        CompletionBuilder::variable("Item", "16"),
-        CompletionBuilder::variable("items", "11"),
-        CompletionBuilder::function("ItemText", "16"),
+        CompletionBuilder::function("rounded-full", None, "15788"),
+        CompletionBuilder::variable("rounded-t-full", None, "15846"),
+        CompletionBuilder::variable("rounded-b-full", None, "15731"),
+        CompletionBuilder::function("rounded-tr-full", None, "15866"),
     ];
-    let matches = sort_matches("item", &completions, SnippetSortOrder::default(), cx).await;
-    assert_eq!(matches[0], "items");
-    assert_eq!(matches[1], "Item");
-    assert_eq!(matches[2], "Item");
-    assert_eq!(matches[3], "ItemText");
-}
 
-#[gpui::test]
-async fn test_sort_matches_for_tailwind_classes(cx: &mut TestAppContext) {
-    let completions = vec![
-        CompletionBuilder::function("rounded-full", "15788"),
-        CompletionBuilder::variable("rounded-t-full", "15846"),
-        CompletionBuilder::variable("rounded-b-full", "15731"),
-        CompletionBuilder::function("rounded-tr-full", "15866"),
-    ];
-    // Case 1: "rounded-full"
-    let matches = sort_matches(
+    let matches = filter_and_sort_matches(
         "rounded-full",
         &completions,
         SnippetSortOrder::default(),
         cx,
     )
     .await;
-    assert_eq!(matches[0], "rounded-full");
-    // Case 2: "roundedfull"
-    let matches = sort_matches("roundedfull", &completions, SnippetSortOrder::default(), cx).await;
-    assert_eq!(matches[0], "rounded-full");
+    assert_eq!(matches[0].string, "rounded-full");
+
+    let matches =
+        filter_and_sort_matches("roundedfull", &completions, SnippetSortOrder::default(), cx).await;
+    assert_eq!(matches[0].string, "rounded-full");
+}
+
+async fn test_for_each_prefix<F>(
+    target: &str,
+    completions: &Vec<Completion>,
+    cx: &mut TestAppContext,
+    mut test_fn: F,
+) where
+    F: FnMut(Vec<StringMatch>),
+{
+    for i in 1..=target.len() {
+        let prefix = &target[..i];
+        let matches =
+            filter_and_sort_matches(prefix, completions, SnippetSortOrder::default(), cx).await;
+        test_fn(matches);
+    }
 }
 
 struct CompletionBuilder;
 
 impl CompletionBuilder {
-    fn constant(label: &str, sort_text: &str) -> Completion {
-        Self::new(label, sort_text, CompletionItemKind::CONSTANT)
+    fn constant(label: &str, filter_text: Option<&str>, sort_text: &str) -> Completion {
+        Self::new(label, filter_text, sort_text, CompletionItemKind::CONSTANT)
     }
 
-    fn function(label: &str, sort_text: &str) -> Completion {
-        Self::new(label, sort_text, CompletionItemKind::FUNCTION)
+    fn function(label: &str, filter_text: Option<&str>, sort_text: &str) -> Completion {
+        Self::new(label, filter_text, sort_text, CompletionItemKind::FUNCTION)
     }
 
-    fn variable(label: &str, sort_text: &str) -> Completion {
-        Self::new(label, sort_text, CompletionItemKind::VARIABLE)
+    fn method(label: &str, filter_text: Option<&str>, sort_text: &str) -> Completion {
+        Self::new(label, filter_text, sort_text, CompletionItemKind::METHOD)
     }
 
-    fn keyword(label: &str, sort_text: &str) -> Completion {
-        Self::new(label, sort_text, CompletionItemKind::KEYWORD)
+    fn variable(label: &str, filter_text: Option<&str>, sort_text: &str) -> Completion {
+        Self::new(label, filter_text, sort_text, CompletionItemKind::VARIABLE)
     }
 
-    fn snippet(label: &str, sort_text: &str) -> Completion {
-        Self::new(label, sort_text, CompletionItemKind::SNIPPET)
+    fn snippet(label: &str, filter_text: Option<&str>, sort_text: &str) -> Completion {
+        Self::new(label, filter_text, sort_text, CompletionItemKind::SNIPPET)
     }
 
-    fn new(label: &str, sort_text: &str, kind: CompletionItemKind) -> Completion {
+    fn new(
+        label: &str,
+        filter_text: Option<&str>,
+        sort_text: &str,
+        kind: CompletionItemKind,
+    ) -> Completion {
         Completion {
             replace_range: Anchor::MIN..Anchor::MAX,
             new_text: label.to_string(),
@@ -451,6 +277,7 @@ impl CompletionBuilder {
                     label: label.to_string(),
                     kind: Some(kind),
                     sort_text: Some(sort_text.to_string()),
+                    filter_text: filter_text.map(|text| text.to_string()),
                     ..Default::default()
                 }),
                 lsp_defaults: None,
@@ -463,16 +290,16 @@ impl CompletionBuilder {
     }
 }
 
-async fn sort_matches(
+async fn filter_and_sort_matches(
     query: &str,
     completions: &Vec<Completion>,
     snippet_sort_order: SnippetSortOrder,
     cx: &mut TestAppContext,
-) -> Vec<String> {
+) -> Vec<StringMatch> {
     let candidates: Arc<[StringMatchCandidate]> = completions
         .iter()
         .enumerate()
-        .map(|(id, completion)| StringMatchCandidate::new(id, &completion.label.text))
+        .map(|(id, completion)| StringMatchCandidate::new(id, &completion.filter_text()))
         .collect();
     let cancel_flag = Arc::new(AtomicBool::new(false));
     let background_executor = cx.executor();
@@ -480,16 +307,11 @@ async fn sort_matches(
         &candidates,
         query,
         query.chars().any(|c| c.is_uppercase()),
+        false,
         100,
         &cancel_flag,
         background_executor,
     )
     .await;
-    let sorted_matches = CompletionsMenu::sort_string_matches(
-        matches,
-        Some(query),
-        snippet_sort_order,
-        &completions,
-    );
-    sorted_matches.into_iter().map(|m| m.string).collect()
+    CompletionsMenu::sort_string_matches(matches, Some(query), snippet_sort_order, &completions)
 }

crates/editor/src/code_context_menus.rs πŸ”—

@@ -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,
                 }
             }

crates/editor/src/editor.rs πŸ”—

@@ -21221,6 +21221,7 @@ fn snippet_completions(
                 &candidates,
                 &last_word,
                 last_word.chars().any(|c| c.is_uppercase()),
+                true,
                 MAX_RESULTS,
                 &Default::default(),
                 executor.clone(),

crates/editor/src/editor_tests.rs πŸ”—

@@ -11950,7 +11950,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();
 }
@@ -15468,7 +15468,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/fuzzy/src/matcher.rs πŸ”—

@@ -17,6 +17,7 @@ pub struct Matcher<'a> {
     lowercase_query: &'a [char],
     query_char_bag: CharBag,
     smart_case: bool,
+    penalize_length: bool,
     min_score: f64,
     match_positions: Vec<usize>,
     last_positions: Vec<usize>,
@@ -35,6 +36,7 @@ impl<'a> Matcher<'a> {
         lowercase_query: &'a [char],
         query_char_bag: CharBag,
         smart_case: bool,
+        penalize_length: bool,
     ) -> Self {
         Self {
             query,
@@ -46,6 +48,7 @@ impl<'a> Matcher<'a> {
             score_matrix: Vec::new(),
             best_position_matrix: Vec::new(),
             smart_case,
+            penalize_length,
         }
     }
 
@@ -294,7 +297,7 @@ impl<'a> Matcher<'a> {
                 let mut multiplier = char_score;
 
                 // Scale the score based on how deep within the path we found the match.
-                if query_idx == 0 {
+                if self.penalize_length && query_idx == 0 {
                     multiplier /= ((prefix.len() + path.len()) - last_slash) as f64;
                 }
 
@@ -355,18 +358,18 @@ mod tests {
     #[test]
     fn test_get_last_positions() {
         let mut query: &[char] = &['d', 'c'];
-        let mut matcher = Matcher::new(query, query, query.into(), false);
+        let mut matcher = Matcher::new(query, query, query.into(), false, true);
         let result = matcher.find_last_positions(&['a', 'b', 'c'], &['b', 'd', 'e', 'f']);
         assert!(!result);
 
         query = &['c', 'd'];
-        let mut matcher = Matcher::new(query, query, query.into(), false);
+        let mut matcher = Matcher::new(query, query, query.into(), false, true);
         let result = matcher.find_last_positions(&['a', 'b', 'c'], &['b', 'd', 'e', 'f']);
         assert!(result);
         assert_eq!(matcher.last_positions, vec![2, 4]);
 
         query = &['z', '/', 'z', 'f'];
-        let mut matcher = Matcher::new(query, query, query.into(), false);
+        let mut matcher = Matcher::new(query, query, query.into(), false, true);
         let result = matcher.find_last_positions(&['z', 'e', 'd', '/'], &['z', 'e', 'd', '/', 'f']);
         assert!(result);
         assert_eq!(matcher.last_positions, vec![0, 3, 4, 8]);
@@ -613,7 +616,7 @@ mod tests {
             });
         }
 
-        let mut matcher = Matcher::new(&query, &lowercase_query, query_chars, smart_case);
+        let mut matcher = Matcher::new(&query, &lowercase_query, query_chars, smart_case, true);
 
         let cancel_flag = AtomicBool::new(false);
         let mut results = Vec::new();

crates/fuzzy/src/paths.rs πŸ”—

@@ -95,7 +95,7 @@ pub fn match_fixed_path_set(
     let query = query.chars().collect::<Vec<_>>();
     let query_char_bag = CharBag::from(&lowercase_query[..]);
 
-    let mut matcher = Matcher::new(&query, &lowercase_query, query_char_bag, smart_case);
+    let mut matcher = Matcher::new(&query, &lowercase_query, query_char_bag, smart_case, true);
 
     let mut results = Vec::new();
     matcher.match_candidates(
@@ -153,7 +153,7 @@ pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
                     let segment_start = segment_idx * segment_size;
                     let segment_end = segment_start + segment_size;
                     let mut matcher =
-                        Matcher::new(query, lowercase_query, query_char_bag, smart_case);
+                        Matcher::new(query, lowercase_query, query_char_bag, smart_case, true);
 
                     let mut tree_start = 0;
                     for candidate_set in candidate_sets {

crates/fuzzy/src/strings.rs πŸ”—

@@ -117,6 +117,7 @@ pub async fn match_strings<T>(
     candidates: &[T],
     query: &str,
     smart_case: bool,
+    penalize_length: bool,
     max_results: usize,
     cancel_flag: &AtomicBool,
     executor: BackgroundExecutor,
@@ -160,8 +161,13 @@ where
                 scope.spawn(async move {
                     let segment_start = cmp::min(segment_idx * segment_size, candidates.len());
                     let segment_end = cmp::min(segment_start + segment_size, candidates.len());
-                    let mut matcher =
-                        Matcher::new(query, lowercase_query, query_char_bag, smart_case);
+                    let mut matcher = Matcher::new(
+                        query,
+                        lowercase_query,
+                        query_char_bag,
+                        smart_case,
+                        penalize_length,
+                    );
 
                     matcher.match_candidates(
                         &[],

crates/project/src/project.rs πŸ”—

@@ -5303,6 +5303,16 @@ impl ProjectItem for Buffer {
 }
 
 impl Completion {
+    pub fn filter_text(&self) -> &str {
+        match &self.source {
+            CompletionSource::Lsp { lsp_completion, .. } => lsp_completion
+                .filter_text
+                .as_deref()
+                .unwrap_or_else(|| self.label.filter_text()),
+            _ => self.label.filter_text(),
+        }
+    }
+
     pub fn kind(&self) -> Option<CompletionItemKind> {
         self.source
             // `lsp::CompletionListItemDefaults` has no `kind` field
@@ -5319,17 +5329,18 @@ 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 = 3;
+        const DEFAULT_KIND_KEY: usize = 4;
         let kind_key = self
             .kind()
             .and_then(|lsp_completion_kind| match lsp_completion_kind {
                 lsp::CompletionItemKind::KEYWORD => Some(0),
                 lsp::CompletionItemKind::VARIABLE => Some(1),
                 lsp::CompletionItemKind::CONSTANT => Some(2),
+                lsp::CompletionItemKind::PROPERTY => Some(3),
                 _ => None,
             })
             .unwrap_or(DEFAULT_KIND_KEY);
-        (kind_key, &self.label.text[self.label.filter_range.clone()])
+        (kind_key, &self.label.filter_text())
     }
 
     /// Whether this completion is a snippet.

crates/project_symbols/src/project_symbols.rs πŸ”—

@@ -66,6 +66,7 @@ impl ProjectSymbolsDelegate {
             &self.visible_match_candidates,
             query,
             false,
+            true,
             MAX_MATCHES,
             &Default::default(),
             cx.background_executor().clone(),
@@ -74,6 +75,7 @@ impl ProjectSymbolsDelegate {
             &self.external_match_candidates,
             query,
             false,
+            true,
             MAX_MATCHES - visible_matches.len().min(MAX_MATCHES),
             &Default::default(),
             cx.background_executor().clone(),
@@ -342,6 +344,7 @@ mod tests {
                             &candidates,
                             &params.query,
                             true,
+                            true,
                             100,
                             &Default::default(),
                             executor.clone(),

crates/tasks_ui/src/modal.rs πŸ”—

@@ -358,6 +358,7 @@ impl PickerDelegate for TasksModalDelegate {
                 &candidates,
                 &query,
                 true,
+                true,
                 1000,
                 &Default::default(),
                 cx.background_executor().clone(),