Improve sorting of completion results (#7727)

Thorsten Ball , Antonio , and bennetbo created

This is an attempt to fix #5013 by doing two things:

1. Rank "obvious" matches in completions higher (see the code comment)
2. When tied: rank keywords higher than variables

Release Notes:

- Improved sorting of completion results to prefer literal matches.
([#5013](https://github.com/zed-industries/zed/issues/5013)).

### Before

![screenshot-2024-02-13-13 08
13@2x](https://github.com/zed-industries/zed/assets/1185253/77decb0b-5b47-45de-ab69-f7b333072b45)
![screenshot-2024-02-13-13 10
42@2x](https://github.com/zed-industries/zed/assets/1185253/ae33d0fe-06f5-4fc1-84f8-ddf6dbe80ba5)


### After

![screenshot-2024-02-13-13 06
22@2x](https://github.com/zed-industries/zed/assets/1185253/3c526bab-6392-4eeb-a2f2-dd73ccf228e8)
![screenshot-2024-02-13-13 06
50@2x](https://github.com/zed-industries/zed/assets/1185253/b5b9d513-766d-4a53-94de-b46271f5978c)

Co-authored-by: Antonio <antonio@zed.dev>
Co-authored-by: bennetbo <bennetbo@gmx.de>

Change summary

crates/editor/src/editor.rs   | 51 +++++++++++++++++++++++++++++++++---
crates/language/src/buffer.rs |  5 ++-
2 files changed, 49 insertions(+), 7 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -1017,12 +1017,53 @@ impl CompletionsMenu {
 
         let completions = self.completions.read();
         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 the language-servers score first and for the weak
+            // matches, we prefer our fuzzy finder first.
+            //
+            // The thinking behind that: it's useless to take the sort_text the language-server gives
+            // us into account when it's obviously a bad match.
+
+            #[derive(PartialEq, Eq, PartialOrd, Ord)]
+            enum MatchScore<'a> {
+                Strong {
+                    sort_text: Option<&'a str>,
+                    score: Reverse<OrderedFloat<f64>>,
+                    sort_key: (usize, &'a str),
+                },
+                Weak {
+                    score: Reverse<OrderedFloat<f64>>,
+                    sort_text: Option<&'a str>,
+                    sort_key: (usize, &'a str),
+                },
+            }
+
             let completion = &completions[mat.candidate_id];
-            (
-                completion.lsp_completion.sort_text.as_ref(),
-                Reverse(OrderedFloat(mat.score)),
-                completion.sort_key(),
-            )
+            let sort_key = completion.sort_key();
+            let sort_text = completion.lsp_completion.sort_text.as_deref();
+            let score = Reverse(OrderedFloat(mat.score));
+
+            if mat.score >= 0.2 {
+                MatchScore::Strong {
+                    sort_text,
+                    score,
+                    sort_key,
+                }
+            } else {
+                MatchScore::Weak {
+                    score,
+                    sort_text,
+                    sort_key,
+                }
+            }
         });
 
         for mat in &mut matches {

crates/language/src/buffer.rs 🔗

@@ -3441,8 +3441,9 @@ impl Completion {
     /// them to the user.
     pub fn sort_key(&self) -> (usize, &str) {
         let kind_key = match self.lsp_completion.kind {
-            Some(lsp::CompletionItemKind::VARIABLE) => 0,
-            _ => 1,
+            Some(lsp::CompletionItemKind::KEYWORD) => 0,
+            Some(lsp::CompletionItemKind::VARIABLE) => 1,
+            _ => 2,
         };
         (kind_key, &self.label.text[self.label.filter_range.clone()])
     }