assistant: Improve `/docs` argument completions (#13876)

Marshall Bowers and Antonio created

This PR improves the completions for arguments in the `/docs` slash
command.

We achieved this by extending the `complete_argument` method on the
`SlashCommand` trait to return a `Vec<ArgumentCompletion>` instead of a
`Vec<String>`.

In addition to the completion `label`, `ArgumentCompletion` has two new
fields that are can be used to customize the completion behavior:

- `new_text`: The actual text that will be inserted when the completion
is accepted, which may be different from what is shown by the completion
label.
- `run_command`: Whether the command is run when the completion is
accepted. This can be set to `false` to allow accepting a completion
without running the command.

Release Notes:

- N/A

---------

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

Change summary

crates/assistant/src/slash_command.rs                         | 21 +++-
crates/assistant/src/slash_command/active_command.rs          |  3 
crates/assistant/src/slash_command/default_command.rs         |  4 
crates/assistant/src/slash_command/diagnostics_command.rs     | 13 ++
crates/assistant/src/slash_command/docs_command.rs            | 23 +++-
crates/assistant/src/slash_command/fetch_command.rs           |  6 
crates/assistant/src/slash_command/file_command.rs            | 14 ++
crates/assistant/src/slash_command/now_command.rs             |  6 
crates/assistant/src/slash_command/project_command.rs         |  4 
crates/assistant/src/slash_command/prompt_command.rs          | 13 ++
crates/assistant/src/slash_command/search_command.rs          |  4 
crates/assistant/src/slash_command/tabs_command.rs            |  3 
crates/assistant/src/slash_command/term_command.rs            | 12 +
crates/assistant_slash_command/src/assistant_slash_command.rs | 12 ++
crates/extension/src/extension_slash_command.rs               | 17 +++
15 files changed, 115 insertions(+), 40 deletions(-)

Detailed changes

crates/assistant/src/slash_command.rs 🔗

@@ -170,7 +170,7 @@ impl SlashCommandCompletionProvider {
                     .await?
                     .into_iter()
                     .map(|command_argument| {
-                        let confirm =
+                        let confirm = if command_argument.run_command {
                             editor
                                 .clone()
                                 .zip(workspace.clone())
@@ -178,7 +178,7 @@ impl SlashCommandCompletionProvider {
                                     Arc::new({
                                         let command_range = command_range.clone();
                                         let command_name = command_name.clone();
-                                        let command_argument = command_argument.clone();
+                                        let command_argument = command_argument.new_text.clone();
                                         move |cx: &mut WindowContext| {
                                             editor
                                                 .update(cx, |editor, cx| {
@@ -194,15 +194,24 @@ impl SlashCommandCompletionProvider {
                                                 .ok();
                                         }
                                     }) as Arc<_>
-                                });
+                                })
+                        } else {
+                            None
+                        };
+
+                        let mut new_text = command_argument.new_text.clone();
+                        if !command_argument.run_command {
+                            new_text.push(' ');
+                        }
+
                         project::Completion {
                             old_range: argument_range.clone(),
-                            label: CodeLabel::plain(command_argument.clone(), None),
-                            new_text: command_argument.clone(),
+                            label: CodeLabel::plain(command_argument.label, None),
+                            new_text,
                             documentation: None,
                             server_id: LanguageServerId(0),
                             lsp_completion: Default::default(),
-                            show_new_completions_on_confirm: false,
+                            show_new_completions_on_confirm: !command_argument.run_command,
                             confirm,
                         }
                     })

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

@@ -4,6 +4,7 @@ use super::{
     SlashCommand, SlashCommandOutput,
 };
 use anyhow::{anyhow, Result};
+use assistant_slash_command::ArgumentCompletion;
 use editor::Editor;
 use gpui::{AppContext, Task, WeakView};
 use language::LspAdapterDelegate;
@@ -33,7 +34,7 @@ impl SlashCommand for ActiveSlashCommand {
         _cancel: Arc<AtomicBool>,
         _workspace: Option<WeakView<Workspace>>,
         _cx: &mut AppContext,
-    ) -> Task<Result<Vec<String>>> {
+    ) -> Task<Result<Vec<ArgumentCompletion>>> {
         Task::ready(Err(anyhow!("this command does not require argument")))
     }
 

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

@@ -1,7 +1,7 @@
 use super::{SlashCommand, SlashCommandOutput};
 use crate::prompt_library::PromptStore;
 use anyhow::{anyhow, Result};
-use assistant_slash_command::SlashCommandOutputSection;
+use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
 use gpui::{AppContext, Task, WeakView};
 use language::LspAdapterDelegate;
 use std::{
@@ -36,7 +36,7 @@ impl SlashCommand for DefaultSlashCommand {
         _cancellation_flag: Arc<AtomicBool>,
         _workspace: Option<WeakView<Workspace>>,
         _cx: &mut AppContext,
-    ) -> Task<Result<Vec<String>>> {
+    ) -> Task<Result<Vec<ArgumentCompletion>>> {
         Task::ready(Err(anyhow!("this command does not require argument")))
     }
 

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

@@ -1,6 +1,6 @@
 use super::{create_label_for_command, SlashCommand, SlashCommandOutput};
 use anyhow::{anyhow, Result};
-use assistant_slash_command::SlashCommandOutputSection;
+use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
 use fuzzy::{PathMatch, StringMatchCandidate};
 use gpui::{AppContext, Model, Task, View, WeakView};
 use language::{
@@ -108,7 +108,7 @@ impl SlashCommand for DiagnosticsSlashCommand {
         cancellation_flag: Arc<AtomicBool>,
         workspace: Option<WeakView<Workspace>>,
         cx: &mut AppContext,
-    ) -> Task<Result<Vec<String>>> {
+    ) -> Task<Result<Vec<ArgumentCompletion>>> {
         let Some(workspace) = workspace.and_then(|workspace| workspace.upgrade()) else {
             return Task::ready(Err(anyhow!("workspace was dropped")));
         };
@@ -143,7 +143,14 @@ impl SlashCommand for DiagnosticsSlashCommand {
                 .map(|candidate| candidate.string),
             );
 
-            Ok(matches)
+            Ok(matches
+                .into_iter()
+                .map(|completion| ArgumentCompletion {
+                    label: completion.clone(),
+                    new_text: completion,
+                    run_command: true,
+                })
+                .collect())
         })
     }
 

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

@@ -3,7 +3,9 @@ use std::sync::atomic::AtomicBool;
 use std::sync::Arc;
 
 use anyhow::{anyhow, bail, Result};
-use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection};
+use assistant_slash_command::{
+    ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
+};
 use gpui::{AppContext, Model, Task, WeakView};
 use indexed_docs::{
     IndexedDocsRegistry, IndexedDocsStore, LocalProvider, PackageName, ProviderId, RustdocIndexer,
@@ -92,7 +94,7 @@ impl SlashCommand for DocsSlashCommand {
         _cancel: Arc<AtomicBool>,
         workspace: Option<WeakView<Workspace>>,
         cx: &mut AppContext,
-    ) -> Task<Result<Vec<String>>> {
+    ) -> Task<Result<Vec<ArgumentCompletion>>> {
         self.ensure_rustdoc_provider_is_registered(workspace, cx);
 
         let indexed_docs_registry = IndexedDocsRegistry::global(cx);
@@ -107,10 +109,17 @@ impl SlashCommand for DocsSlashCommand {
             ///
             /// We will likely want to extend `complete_argument` with support for replacing just
             /// a particular range of the argument when a completion is accepted.
-            fn prefix_with_provider(provider: ProviderId, items: Vec<String>) -> Vec<String> {
+            fn prefix_with_provider(
+                provider: ProviderId,
+                items: Vec<String>,
+            ) -> Vec<ArgumentCompletion> {
                 items
                     .into_iter()
-                    .map(|item| format!("{provider} {item}"))
+                    .map(|item| ArgumentCompletion {
+                        label: item.clone(),
+                        new_text: format!("{provider} {item}"),
+                        run_command: true,
+                    })
                     .collect()
             }
 
@@ -119,7 +128,11 @@ impl SlashCommand for DocsSlashCommand {
                     let providers = indexed_docs_registry.list_providers();
                     Ok(providers
                         .into_iter()
-                        .map(|provider| provider.to_string())
+                        .map(|provider| ArgumentCompletion {
+                            label: provider.to_string(),
+                            new_text: provider.to_string(),
+                            run_command: false,
+                        })
                         .collect())
                 }
                 DocsSlashCommandArgs::SearchPackageDocs {

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

@@ -4,7 +4,9 @@ use std::sync::atomic::AtomicBool;
 use std::sync::Arc;
 
 use anyhow::{anyhow, bail, Context, Result};
-use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection};
+use assistant_slash_command::{
+    ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
+};
 use futures::AsyncReadExt;
 use gpui::{AppContext, Task, WeakView};
 use html_to_markdown::{convert_html_to_markdown, markdown, TagHandler};
@@ -119,7 +121,7 @@ impl SlashCommand for FetchSlashCommand {
         _cancel: Arc<AtomicBool>,
         _workspace: Option<WeakView<Workspace>>,
         _cx: &mut AppContext,
-    ) -> Task<Result<Vec<String>>> {
+    ) -> Task<Result<Vec<ArgumentCompletion>>> {
         Task::ready(Ok(Vec::new()))
     }
 

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

@@ -1,6 +1,6 @@
 use super::{diagnostics_command::write_single_file_diagnostics, SlashCommand, SlashCommandOutput};
 use anyhow::{anyhow, Result};
-use assistant_slash_command::SlashCommandOutputSection;
+use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
 use fuzzy::PathMatch;
 use gpui::{AppContext, Model, Task, View, WeakView};
 use language::{BufferSnapshot, LineEnding, LspAdapterDelegate};
@@ -105,7 +105,7 @@ impl SlashCommand for FileSlashCommand {
         cancellation_flag: Arc<AtomicBool>,
         workspace: Option<WeakView<Workspace>>,
         cx: &mut AppContext,
-    ) -> Task<Result<Vec<String>>> {
+    ) -> Task<Result<Vec<ArgumentCompletion>>> {
         let Some(workspace) = workspace.and_then(|workspace| workspace.upgrade()) else {
             return Task::ready(Err(anyhow!("workspace was dropped")));
         };
@@ -116,11 +116,17 @@ impl SlashCommand for FileSlashCommand {
                 .await
                 .into_iter()
                 .map(|path_match| {
-                    format!(
+                    let text = format!(
                         "{}{}",
                         path_match.path_prefix,
                         path_match.path.to_string_lossy()
-                    )
+                    );
+
+                    ArgumentCompletion {
+                        label: text.clone(),
+                        new_text: text,
+                        run_command: true,
+                    }
                 })
                 .collect())
         })

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

@@ -2,7 +2,9 @@ use std::sync::atomic::AtomicBool;
 use std::sync::Arc;
 
 use anyhow::Result;
-use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection};
+use assistant_slash_command::{
+    ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
+};
 use chrono::Local;
 use gpui::{AppContext, Task, WeakView};
 use language::LspAdapterDelegate;
@@ -34,7 +36,7 @@ impl SlashCommand for NowSlashCommand {
         _cancel: Arc<AtomicBool>,
         _workspace: Option<WeakView<Workspace>>,
         _cx: &mut AppContext,
-    ) -> Task<Result<Vec<String>>> {
+    ) -> Task<Result<Vec<ArgumentCompletion>>> {
         Task::ready(Ok(Vec::new()))
     }
 

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

@@ -1,6 +1,6 @@
 use super::{SlashCommand, SlashCommandOutput};
 use anyhow::{anyhow, Context, Result};
-use assistant_slash_command::SlashCommandOutputSection;
+use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
 use fs::Fs;
 use gpui::{AppContext, Model, Task, WeakView};
 use language::LspAdapterDelegate;
@@ -107,7 +107,7 @@ impl SlashCommand for ProjectSlashCommand {
         _cancel: Arc<AtomicBool>,
         _workspace: Option<WeakView<Workspace>>,
         _cx: &mut AppContext,
-    ) -> Task<Result<Vec<String>>> {
+    ) -> Task<Result<Vec<ArgumentCompletion>>> {
         Task::ready(Err(anyhow!("this command does not require argument")))
     }
 

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

@@ -1,7 +1,7 @@
 use super::{SlashCommand, SlashCommandOutput};
 use crate::prompt_library::PromptStore;
 use anyhow::{anyhow, Context, Result};
-use assistant_slash_command::SlashCommandOutputSection;
+use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
 use gpui::{AppContext, Task, WeakView};
 use language::LspAdapterDelegate;
 use std::sync::{atomic::AtomicBool, Arc};
@@ -33,13 +33,20 @@ impl SlashCommand for PromptSlashCommand {
         _cancellation_flag: Arc<AtomicBool>,
         _workspace: Option<WeakView<Workspace>>,
         cx: &mut AppContext,
-    ) -> Task<Result<Vec<String>>> {
+    ) -> Task<Result<Vec<ArgumentCompletion>>> {
         let store = PromptStore::global(cx);
         cx.background_executor().spawn(async move {
             let prompts = store.await?.search(query).await;
             Ok(prompts
                 .into_iter()
-                .filter_map(|prompt| Some(prompt.title?.to_string()))
+                .filter_map(|prompt| {
+                    let prompt_title = prompt.title?.to_string();
+                    Some(ArgumentCompletion {
+                        label: prompt_title.clone(),
+                        new_text: prompt_title,
+                        run_command: true,
+                    })
+                })
                 .collect())
         })
     }

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

@@ -4,7 +4,7 @@ use super::{
     SlashCommand, SlashCommandOutput,
 };
 use anyhow::Result;
-use assistant_slash_command::SlashCommandOutputSection;
+use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
 use gpui::{AppContext, Task, WeakView};
 use language::{CodeLabel, LineEnding, LspAdapterDelegate};
 use semantic_index::SemanticIndex;
@@ -46,7 +46,7 @@ impl SlashCommand for SearchSlashCommand {
         _cancel: Arc<AtomicBool>,
         _workspace: Option<WeakView<Workspace>>,
         _cx: &mut AppContext,
-    ) -> Task<Result<Vec<String>>> {
+    ) -> Task<Result<Vec<ArgumentCompletion>>> {
         Task::ready(Ok(Vec::new()))
     }
 

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

@@ -4,6 +4,7 @@ use super::{
     SlashCommand, SlashCommandOutput,
 };
 use anyhow::{anyhow, Result};
+use assistant_slash_command::ArgumentCompletion;
 use collections::HashMap;
 use editor::Editor;
 use gpui::{AppContext, Entity, Task, WeakView};
@@ -37,7 +38,7 @@ impl SlashCommand for TabsSlashCommand {
         _cancel: Arc<std::sync::atomic::AtomicBool>,
         _workspace: Option<WeakView<Workspace>>,
         _cx: &mut AppContext,
-    ) -> Task<Result<Vec<String>>> {
+    ) -> Task<Result<Vec<ArgumentCompletion>>> {
         Task::ready(Err(anyhow!("this command does not require argument")))
     }
 

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

@@ -2,7 +2,9 @@ use std::sync::atomic::AtomicBool;
 use std::sync::Arc;
 
 use anyhow::Result;
-use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection};
+use assistant_slash_command::{
+    ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
+};
 use gpui::{AppContext, Task, WeakView};
 use language::{CodeLabel, LspAdapterDelegate};
 use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
@@ -42,8 +44,12 @@ impl SlashCommand for TermSlashCommand {
         _cancel: Arc<AtomicBool>,
         _workspace: Option<WeakView<Workspace>>,
         _cx: &mut AppContext,
-    ) -> Task<Result<Vec<String>>> {
-        Task::ready(Ok(vec![LINE_COUNT_ARG.to_string()]))
+    ) -> Task<Result<Vec<ArgumentCompletion>>> {
+        Task::ready(Ok(vec![ArgumentCompletion {
+            label: LINE_COUNT_ARG.to_string(),
+            new_text: LINE_COUNT_ARG.to_string(),
+            run_command: true,
+        }]))
     }
 
     fn run(

crates/assistant_slash_command/src/assistant_slash_command.rs 🔗

@@ -15,6 +15,16 @@ pub fn init(cx: &mut AppContext) {
     SlashCommandRegistry::default_global(cx);
 }
 
+#[derive(Debug)]
+pub struct ArgumentCompletion {
+    /// The label to display for this completion.
+    pub label: String,
+    /// The new text that should be inserted into the command when this completion is accepted.
+    pub new_text: String,
+    /// Whether the command should be run when accepting this completion.
+    pub run_command: bool,
+}
+
 pub trait SlashCommand: 'static + Send + Sync {
     fn name(&self) -> String;
     fn label(&self, _cx: &AppContext) -> CodeLabel {
@@ -28,7 +38,7 @@ pub trait SlashCommand: 'static + Send + Sync {
         cancel: Arc<AtomicBool>,
         workspace: Option<WeakView<Workspace>>,
         cx: &mut AppContext,
-    ) -> Task<Result<Vec<String>>>;
+    ) -> Task<Result<Vec<ArgumentCompletion>>>;
     fn requires_argument(&self) -> bool;
     fn run(
         self: Arc<Self>,

crates/extension/src/extension_slash_command.rs 🔗

@@ -1,7 +1,9 @@
 use std::sync::{atomic::AtomicBool, Arc};
 
 use anyhow::{anyhow, Result};
-use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection};
+use assistant_slash_command::{
+    ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
+};
 use futures::FutureExt;
 use gpui::{AppContext, Task, WeakView, WindowContext};
 use language::LspAdapterDelegate;
@@ -41,7 +43,7 @@ impl SlashCommand for ExtensionSlashCommand {
         _cancel: Arc<AtomicBool>,
         _workspace: Option<WeakView<Workspace>>,
         cx: &mut AppContext,
-    ) -> Task<Result<Vec<String>>> {
+    ) -> Task<Result<Vec<ArgumentCompletion>>> {
         cx.background_executor().spawn(async move {
             self.extension
                 .call({
@@ -57,7 +59,16 @@ impl SlashCommand for ExtensionSlashCommand {
                                 .await?
                                 .map_err(|e| anyhow!("{}", e))?;
 
-                            anyhow::Ok(completions)
+                            anyhow::Ok(
+                                completions
+                                    .into_iter()
+                                    .map(|completion| ArgumentCompletion {
+                                        label: completion.clone(),
+                                        new_text: completion,
+                                        run_command: true,
+                                    })
+                                    .collect(),
+                            )
                         }
                         .boxed()
                     }