Fix slash command argument completion bugs (#16233)

Kirill Bulatov and Mikayla Maki created

Release Notes:

- N/A

---------

Co-authored-by: Mikayla Maki <mikayla@zed.dev>

Change summary

crates/assistant/src/slash_command.rs                         | 72 +++-
crates/assistant/src/slash_command/diagnostics_command.rs     |  1 
crates/assistant/src/slash_command/docs_command.rs            |  5 
crates/assistant/src/slash_command/file_command.rs            |  3 
crates/assistant/src/slash_command/prompt_command.rs          |  8 
crates/assistant/src/slash_command/tab_command.rs             |  2 
crates/assistant_slash_command/src/assistant_slash_command.rs |  2 
crates/extension/src/extension_slash_command.rs               |  1 
8 files changed, 67 insertions(+), 27 deletions(-)

Detailed changes

crates/assistant/src/slash_command.rs 🔗

@@ -150,6 +150,7 @@ impl SlashCommandCompletionProvider {
         arguments: &[String],
         command_range: Range<Anchor>,
         argument_range: Range<Anchor>,
+        last_argument_range: Range<Anchor>,
         cx: &mut WindowContext,
     ) -> Task<Result<Vec<project::Completion>>> {
         let new_cancel_flag = Arc::new(AtomicBool::new(false));
@@ -180,24 +181,30 @@ impl SlashCommandCompletionProvider {
                                 .map(|(editor, workspace)| {
                                     Arc::new({
                                         let mut completed_arguments = arguments.clone();
-                                        completed_arguments.pop();
+                                        if new_argument.replace_previous_arguments {
+                                            completed_arguments.clear();
+                                        } else {
+                                            completed_arguments.pop();
+                                        }
                                         completed_arguments.push(new_argument.new_text.clone());
 
                                         let command_range = command_range.clone();
                                         let command_name = command_name.clone();
-                                        move |_: CompletionIntent, cx: &mut WindowContext| {
-                                            editor
-                                                .update(cx, |editor, cx| {
-                                                    editor.run_command(
-                                                        command_range.clone(),
-                                                        &command_name,
-                                                        &completed_arguments,
-                                                        true,
-                                                        workspace.clone(),
-                                                        cx,
-                                                    );
-                                                })
-                                                .ok();
+                                        move |intent: CompletionIntent, cx: &mut WindowContext| {
+                                            if intent.is_complete() {
+                                                editor
+                                                    .update(cx, |editor, cx| {
+                                                        editor.run_command(
+                                                            command_range.clone(),
+                                                            &command_name,
+                                                            &completed_arguments,
+                                                            true,
+                                                            workspace.clone(),
+                                                            cx,
+                                                        );
+                                                    })
+                                                    .ok();
+                                            }
                                         }
                                     }) as Arc<_>
                                 })
@@ -211,7 +218,11 @@ impl SlashCommandCompletionProvider {
                         }
 
                         project::Completion {
-                            old_range: argument_range.clone(),
+                            old_range: if new_argument.replace_previous_arguments {
+                                argument_range.clone()
+                            } else {
+                                last_argument_range.clone()
+                            },
                             label: new_argument.label,
                             new_text,
                             documentation: None,
@@ -237,7 +248,7 @@ impl CompletionProvider for SlashCommandCompletionProvider {
         _: editor::CompletionContext,
         cx: &mut ViewContext<Editor>,
     ) -> Task<Result<Vec<project::Completion>>> {
-        let Some((name, arguments, command_range, argument_range)) =
+        let Some((name, arguments, command_range, last_argument_range)) =
             buffer.update(cx, |buffer, _cx| {
                 let position = buffer_position.to_point(buffer);
                 let line_start = Point::new(position.row, 0);
@@ -254,31 +265,46 @@ impl CompletionProvider for SlashCommandCompletionProvider {
                     ..buffer.anchor_after(command_range_end);
 
                 let name = line[call.name.clone()].to_string();
-                let (arguments, argument_range) = if let Some(argument) = call.arguments.last() {
-                    let start =
+                let (arguments, last_argument_range) = if let Some(argument) = call.arguments.last()
+                {
+                    let last_arg_start =
                         buffer.anchor_after(Point::new(position.row, argument.start as u32));
+                    let first_arg_start = call.arguments.first().expect("we have the last element");
+                    let first_arg_start =
+                        buffer.anchor_after(Point::new(position.row, first_arg_start.start as u32));
                     let arguments = call
                         .arguments
                         .iter()
                         .filter_map(|argument| Some(line.get(argument.clone())?.to_string()))
                         .collect::<Vec<_>>();
-                    (Some(arguments), start..buffer_position)
+                    let argument_range = first_arg_start..buffer_position;
+                    (
+                        Some((arguments, argument_range)),
+                        last_arg_start..buffer_position,
+                    )
                 } else {
                     let start =
                         buffer.anchor_after(Point::new(position.row, call.name.start as u32));
                     (None, start..buffer_position)
                 };
 
-                Some((name, arguments, command_range, argument_range))
+                Some((name, arguments, command_range, last_argument_range))
             })
         else {
             return Task::ready(Ok(Vec::new()));
         };
 
-        if let Some(arguments) = arguments {
-            self.complete_command_argument(&name, &arguments, command_range, argument_range, cx)
+        if let Some((arguments, argument_range)) = arguments {
+            self.complete_command_argument(
+                &name,
+                &arguments,
+                command_range,
+                argument_range,
+                last_argument_range,
+                cx,
+            )
         } else {
-            self.complete_command_name(&name, command_range, argument_range, cx)
+            self.complete_command_name(&name, command_range, last_argument_range, cx)
         }
     }
 

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

@@ -182,6 +182,7 @@ impl SlashCommand for DocsSlashCommand {
                         label: item.clone().into(),
                         new_text: item.to_string(),
                         run_command: true,
+                        replace_previous_arguments: false,
                     })
                     .collect()
             }
@@ -194,6 +195,7 @@ impl SlashCommand for DocsSlashCommand {
                             label: "No available docs providers.".into(),
                             new_text: String::new(),
                             run_command: false,
+                            replace_previous_arguments: false,
                         }]);
                     }
 
@@ -203,6 +205,7 @@ impl SlashCommand for DocsSlashCommand {
                             label: provider.to_string().into(),
                             new_text: provider.to_string(),
                             run_command: false,
+                            replace_previous_arguments: false,
                         })
                         .collect())
                 }
