From 68accaeb00be484448b0e6e55a65a0a860ce9a00 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 5 Jul 2024 13:29:17 -0400 Subject: [PATCH] assistant: Improve `/docs` argument completions (#13876) 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` instead of a `Vec`. 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 --- crates/assistant/src/slash_command.rs | 21 ++++++++++++----- .../src/slash_command/active_command.rs | 3 ++- .../src/slash_command/default_command.rs | 4 ++-- .../src/slash_command/diagnostics_command.rs | 13 ++++++++--- .../src/slash_command/docs_command.rs | 23 +++++++++++++++---- .../src/slash_command/fetch_command.rs | 6 +++-- .../src/slash_command/file_command.rs | 14 +++++++---- .../src/slash_command/now_command.rs | 6 +++-- .../src/slash_command/project_command.rs | 4 ++-- .../src/slash_command/prompt_command.rs | 13 ++++++++--- .../src/slash_command/search_command.rs | 4 ++-- .../src/slash_command/tabs_command.rs | 3 ++- .../src/slash_command/term_command.rs | 12 +++++++--- .../src/assistant_slash_command.rs | 12 +++++++++- .../extension/src/extension_slash_command.rs | 17 +++++++++++--- 15 files changed, 115 insertions(+), 40 deletions(-) diff --git a/crates/assistant/src/slash_command.rs b/crates/assistant/src/slash_command.rs index 37678e0fc6..ebb563313a 100644 --- a/crates/assistant/src/slash_command.rs +++ b/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, } }) diff --git a/crates/assistant/src/slash_command/active_command.rs b/crates/assistant/src/slash_command/active_command.rs index f3ec3a45e9..0f46937560 100644 --- a/crates/assistant/src/slash_command/active_command.rs +++ b/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, _workspace: Option>, _cx: &mut AppContext, - ) -> Task>> { + ) -> Task>> { Task::ready(Err(anyhow!("this command does not require argument"))) } diff --git a/crates/assistant/src/slash_command/default_command.rs b/crates/assistant/src/slash_command/default_command.rs index 44ffdfcd11..ccc9d1fbdb 100644 --- a/crates/assistant/src/slash_command/default_command.rs +++ b/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, _workspace: Option>, _cx: &mut AppContext, - ) -> Task>> { + ) -> Task>> { Task::ready(Err(anyhow!("this command does not require argument"))) } diff --git a/crates/assistant/src/slash_command/diagnostics_command.rs b/crates/assistant/src/slash_command/diagnostics_command.rs index ab01b3c803..20e712803b 100644 --- a/crates/assistant/src/slash_command/diagnostics_command.rs +++ b/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, workspace: Option>, cx: &mut AppContext, - ) -> Task>> { + ) -> Task>> { 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()) }) } diff --git a/crates/assistant/src/slash_command/docs_command.rs b/crates/assistant/src/slash_command/docs_command.rs index c66e68bef1..9b27635fd2 100644 --- a/crates/assistant/src/slash_command/docs_command.rs +++ b/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, workspace: Option>, cx: &mut AppContext, - ) -> Task>> { + ) -> Task>> { 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) -> Vec { + fn prefix_with_provider( + provider: ProviderId, + items: Vec, + ) -> Vec { 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 { diff --git a/crates/assistant/src/slash_command/fetch_command.rs b/crates/assistant/src/slash_command/fetch_command.rs index 7a8230186f..547cacf559 100644 --- a/crates/assistant/src/slash_command/fetch_command.rs +++ b/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, _workspace: Option>, _cx: &mut AppContext, - ) -> Task>> { + ) -> Task>> { Task::ready(Ok(Vec::new())) } diff --git a/crates/assistant/src/slash_command/file_command.rs b/crates/assistant/src/slash_command/file_command.rs index 8acf2d1369..d5d5662914 100644 --- a/crates/assistant/src/slash_command/file_command.rs +++ b/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, workspace: Option>, cx: &mut AppContext, - ) -> Task>> { + ) -> Task>> { 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()) }) diff --git a/crates/assistant/src/slash_command/now_command.rs b/crates/assistant/src/slash_command/now_command.rs index 108a43027e..c7bacf8746 100644 --- a/crates/assistant/src/slash_command/now_command.rs +++ b/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, _workspace: Option>, _cx: &mut AppContext, - ) -> Task>> { + ) -> Task>> { Task::ready(Ok(Vec::new())) } diff --git a/crates/assistant/src/slash_command/project_command.rs b/crates/assistant/src/slash_command/project_command.rs index b51c051e11..476e60c5d4 100644 --- a/crates/assistant/src/slash_command/project_command.rs +++ b/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, _workspace: Option>, _cx: &mut AppContext, - ) -> Task>> { + ) -> Task>> { Task::ready(Err(anyhow!("this command does not require argument"))) } diff --git a/crates/assistant/src/slash_command/prompt_command.rs b/crates/assistant/src/slash_command/prompt_command.rs index ac4d77789e..1edf2d51df 100644 --- a/crates/assistant/src/slash_command/prompt_command.rs +++ b/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, _workspace: Option>, cx: &mut AppContext, - ) -> Task>> { + ) -> Task>> { 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()) }) } diff --git a/crates/assistant/src/slash_command/search_command.rs b/crates/assistant/src/slash_command/search_command.rs index 6fa5a16bc9..cdf1da7a9b 100644 --- a/crates/assistant/src/slash_command/search_command.rs +++ b/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, _workspace: Option>, _cx: &mut AppContext, - ) -> Task>> { + ) -> Task>> { Task::ready(Ok(Vec::new())) } diff --git a/crates/assistant/src/slash_command/tabs_command.rs b/crates/assistant/src/slash_command/tabs_command.rs index 31a66b1c77..78be293bd7 100644 --- a/crates/assistant/src/slash_command/tabs_command.rs +++ b/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, _workspace: Option>, _cx: &mut AppContext, - ) -> Task>> { + ) -> Task>> { Task::ready(Err(anyhow!("this command does not require argument"))) } diff --git a/crates/assistant/src/slash_command/term_command.rs b/crates/assistant/src/slash_command/term_command.rs index 60619afcf5..09b7323a02 100644 --- a/crates/assistant/src/slash_command/term_command.rs +++ b/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, _workspace: Option>, _cx: &mut AppContext, - ) -> Task>> { - Task::ready(Ok(vec![LINE_COUNT_ARG.to_string()])) + ) -> Task>> { + Task::ready(Ok(vec![ArgumentCompletion { + label: LINE_COUNT_ARG.to_string(), + new_text: LINE_COUNT_ARG.to_string(), + run_command: true, + }])) } fn run( diff --git a/crates/assistant_slash_command/src/assistant_slash_command.rs b/crates/assistant_slash_command/src/assistant_slash_command.rs index c468d134fe..d361f49d42 100644 --- a/crates/assistant_slash_command/src/assistant_slash_command.rs +++ b/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, workspace: Option>, cx: &mut AppContext, - ) -> Task>>; + ) -> Task>>; fn requires_argument(&self) -> bool; fn run( self: Arc, diff --git a/crates/extension/src/extension_slash_command.rs b/crates/extension/src/extension_slash_command.rs index e10007bfc2..3c8a965984 100644 --- a/crates/extension/src/extension_slash_command.rs +++ b/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, _workspace: Option>, cx: &mut AppContext, - ) -> Task>> { + ) -> Task>> { 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() }