Disable forceful sorting of the slash command argument completions (#16240)

Kirill Bulatov created

Also bubble up the current active tab's path in the \tab argument
completions.

Release Notes:

- N/A

Change summary

crates/assistant/src/slash_command.rs             |   4 
crates/assistant/src/slash_command/tab_command.rs |  76 ++++++++---
crates/editor/src/editor.rs                       | 103 +++++++++-------
3 files changed, 114 insertions(+), 69 deletions(-)

Detailed changes

crates/assistant/src/slash_command.rs 🔗

@@ -346,6 +346,10 @@ impl CompletionProvider for SlashCommandCompletionProvider {
             false
         }
     }
+
+    fn sort_completions(&self) -> bool {
+        false
+    }
 }
 
 impl SlashCommandLine {

crates/assistant/src/slash_command/tab_command.rs 🔗

@@ -62,6 +62,16 @@ impl SlashCommand for TabSlashCommand {
         if has_all_tabs_completion_item {
             return Task::ready(Ok(Vec::new()));
         }
+
+        let active_item_path = workspace.as_ref().and_then(|workspace| {
+            workspace
+                .update(cx, |workspace, cx| {
+                    let snapshot = active_item_buffer(workspace, cx).ok()?;
+                    snapshot.resolve_file_path(cx, true)
+                })
+                .ok()
+                .flatten()
+        });
         let current_query = arguments.last().cloned().unwrap_or_default();
         let tab_items_search =
             tab_items_for_queries(workspace, &[current_query], cancel, false, cx);
@@ -73,6 +83,9 @@ impl SlashCommand for TabSlashCommand {
                 if argument_set.contains(&path_string) {
                     return None;
                 }
+                if active_item_path.is_some() && active_item_path == path {
+                    return None;
+                }
                 Some(ArgumentCompletion {
                     label: path_string.clone().into(),
                     new_text: path_string,
@@ -81,15 +94,26 @@ impl SlashCommand for TabSlashCommand {
                 })
             });
 
-            Ok(Some(ArgumentCompletion {
-                label: ALL_TABS_COMPLETION_ITEM.into(),
-                new_text: ALL_TABS_COMPLETION_ITEM.to_owned(),
-                replace_previous_arguments: false,
-                run_command: true,
-            })
-            .into_iter()
-            .chain(tab_completion_items)
-            .collect::<Vec<_>>())
+            let active_item_completion = active_item_path.as_deref().map(|active_item_path| {
+                let path_string = active_item_path.to_string_lossy().to_string();
+                ArgumentCompletion {
+                    label: path_string.clone().into(),
+                    new_text: path_string,
+                    replace_previous_arguments: false,
+                    run_command,
+                }
+            });
+
+            Ok(active_item_completion
+                .into_iter()
+                .chain(Some(ArgumentCompletion {
+                    label: ALL_TABS_COMPLETION_ITEM.into(),
+                    new_text: ALL_TABS_COMPLETION_ITEM.to_owned(),
+                    replace_previous_arguments: false,
+                    run_command: true,
+                }))
+                .chain(tab_completion_items)
+                .collect())
         })
     }
 
@@ -162,19 +186,7 @@ fn tab_items_for_queries(
                 .context("no workspace")?
                 .update(&mut cx, |workspace, cx| {
                     if strict_match && empty_query {
-                        let active_editor = workspace
-                            .active_item(cx)
-                            .context("no active item")?
-                            .downcast::<Editor>()
-                            .context("active item is not an editor")?;
-                        let snapshot = active_editor
-                            .read(cx)
-                            .buffer()
-                            .read(cx)
-                            .as_singleton()
-                            .context("active editor is not a singleton buffer")?
-                            .read(cx)
-                            .snapshot();
+                        let snapshot = active_item_buffer(workspace, cx)?;
                         let full_path = snapshot.resolve_file_path(cx, true);
                         return anyhow::Ok(vec![(full_path, snapshot, 0)]);
                     }
@@ -279,3 +291,23 @@ fn tab_items_for_queries(
             .await
     })
 }
+
+fn active_item_buffer(
+    workspace: &mut Workspace,
+    cx: &mut ui::ViewContext<Workspace>,
+) -> anyhow::Result<BufferSnapshot> {
+    let active_editor = workspace
+        .active_item(cx)
+        .context("no active item")?
+        .downcast::<Editor>()
+        .context("active item is not an editor")?;
+    let snapshot = active_editor
+        .read(cx)
+        .buffer()
+        .read(cx)
+        .as_singleton()
+        .context("active editor is not a singleton buffer")?
+        .read(cx)
+        .snapshot();
+    Ok(snapshot)
+}

crates/editor/src/editor.rs 🔗

@@ -900,6 +900,7 @@ enum ContextMenuOrigin {
 #[derive(Clone)]
 struct CompletionsMenu {
     id: CompletionId,
+    sort_completions: bool,
     initial_position: Anchor,
     buffer: Model<Buffer>,
     completions: Arc<RwLock<Box<[Completion]>>>,
@@ -1225,55 +1226,57 @@ 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];
-            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,
+        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 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),
+                    },
                 }
-            } else {
-                MatchScore::Weak {
-                    score,
-                    sort_text,
-                    sort_key,
+
+                let completion = &completions[mat.candidate_id];
+                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 {
             let completion = &completions[mat.candidate_id];
@@ -4105,6 +4108,7 @@ impl Editor {
             trigger_kind,
         };
         let completions = provider.completions(&buffer, buffer_position, completion_context, cx);
+        let sort_completions = provider.sort_completions();
 
         let id = post_inc(&mut self.next_completion_id);
         let task = cx.spawn(|this, mut cx| {
@@ -4116,6 +4120,7 @@ impl Editor {
                 let menu = if let Some(completions) = completions {
                     let mut menu = CompletionsMenu {
                         id,
+                        sort_completions,
                         initial_position: position,
                         match_candidates: completions
                             .iter()
@@ -12045,6 +12050,10 @@ pub trait CompletionProvider {
         trigger_in_words: bool,
         cx: &mut ViewContext<Editor>,
     ) -> bool;
+
+    fn sort_completions(&self) -> bool {
+        true
+    }
 }
 
 fn snippet_completions(