@@ -234,6 +237,7 @@ impl SlashCommand for DocsSlashCommand {
                             label: format!("{package_name} (unindexed)").into(),
                             new_text: format!("{package_name}"),
                             run_command: true,
+                            replace_previous_arguments: false,
                         })
                         .collect::<Vec<_>>();
                     items.extend(workspace_crate_completions);
@@ -247,6 +251,7 @@ impl SlashCommand for DocsSlashCommand {
                             .into(),
                             new_text: provider.to_string(),
                             run_command: false,
+                            replace_previous_arguments: false,
                         }]);
                     }
 

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

@@ -35,7 +35,7 @@ impl SlashCommand for PromptSlashCommand {
         cx: &mut WindowContext,
     ) -> Task<Result<Vec<ArgumentCompletion>>> {
         let store = PromptStore::global(cx);
-        let query = arguments.last().cloned().unwrap_or_default();
+        let query = arguments.to_owned().join(" ");
         cx.background_executor().spawn(async move {
             let prompts = store.await?.search(query).await;
             Ok(prompts
@@ -46,6 +46,7 @@ impl SlashCommand for PromptSlashCommand {
                         label: prompt_title.clone().into(),
                         new_text: prompt_title,
                         run_command: true,
+                        replace_previous_arguments: true,
                     })
                 })
                 .collect())
@@ -59,12 +60,13 @@ impl SlashCommand for PromptSlashCommand {
         _delegate: Option<Arc<dyn LspAdapterDelegate>>,
         cx: &mut WindowContext,
     ) -> Task<Result<SlashCommandOutput>> {
-        let Some(title) = arguments.first() else {
+        let title = arguments.to_owned().join(" ");
+        if title.trim().is_empty() {
             return Task::ready(Err(anyhow!("missing prompt name")));
         };
 
         let store = PromptStore::global(cx);
-        let title = SharedString::from(title.to_string());
+        let title = SharedString::from(title.clone());
         let prompt = cx.background_executor().spawn({
             let title = title.clone();
             async move {

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

@@ -76,6 +76,7 @@ impl SlashCommand for TabSlashCommand {
                 Some(ArgumentCompletion {
                     label: path_string.clone().into(),
                     new_text: path_string,
+                    replace_previous_arguments: false,
                     run_command,
                 })
             });
@@ -83,6 +84,7 @@ 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()

crates/assistant_slash_command/src/assistant_slash_command.rs 🔗

@@ -23,6 +23,8 @@ pub struct ArgumentCompletion {
     pub new_text: String,
     /// Whether the command should be run when accepting this completion.
     pub run_command: bool,
+    /// Whether to replace the all arguments, or whether to treat this as an independent argument.
+    pub replace_previous_arguments: bool,
 }
 
 pub trait SlashCommand: 'static + Send + Sync {

crates/extension/src/extension_slash_command.rs 🔗

@@ -66,6 +66,7 @@ impl SlashCommand for ExtensionSlashCommand {
                                     .map(|completion| ArgumentCompletion {
                                         label: completion.label.into(),
                                         new_text: completion.new_text,
+                                        replace_previous_arguments: false,
                                         run_command: completion.run_command,
                                     })
                                     .collect(),