Further improve /tabs command and slash arguments completion (#16216)

Kirill Bulatov and Marshall Bowers created

* renames `/tabs` to `/tab`
* allows to insert multiple tabs when fuzzy matching by the names
* improve slash command completion API, introduce a notion of multiple
arguments
* properly fire off commands on arguments' completions with
`run_command: true`

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <marshall@zed.dev>

Change summary

crates/assistant/src/assistant.rs                             |   4 
crates/assistant/src/assistant_panel.rs                       |  25 
crates/assistant/src/context.rs                               |  28 
crates/assistant/src/slash_command.rs                         | 105 +-
crates/assistant/src/slash_command/default_command.rs         |   4 
crates/assistant/src/slash_command/diagnostics_command.rs     |  41 
crates/assistant/src/slash_command/docs_command.rs            |  71 +
crates/assistant/src/slash_command/fetch_command.rs           |   6 
crates/assistant/src/slash_command/file_command.rs            |  13 
crates/assistant/src/slash_command/now_command.rs             |   4 
crates/assistant/src/slash_command/project_command.rs         |   4 
crates/assistant/src/slash_command/prompt_command.rs          |   7 
crates/assistant/src/slash_command/search_command.rs          |   8 
crates/assistant/src/slash_command/symbols_command.rs         |   4 
crates/assistant/src/slash_command/tab_command.rs             | 180 +++-
crates/assistant/src/slash_command/terminal_command.rs        |  39 
crates/assistant/src/slash_command/workflow_command.rs        |   4 
crates/assistant_slash_command/src/assistant_slash_command.rs |   4 
crates/extension/src/extension_slash_command.rs               |  16 
crates/extension/src/wasm_host/wit.rs                         |   8 
crates/extension_api/src/extension_api.rs                     |  12 
crates/extension_api/wit/since_v0.1.0/extension.wit           |   4 
extensions/gleam/src/gleam.rs                                 |   6 
23 files changed, 333 insertions(+), 264 deletions(-)

Detailed changes

crates/assistant/src/assistant.rs 🔗

@@ -37,7 +37,7 @@ use serde::{Deserialize, Serialize};
 use settings::{update_settings_file, Settings, SettingsStore};
 use slash_command::{
     default_command, diagnostics_command, docs_command, fetch_command, file_command, now_command,
-    project_command, prompt_command, search_command, symbols_command, tabs_command,
+    project_command, prompt_command, search_command, symbols_command, tab_command,
     terminal_command, workflow_command,
 };
 use std::sync::Arc;
@@ -294,7 +294,7 @@ fn register_slash_commands(prompt_builder: Option<Arc<PromptBuilder>>, cx: &mut
     let slash_command_registry = SlashCommandRegistry::global(cx);
     slash_command_registry.register_command(file_command::FileSlashCommand, true);
     slash_command_registry.register_command(symbols_command::OutlineSlashCommand, true);
-    slash_command_registry.register_command(tabs_command::TabsSlashCommand, true);
+    slash_command_registry.register_command(tab_command::TabSlashCommand, true);
     slash_command_registry.register_command(project_command::ProjectSlashCommand, true);
     slash_command_registry.register_command(prompt_command::PromptSlashCommand, true);
     slash_command_registry.register_command(default_command::DefaultSlashCommand, false);

crates/assistant/src/assistant_panel.rs 🔗

@@ -1814,7 +1814,7 @@ impl ContextEditor {
         self.run_command(
             command.source_range,
             &command.name,
-            command.argument.as_deref(),
+            &command.arguments,
             false,
             self.workspace.clone(),
             cx,
@@ -2120,7 +2120,7 @@ impl ContextEditor {
                 self.run_command(
                     command.source_range,
                     &command.name,
-                    command.argument.as_deref(),
+                    &command.arguments,
                     true,
                     workspace.clone(),
                     cx,
@@ -2134,19 +2134,13 @@ impl ContextEditor {
         &mut self,
         command_range: Range<language::Anchor>,
         name: &str,
-        argument: Option<&str>,
+        arguments: &[String],
         insert_trailing_newline: bool,
         workspace: WeakView<Workspace>,
         cx: &mut ViewContext<Self>,
     ) {
         if let Some(command) = SlashCommandRegistry::global(cx).command(name) {
-            let argument = argument.map(ToString::to_string);
-            let output = command.run(
-                argument.as_deref(),
-                workspace,
-                self.lsp_adapter_delegate.clone(),
-                cx,
-            );
+            let output = command.run(arguments, workspace, self.lsp_adapter_delegate.clone(), cx);
             self.context.update(cx, |context, cx| {
                 context.insert_command_output(command_range, output, insert_trailing_newline, cx)
             });
@@ -2232,7 +2226,7 @@ impl ContextEditor {
                                             context_editor.run_command(
                                                 command.source_range.clone(),
                                                 &command.name,
-                                                command.argument.as_deref(),
+                                                &command.arguments,
                                                 false,
                                                 workspace.clone(),
                                                 cx,
@@ -2345,7 +2339,7 @@ impl ContextEditor {
                         self.run_command(
                             command.source_range,
                             &command.name,
-                            command.argument.as_deref(),
+                            &command.arguments,
                             false,
                             self.workspace.clone(),
                             cx,
@@ -4559,11 +4553,10 @@ fn render_docs_slash_command_trailer(
     command: PendingSlashCommand,
     cx: &mut WindowContext,
 ) -> AnyElement {
-    let Some(argument) = command.argument else {
+    if command.arguments.is_empty() {
         return Empty.into_any();
-    };
-
-    let args = DocsSlashCommandArgs::parse(&argument);
+    }
+    let args = DocsSlashCommandArgs::parse(&command.arguments);
 
     let Some(store) = args
         .provider()

crates/assistant/src/context.rs 🔗

@@ -1206,21 +1206,31 @@ impl Context {
             while let Some(line) = lines.next() {
                 if let Some(command_line) = SlashCommandLine::parse(line) {
                     let name = &line[command_line.name.clone()];
-                    let argument = command_line.argument.as_ref().and_then(|argument| {
-                        (!argument.is_empty()).then_some(&line[argument.clone()])
-                    });
+                    let arguments = command_line
+                        .arguments
+                        .iter()
+                        .filter_map(|argument_range| {
+                            if argument_range.is_empty() {
+                                None
+                            } else {
+                                line.get(argument_range.clone())
+                            }
+                        })
+                        .map(ToOwned::to_owned)
+                        .collect::<SmallVec<_>>();
                     if let Some(command) = SlashCommandRegistry::global(cx).command(name) {
-                        if !command.requires_argument() || argument.is_some() {
+                        if !command.requires_argument() || !arguments.is_empty() {
                             let start_ix = offset + command_line.name.start - 1;
                             let end_ix = offset
                                 + command_line
-                                    .argument
+                                    .arguments
+                                    .last()
                                     .map_or(command_line.name.end, |argument| argument.end);
                             let source_range =
                                 buffer.anchor_after(start_ix)..buffer.anchor_after(end_ix);
                             let pending_command = PendingSlashCommand {
                                 name: name.to_string(),
-                                argument: argument.map(ToString::to_string),
+                                arguments,
                                 source_range,
                                 status: PendingSlashCommandStatus::Idle,
                             };
@@ -2457,7 +2467,7 @@ impl ContextVersion {
 #[derive(Debug, Clone)]
 pub struct PendingSlashCommand {
     pub name: String,
-    pub argument: Option<String>,
+    pub arguments: SmallVec<[String; 3]>,
     pub status: PendingSlashCommandStatus,
     pub source_range: Range<language::Anchor>,
 }
@@ -3758,7 +3768,7 @@ mod tests {
 
         fn complete_argument(
             self: Arc<Self>,
-            _query: String,
+            _arguments: &[String],
             _cancel: Arc<AtomicBool>,
             _workspace: Option<WeakView<Workspace>>,
             _cx: &mut WindowContext,
@@ -3772,7 +3782,7 @@ mod tests {
 
         fn run(
             self: Arc<Self>,
-            _argument: Option<&str>,
+            _arguments: &[String],
             _workspace: WeakView<Workspace>,
             _delegate: Option<Arc<dyn LspAdapterDelegate>>,
             _cx: &mut WindowContext,

crates/assistant/src/slash_command.rs 🔗

@@ -28,7 +28,7 @@ pub mod project_command;
 pub mod prompt_command;
 pub mod search_command;
 pub mod symbols_command;
-pub mod tabs_command;
+pub mod tab_command;
 pub mod terminal_command;
 pub mod workflow_command;
 
@@ -41,8 +41,8 @@ pub(crate) struct SlashCommandCompletionProvider {
 pub(crate) struct SlashCommandLine {
     /// The range within the line containing the command name.
     pub name: Range<usize>,
-    /// The range within the line containing the command argument.
-    pub argument: Option<Range<usize>>,
+    /// Ranges within the line containing the command arguments.
+    pub arguments: Vec<Range<usize>>,
 }
 
 impl SlashCommandCompletionProvider {
@@ -115,7 +115,7 @@ impl SlashCommandCompletionProvider {
                                                         editor.run_command(
                                                             command_range.clone(),
                                                             &command_name,
-                                                            None,
+                                                            &[],
                                                             true,
                                                             workspace.clone(),
                                                             cx,
@@ -147,7 +147,7 @@ impl SlashCommandCompletionProvider {
     fn complete_command_argument(
         &self,
         command_name: &str,
-        argument: String,
+        arguments: &[String],
         command_range: Range<Anchor>,
         argument_range: Range<Anchor>,
         cx: &mut WindowContext,
@@ -159,7 +159,7 @@ impl SlashCommandCompletionProvider {
         let commands = SlashCommandRegistry::global(cx);
         if let Some(command) = commands.command(command_name) {
             let completions = command.complete_argument(
-                argument,
+                arguments,
                 new_cancel_flag.clone(),
                 self.workspace.clone(),
                 cx,
@@ -167,35 +167,37 @@ impl SlashCommandCompletionProvider {
             let command_name: Arc<str> = command_name.into();
             let editor = self.editor.clone();
             let workspace = self.workspace.clone();
+            let arguments = arguments.to_vec();
             cx.background_executor().spawn(async move {
                 Ok(completions
                     .await?
                     .into_iter()
-                    .map(|command_argument| {
-                        let confirm = if command_argument.run_command {
+                    .map(|new_argument| {
+                        let confirm = if new_argument.run_command {
                             editor
                                 .clone()
                                 .zip(workspace.clone())
                                 .map(|(editor, workspace)| {
                                     Arc::new({
+                                        let mut completed_arguments = arguments.clone();
+                                        completed_arguments.pop();
+                                        completed_arguments.push(new_argument.new_text.clone());
+
                                         let command_range = command_range.clone();
                                         let command_name = command_name.clone();
-                                        let command_argument = command_argument.new_text.clone();
-                                        move |intent: CompletionIntent, cx: &mut WindowContext| {
-                                            if intent.is_complete() {
-                                                editor
-                                                    .update(cx, |editor, cx| {
-                                                        editor.run_command(
-                                                            command_range.clone(),
-                                                            &command_name,
-                                                            Some(&command_argument),
-                                                            true,
-                                                            workspace.clone(),
-                                                            cx,
-                                                        );
-                                                    })
-                                                    .ok();
-                                            }
+                                        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();
                                         }
                                     }) as Arc<_>
                                 })
@@ -203,27 +205,26 @@ impl SlashCommandCompletionProvider {
                             None
                         };
 
-                        let mut new_text = command_argument.new_text.clone();
-                        if !command_argument.run_command {
+                        let mut new_text = new_argument.new_text.clone();
+                        if !new_argument.run_command {
                             new_text.push(' ');
                         }
 
                         project::Completion {
                             old_range: argument_range.clone(),
-                            label: command_argument.label,
+                            label: new_argument.label,
                             new_text,
                             documentation: None,
                             server_id: LanguageServerId(0),
                             lsp_completion: Default::default(),
-                            show_new_completions_on_confirm: !command_argument.run_command,
+                            show_new_completions_on_confirm: !new_argument.run_command,
                             confirm,
                         }
                     })
                     .collect())
             })
         } else {
-            cx.background_executor()
-                .spawn(async move { Ok(Vec::new()) })
+            Task::ready(Ok(Vec::new()))
         }
     }
 }
@@ -236,7 +237,7 @@ impl CompletionProvider for SlashCommandCompletionProvider {
         _: editor::CompletionContext,
         cx: &mut ViewContext<Editor>,
     ) -> Task<Result<Vec<project::Completion>>> {
-        let Some((name, argument, command_range, argument_range)) =
+        let Some((name, arguments, command_range, argument_range)) =
             buffer.update(cx, |buffer, _cx| {
                 let position = buffer_position.to_point(buffer);
                 let line_start = Point::new(position.row, 0);
@@ -247,30 +248,35 @@ impl CompletionProvider for SlashCommandCompletionProvider {
                 let command_range_start = Point::new(position.row, call.name.start as u32 - 1);
                 let command_range_end = Point::new(
                     position.row,
-                    call.argument.as_ref().map_or(call.name.end, |arg| arg.end) as u32,
+                    call.arguments.last().map_or(call.name.end, |arg| arg.end) as u32,
                 );
                 let command_range = buffer.anchor_after(command_range_start)
                     ..buffer.anchor_after(command_range_end);
 
                 let name = line[call.name.clone()].to_string();
-
-                Some(if let Some(argument) = call.argument {
+                let (arguments, argument_range) = if let Some(argument) = call.arguments.last() {
                     let start =
                         buffer.anchor_after(Point::new(position.row, argument.start as u32));
-                    let argument = line[argument.clone()].to_string();
-                    (name, Some(argument), command_range, start..buffer_position)
+                    let arguments = call
+                        .arguments
+                        .iter()
+                        .filter_map(|argument| Some(line.get(argument.clone())?.to_string()))
+                        .collect::<Vec<_>>();
+                    (Some(arguments), start..buffer_position)
                 } else {
                     let start =
                         buffer.anchor_after(Point::new(position.row, call.name.start as u32));
-                    (name, None, command_range, start..buffer_position)
-                })
+                    (None, start..buffer_position)
+                };
+
+                Some((name, arguments, command_range, argument_range))
             })
         else {
             return Task::ready(Ok(Vec::new()));
         };
 
-        if let Some(argument) = argument {
-            self.complete_command_argument(&name, argument, command_range, argument_range, cx)
+        if let Some(arguments) = arguments {
+            self.complete_command_argument(&name, &arguments, command_range, argument_range, cx)
         } else {
             self.complete_command_name(&name, command_range, argument_range, cx)
         }
@@ -325,16 +331,23 @@ impl SlashCommandLine {
             if let Some(call) = &mut call {
                 // The command arguments start at the first non-whitespace character
                 // after the command name, and continue until the end of the line.
-                if let Some(argument) = &mut call.argument {
-                    if (*argument).is_empty() && c.is_whitespace() {
-                        argument.start = next_ix;
+                if let Some(argument) = call.arguments.last_mut() {
+                    if c.is_whitespace() {
+                        if (*argument).is_empty() {
+                            argument.start = next_ix;
+                            argument.end = next_ix;
+                        } else {
+                            argument.end = ix;
+                            call.arguments.push(next_ix..next_ix);
+                        }
+                    } else {
+                        argument.end = next_ix;
                     }
-                    argument.end = next_ix;
                 }
                 // The command name ends at the first whitespace character.
                 else if !call.name.is_empty() {
                     if c.is_whitespace() {
-                        call.argument = Some(next_ix..next_ix);
+                        call.arguments = vec![next_ix..next_ix];
                     } else {
                         call.name.end = next_ix;
                     }
@@ -350,7 +363,7 @@ impl SlashCommandLine {
             else if c == '/' {
                 call = Some(SlashCommandLine {
                     name: next_ix..next_ix,
-                    argument: None,
+                    arguments: Vec::new(),
                 });
             }
             // The line can't contain anything before the slash except for whitespace.

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

@@ -32,7 +32,7 @@ impl SlashCommand for DefaultSlashCommand {
 
     fn complete_argument(
         self: Arc<Self>,
-        _query: String,
+        _arguments: &[String],
         _cancellation_flag: Arc<AtomicBool>,
         _workspace: Option<WeakView<Workspace>>,
         _cx: &mut WindowContext,
@@ -42,7 +42,7 @@ impl SlashCommand for DefaultSlashCommand {
 
     fn run(
         self: Arc<Self>,
-        _argument: Option<&str>,
+        _arguments: &[String],
         _workspace: WeakView<Workspace>,
         _delegate: Option<Arc<dyn LspAdapterDelegate>>,
         cx: &mut WindowContext,

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

@@ -105,7 +105,7 @@ impl SlashCommand for DiagnosticsSlashCommand {
 
     fn complete_argument(
         self: Arc<Self>,
-        query: String,
+        arguments: &[String],
         cancellation_flag: Arc<AtomicBool>,
         workspace: Option<WeakView<Workspace>>,
         cx: &mut WindowContext,
@@ -113,7 +113,7 @@ impl SlashCommand for DiagnosticsSlashCommand {
         let Some(workspace) = workspace.and_then(|workspace| workspace.upgrade()) else {
             return Task::ready(Err(anyhow!("workspace was dropped")));
         };
-        let query = query.split_whitespace().last().unwrap_or("").to_string();
+        let query = arguments.last().cloned().unwrap_or_default();
 
         let paths = self.search_paths(query.clone(), cancellation_flag.clone(), &workspace, cx);
         let executor = cx.background_executor().clone();
@@ -157,7 +157,7 @@ impl SlashCommand for DiagnosticsSlashCommand {
 
     fn run(
         self: Arc<Self>,
-        argument: Option<&str>,
+        arguments: &[String],
         workspace: WeakView<Workspace>,
         _delegate: Option<Arc<dyn LspAdapterDelegate>>,
         cx: &mut WindowContext,
@@ -166,7 +166,7 @@ impl SlashCommand for DiagnosticsSlashCommand {
             return Task::ready(Err(anyhow!("workspace was dropped")));
         };
 
-        let options = Options::parse(argument);
+        let options = Options::parse(arguments);
 
         let task = collect_diagnostics(workspace.read(cx).project().clone(), options, cx);
 
@@ -244,25 +244,20 @@ struct Options {
 const INCLUDE_WARNINGS_ARGUMENT: &str = "--include-warnings";
 
 impl Options {
-    fn parse(arguments_line: Option<&str>) -> Self {
-        arguments_line
-            .map(|arguments_line| {
-                let args = arguments_line.split_whitespace().collect::<Vec<_>>();
-                let mut include_warnings = false;
-                let mut path_matcher = None;
-                for arg in args {
-                    if arg == INCLUDE_WARNINGS_ARGUMENT {
-                        include_warnings = true;
-                    } else {
-                        path_matcher = PathMatcher::new(&[arg.to_owned()]).log_err();
-                    }
-                }
-                Self {
-                    include_warnings,
-                    path_matcher,
-                }
-            })
-            .unwrap_or_default()
+    fn parse(arguments: &[String]) -> Self {
+        let mut include_warnings = false;
+        let mut path_matcher = None;
+        for arg in arguments {
+            if arg == INCLUDE_WARNINGS_ARGUMENT {
+                include_warnings = true;
+            } else {
+                path_matcher = PathMatcher::new(&[arg.to_owned()]).log_err();
+            }
+        }
+        Self {
+            include_warnings,
+            path_matcher,
+        }
     }
 
     fn match_candidates_for_args() -> [StringMatchCandidate; 1] {

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

@@ -161,7 +161,7 @@ impl SlashCommand for DocsSlashCommand {
 
     fn complete_argument(
         self: Arc<Self>,
-        query: String,
+        arguments: &[String],
         _cancel: Arc<AtomicBool>,
         workspace: Option<WeakView<Workspace>>,
         cx: &mut WindowContext,
@@ -169,21 +169,18 @@ impl SlashCommand for DocsSlashCommand {
         self.ensure_rust_doc_providers_are_registered(workspace, cx);
 
         let indexed_docs_registry = IndexedDocsRegistry::global(cx);
-        let args = DocsSlashCommandArgs::parse(&query);
+        let args = DocsSlashCommandArgs::parse(arguments);
         let store = args
             .provider()
             .ok_or_else(|| anyhow!("no docs provider specified"))
             .and_then(|provider| IndexedDocsStore::try_global(provider, cx));
         cx.background_executor().spawn(async move {
-            fn build_completions(
-                provider: ProviderId,
-                items: Vec<String>,
-            ) -> Vec<ArgumentCompletion> {
+            fn build_completions(items: Vec<String>) -> Vec<ArgumentCompletion> {
                 items
                     .into_iter()
                     .map(|item| ArgumentCompletion {
                         label: item.clone().into(),
-                        new_text: format!("{provider} {item}"),
+                        new_text: item.to_string(),
                         run_command: true,
                     })
                     .collect()
@@ -225,7 +222,7 @@ impl SlashCommand for DocsSlashCommand {
                     let suggested_packages = store.clone().suggest_packages().await?;
                     let search_results = store.search(package).await;
 
-                    let mut items = build_completions(provider.clone(), search_results);
+                    let mut items = build_completions(search_results);
                     let workspace_crate_completions = suggested_packages
                         .into_iter()
                         .filter(|package_name| {
@@ -235,8 +232,8 @@ impl SlashCommand for DocsSlashCommand {
                         })
                         .map(|package_name| ArgumentCompletion {
                             label: format!("{package_name} (unindexed)").into(),
-                            new_text: format!("{provider} {package_name}"),
-                            run_command: true,
+                            new_text: format!("{package_name}"),
+                            run_command: false,
                         })
                         .collect::<Vec<_>>();
                     items.extend(workspace_crate_completions);
@@ -255,14 +252,10 @@ impl SlashCommand for DocsSlashCommand {
 
                     Ok(items)
                 }
-                DocsSlashCommandArgs::SearchItemDocs {
-                    provider,
-                    item_path,
-                    ..
-                } => {
+                DocsSlashCommandArgs::SearchItemDocs { item_path, .. } => {
                     let store = store?;
                     let items = store.search(item_path).await;
-                    Ok(build_completions(provider, items))
+                    Ok(build_completions(items))
                 }
             }
         })
@@ -270,16 +263,16 @@ impl SlashCommand for DocsSlashCommand {
 
     fn run(
         self: Arc<Self>,
-        argument: Option<&str>,
+        arguments: &[String],
         _workspace: WeakView<Workspace>,
         _delegate: Option<Arc<dyn LspAdapterDelegate>>,
         cx: &mut WindowContext,
     ) -> Task<Result<SlashCommandOutput>> {
-        let Some(argument) = argument else {
-            return Task::ready(Err(anyhow!("missing argument")));
+        if arguments.is_empty() {
+            return Task::ready(Err(anyhow!("missing an argument")));
         };
 
-        let args = DocsSlashCommandArgs::parse(argument);
+        let args = DocsSlashCommandArgs::parse(arguments);
         let executor = cx.background_executor().clone();
         let task = cx.background_executor().spawn({
             let store = args
@@ -379,12 +372,18 @@ pub(crate) enum DocsSlashCommandArgs {
 }
 
 impl DocsSlashCommandArgs {
-    pub fn parse(argument: &str) -> Self {
-        let Some((provider, argument)) = argument.split_once(' ') else {
+    pub fn parse(arguments: &[String]) -> Self {
+        let Some(provider) = arguments
+            .get(0)
+            .cloned()
+            .filter(|arg| !arg.trim().is_empty())
+        else {
             return Self::NoProvider;
         };
-
         let provider = ProviderId(provider.into());
+        let Some(argument) = arguments.get(1) else {
+            return Self::NoProvider;
+        };
 
         if let Some((package, rest)) = argument.split_once(is_item_path_delimiter) {
             if rest.trim().is_empty() {
@@ -444,16 +443,16 @@ mod tests {
     #[test]
     fn test_parse_docs_slash_command_args() {
         assert_eq!(
-            DocsSlashCommandArgs::parse(""),
+            DocsSlashCommandArgs::parse(&["".to_string()]),
             DocsSlashCommandArgs::NoProvider
         );
         assert_eq!(
-            DocsSlashCommandArgs::parse("rustdoc"),
+            DocsSlashCommandArgs::parse(&["rustdoc".to_string()]),
             DocsSlashCommandArgs::NoProvider
         );
 
         assert_eq!(
-            DocsSlashCommandArgs::parse("rustdoc "),
+            DocsSlashCommandArgs::parse(&["rustdoc".to_string(), "".to_string()]),
             DocsSlashCommandArgs::SearchPackageDocs {
                 provider: ProviderId("rustdoc".into()),
                 package: "".into(),
@@ -461,7 +460,7 @@ mod tests {
             }
         );
         assert_eq!(
-            DocsSlashCommandArgs::parse("gleam "),
+            DocsSlashCommandArgs::parse(&["gleam".to_string(), "".to_string()]),
             DocsSlashCommandArgs::SearchPackageDocs {
                 provider: ProviderId("gleam".into()),
                 package: "".into(),
@@ -470,7 +469,7 @@ mod tests {
         );
 
         assert_eq!(
-            DocsSlashCommandArgs::parse("rustdoc gpui"),
+            DocsSlashCommandArgs::parse(&["rustdoc".to_string(), "gpui".to_string()]),
             DocsSlashCommandArgs::SearchPackageDocs {
                 provider: ProviderId("rustdoc".into()),
                 package: "gpui".into(),
@@ -478,7 +477,7 @@ mod tests {
             }
         );
         assert_eq!(
-            DocsSlashCommandArgs::parse("gleam gleam_stdlib"),
+            DocsSlashCommandArgs::parse(&["gleam".to_string(), "gleam_stdlib".to_string()]),
             DocsSlashCommandArgs::SearchPackageDocs {
                 provider: ProviderId("gleam".into()),
                 package: "gleam_stdlib".into(),
@@ -488,7 +487,7 @@ mod tests {
 
         // Adding an item path delimiter indicates we can start indexing.
         assert_eq!(
-            DocsSlashCommandArgs::parse("rustdoc gpui:"),
+            DocsSlashCommandArgs::parse(&["rustdoc".to_string(), "gpui:".to_string()]),
             DocsSlashCommandArgs::SearchPackageDocs {
                 provider: ProviderId("rustdoc".into()),
                 package: "gpui".into(),
@@ -496,7 +495,7 @@ mod tests {
             }
         );
         assert_eq!(
-            DocsSlashCommandArgs::parse("gleam gleam_stdlib/"),
+            DocsSlashCommandArgs::parse(&["gleam".to_string(), "gleam_stdlib/".to_string()]),
             DocsSlashCommandArgs::SearchPackageDocs {
                 provider: ProviderId("gleam".into()),
                 package: "gleam_stdlib".into(),
@@ -505,7 +504,10 @@ mod tests {
         );
 
         assert_eq!(
-            DocsSlashCommandArgs::parse("rustdoc gpui::foo::bar::Baz"),
+            DocsSlashCommandArgs::parse(&[
+                "rustdoc".to_string(),
+                "gpui::foo::bar::Baz".to_string()
+            ]),
             DocsSlashCommandArgs::SearchItemDocs {
                 provider: ProviderId("rustdoc".into()),
                 package: "gpui".into(),
@@ -513,7 +515,10 @@ mod tests {
             }
         );
         assert_eq!(
-            DocsSlashCommandArgs::parse("gleam gleam_stdlib/gleam/int"),
+            DocsSlashCommandArgs::parse(&[
+                "gleam".to_string(),
+                "gleam_stdlib/gleam/int".to_string()
+            ]),
             DocsSlashCommandArgs::SearchItemDocs {
                 provider: ProviderId("gleam".into()),
                 package: "gleam_stdlib".into(),

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

@@ -117,7 +117,7 @@ impl SlashCommand for FetchSlashCommand {
 
     fn complete_argument(
         self: Arc<Self>,
-        _query: String,
+        _arguments: &[String],
         _cancel: Arc<AtomicBool>,
         _workspace: Option<WeakView<Workspace>>,
         _cx: &mut WindowContext,
@@ -127,12 +127,12 @@ impl SlashCommand for FetchSlashCommand {
 
     fn run(
         self: Arc<Self>,
-        argument: Option<&str>,
+        arguments: &[String],
         workspace: WeakView<Workspace>,
         _delegate: Option<Arc<dyn LspAdapterDelegate>>,
         cx: &mut WindowContext,
     ) -> Task<Result<SlashCommandOutput>> {
-        let Some(argument) = argument else {
+        let Some(argument) = arguments.first() else {
             return Task::ready(Err(anyhow!("missing URL")));
         };
         let Some(workspace) = workspace.upgrade() else {

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

@@ -122,7 +122,7 @@ impl SlashCommand for FileSlashCommand {
 
     fn complete_argument(
         self: Arc<Self>,
-        query: String,
+        arguments: &[String],
         cancellation_flag: Arc<AtomicBool>,
         workspace: Option<WeakView<Workspace>>,
         cx: &mut WindowContext,
@@ -131,7 +131,12 @@ impl SlashCommand for FileSlashCommand {
             return Task::ready(Err(anyhow!("workspace was dropped")));
         };
 
-        let paths = self.search_paths(query, cancellation_flag, &workspace, cx);
+        let paths = self.search_paths(
+            arguments.last().cloned().unwrap_or_default(),
+            cancellation_flag,
+            &workspace,
+            cx,
+        );
         let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
         cx.background_executor().spawn(async move {
             Ok(paths
@@ -168,7 +173,7 @@ impl SlashCommand for FileSlashCommand {
 
     fn run(
         self: Arc<Self>,
-        argument: Option<&str>,
+        arguments: &[String],
         workspace: WeakView<Workspace>,
         _delegate: Option<Arc<dyn LspAdapterDelegate>>,
         cx: &mut WindowContext,
@@ -177,7 +182,7 @@ impl SlashCommand for FileSlashCommand {
             return Task::ready(Err(anyhow!("workspace was dropped")));
         };
 
-        let Some(argument) = argument else {
+        let Some(argument) = arguments.first() else {
             return Task::ready(Err(anyhow!("missing path")));
         };
 

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

@@ -32,7 +32,7 @@ impl SlashCommand for NowSlashCommand {
 
     fn complete_argument(
         self: Arc<Self>,
-        _query: String,
+        _arguments: &[String],
         _cancel: Arc<AtomicBool>,
         _workspace: Option<WeakView<Workspace>>,
         _cx: &mut WindowContext,
@@ -42,7 +42,7 @@ impl SlashCommand for NowSlashCommand {
 
     fn run(
         self: Arc<Self>,
-        _argument: Option<&str>,
+        _arguments: &[String],
         _workspace: WeakView<Workspace>,
         _delegate: Option<Arc<dyn LspAdapterDelegate>>,
         _cx: &mut WindowContext,

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

@@ -103,7 +103,7 @@ impl SlashCommand for ProjectSlashCommand {
 
     fn complete_argument(
         self: Arc<Self>,
-        _query: String,
+        _arguments: &[String],
         _cancel: Arc<AtomicBool>,
         _workspace: Option<WeakView<Workspace>>,
         _cx: &mut WindowContext,
@@ -117,7 +117,7 @@ impl SlashCommand for ProjectSlashCommand {
 
     fn run(
         self: Arc<Self>,
-        _argument: Option<&str>,
+        _arguments: &[String],
         workspace: WeakView<Workspace>,
         _delegate: Option<Arc<dyn LspAdapterDelegate>>,
         cx: &mut WindowContext,

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

@@ -29,12 +29,13 @@ impl SlashCommand for PromptSlashCommand {
 
     fn complete_argument(
         self: Arc<Self>,
-        query: String,
+        arguments: &[String],
         _cancellation_flag: Arc<AtomicBool>,
         _workspace: Option<WeakView<Workspace>>,
         cx: &mut WindowContext,
     ) -> Task<Result<Vec<ArgumentCompletion>>> {
         let store = PromptStore::global(cx);
+        let query = arguments.last().cloned().unwrap_or_default();
         cx.background_executor().spawn(async move {
             let prompts = store.await?.search(query).await;
             Ok(prompts
@@ -53,12 +54,12 @@ impl SlashCommand for PromptSlashCommand {
 
     fn run(
         self: Arc<Self>,
-        title: Option<&str>,
+        arguments: &[String],
         _workspace: WeakView<Workspace>,
         _delegate: Option<Arc<dyn LspAdapterDelegate>>,
         cx: &mut WindowContext,
     ) -> Task<Result<SlashCommandOutput>> {
-        let Some(title) = title else {
+        let Some(title) = arguments.first() else {
             return Task::ready(Err(anyhow!("missing prompt name")));
         };
 

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

@@ -49,7 +49,7 @@ impl SlashCommand for SearchSlashCommand {
 
     fn complete_argument(
         self: Arc<Self>,
-        _query: String,
+        _arguments: &[String],
         _cancel: Arc<AtomicBool>,
         _workspace: Option<WeakView<Workspace>>,
         _cx: &mut WindowContext,
@@ -59,7 +59,7 @@ impl SlashCommand for SearchSlashCommand {
 
     fn run(
         self: Arc<Self>,
-        argument: Option<&str>,
+        arguments: &[String],
         workspace: WeakView<Workspace>,
         _delegate: Option<Arc<dyn LspAdapterDelegate>>,
         cx: &mut WindowContext,
@@ -67,13 +67,13 @@ impl SlashCommand for SearchSlashCommand {
         let Some(workspace) = workspace.upgrade() else {
             return Task::ready(Err(anyhow::anyhow!("workspace was dropped")));
         };
-        let Some(argument) = argument else {
+        if arguments.is_empty() {
             return Task::ready(Err(anyhow::anyhow!("missing search query")));
         };
 
         let mut limit = None;
         let mut query = String::new();
-        for part in argument.split(' ') {
+        for part in arguments {
             if let Some(parameter) = part.strip_prefix("--") {
                 if let Ok(count) = parameter.parse::<usize>() {
                     limit = Some(count);

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

@@ -26,7 +26,7 @@ impl SlashCommand for OutlineSlashCommand {
 
     fn complete_argument(
         self: Arc<Self>,
-        _query: String,
+        _arguments: &[String],
         _cancel: Arc<AtomicBool>,
         _workspace: Option<WeakView<Workspace>>,
         _cx: &mut WindowContext,
@@ -40,7 +40,7 @@ impl SlashCommand for OutlineSlashCommand {
 
     fn run(
         self: Arc<Self>,
-        _argument: Option<&str>,
+        _arguments: &[String],
         workspace: WeakView<Workspace>,
         _delegate: Option<Arc<dyn LspAdapterDelegate>>,
         cx: &mut WindowContext,

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

@@ -5,8 +5,9 @@ use super::{
 };
 use anyhow::{Context, Result};
 use assistant_slash_command::ArgumentCompletion;
-use collections::HashMap;
+use collections::{HashMap, HashSet};
 use editor::Editor;
+use futures::future::join_all;
 use gpui::{Entity, Task, WeakView};
 use language::{BufferSnapshot, LspAdapterDelegate};
 use std::{
@@ -17,13 +18,13 @@ use std::{
 use ui::WindowContext;
 use workspace::Workspace;
 
-pub(crate) struct TabsSlashCommand;
+pub(crate) struct TabSlashCommand;
 
 const ALL_TABS_COMPLETION_ITEM: &str = "all";
 
-impl SlashCommand for TabsSlashCommand {
+impl SlashCommand for TabSlashCommand {
     fn name(&self) -> String {
-        "tabs".into()
+        "tab".into()
     }
 
     fn description(&self) -> String {
@@ -40,51 +41,66 @@ impl SlashCommand for TabsSlashCommand {
 
     fn complete_argument(
         self: Arc<Self>,
-        query: String,
+        arguments: &[String],
         cancel: Arc<AtomicBool>,
         workspace: Option<WeakView<Workspace>>,
         cx: &mut WindowContext,
     ) -> Task<Result<Vec<ArgumentCompletion>>> {
-        let all_tabs_completion_item = if ALL_TABS_COMPLETION_ITEM.contains(&query) {
-            Some(ArgumentCompletion {
+        let mut has_all_tabs_completion_item = false;
+        let argument_set = arguments
+            .iter()
+            .filter(|argument| {
+                if has_all_tabs_completion_item || ALL_TABS_COMPLETION_ITEM == argument.as_str() {
+                    has_all_tabs_completion_item = true;
+                    false
+                } else {
+                    true
+                }
+            })
+            .cloned()
+            .collect::<HashSet<_>>();
+        if has_all_tabs_completion_item {
+            return Task::ready(Ok(Vec::new()));
+        }
+        let current_query = arguments.last().cloned().unwrap_or_default();
+        let tab_items_search =
+            tab_items_for_queries(workspace, &[current_query], cancel, false, cx);
+        cx.spawn(|_| async move {
+            let tab_items = tab_items_search.await?;
+            let run_command = tab_items.len() == 1;
+            let tab_completion_items = tab_items.into_iter().filter_map(|(path, ..)| {
+                let path_string = path.as_deref()?.to_string_lossy().to_string();
+                if argument_set.contains(&path_string) {
+                    return None;
+                }
+                Some(ArgumentCompletion {
+                    label: path_string.clone().into(),
+                    new_text: path_string,
+                    run_command,
+                })
+            });
+
+            Ok(Some(ArgumentCompletion {
                 label: ALL_TABS_COMPLETION_ITEM.into(),
                 new_text: ALL_TABS_COMPLETION_ITEM.to_owned(),
                 run_command: true,
             })
-        } else {
-            None
-        };
-        let tab_items_search = tab_items_for_query(workspace, query, cancel, false, cx);
-        cx.spawn(|_| async move {
-            let tab_completion_items =
-                tab_items_search
-                    .await?
-                    .into_iter()
-                    .filter_map(|(path, ..)| {
-                        let path_string = path.as_deref()?.to_string_lossy().to_string();
-                        Some(ArgumentCompletion {
-                            label: path_string.clone().into(),
-                            new_text: path_string,
-                            run_command: true,
-                        })
-                    });
-            Ok(all_tabs_completion_item
-                .into_iter()
-                .chain(tab_completion_items)
-                .collect::<Vec<_>>())
+            .into_iter()
+            .chain(tab_completion_items)
+            .collect::<Vec<_>>())
         })
     }
 
     fn run(
         self: Arc<Self>,
-        argument: Option<&str>,
+        arguments: &[String],
         workspace: WeakView<Workspace>,
         _delegate: Option<Arc<dyn LspAdapterDelegate>>,
         cx: &mut WindowContext,
     ) -> Task<Result<SlashCommandOutput>> {
-        let tab_items_search = tab_items_for_query(
+        let tab_items_search = tab_items_for_queries(
             Some(workspace),
-            argument.map(ToOwned::to_owned).unwrap_or_default(),
+            arguments,
             Arc::new(AtomicBool::new(false)),
             true,
             cx,
@@ -129,20 +145,21 @@ impl SlashCommand for TabsSlashCommand {
     }
 }
 
-fn tab_items_for_query(
+fn tab_items_for_queries(
     workspace: Option<WeakView<Workspace>>,
-    mut query: String,
+    queries: &[String],
     cancel: Arc<AtomicBool>,
-    use_active_tab_for_empty_query: bool,
+    strict_match: bool,
     cx: &mut WindowContext,
 ) -> Task<anyhow::Result<Vec<(Option<PathBuf>, BufferSnapshot, usize)>>> {
+    let empty_query = queries.is_empty() || queries.iter().all(|query| query.trim().is_empty());
+    let queries = queries.to_owned();
     cx.spawn(|mut cx| async move {
-        query.make_ascii_lowercase();
         let mut open_buffers =
             workspace
                 .context("no workspace")?
                 .update(&mut cx, |workspace, cx| {
-                    if use_active_tab_for_empty_query && query.trim().is_empty() {
+                    if strict_match && empty_query {
                         let active_editor = workspace
                             .active_item(cx)
                             .context("no active item")?
@@ -189,38 +206,73 @@ fn tab_items_for_query(
         cx.background_executor()
             .spawn(async move {
                 open_buffers.sort_by_key(|(_, _, timestamp)| *timestamp);
-                let query = query.trim();
-                if query.is_empty() || query == ALL_TABS_COMPLETION_ITEM {
+                if empty_query
+                    || queries
+                        .iter()
+                        .any(|query| query == ALL_TABS_COMPLETION_ITEM)
+                {
                     return Ok(open_buffers);
                 }
 
-                let match_candidates = open_buffers
-                    .iter()
-                    .enumerate()
-                    .filter_map(|(id, (full_path, ..))| {
-                        let path_string = full_path.as_deref()?.to_string_lossy().to_string();
-                        Some(fuzzy::StringMatchCandidate {
-                            id,
-                            char_bag: path_string.as_str().into(),
-                            string: path_string,
+                let matched_items = if strict_match {
+                    let match_candidates = open_buffers
+                        .iter()
+                        .enumerate()
+                        .filter_map(|(id, (full_path, ..))| {
+                            let path_string = full_path.as_deref()?.to_string_lossy().to_string();
+                            Some((id, path_string))
+                        })
+                        .fold(HashMap::default(), |mut candidates, (id, path_string)| {
+                            candidates
+                                .entry(path_string)
+                                .or_insert_with(|| Vec::new())
+                                .push(id);
+                            candidates
+                        });
+
+                    queries
+                        .iter()
+                        .filter_map(|query| match_candidates.get(query))
+                        .flatten()
+                        .copied()
+                        .filter_map(|id| open_buffers.get(id))
+                        .cloned()
+                        .collect()
+                } else {
+                    let match_candidates = open_buffers
+                        .iter()
+                        .enumerate()
+                        .filter_map(|(id, (full_path, ..))| {
+                            let path_string = full_path.as_deref()?.to_string_lossy().to_string();
+                            Some(fuzzy::StringMatchCandidate {
+                                id,
+                                char_bag: path_string.as_str().into(),
+                                string: path_string,
+                            })
                         })
-                    })
-                    .collect::<Vec<_>>();
-                let string_matches = fuzzy::match_strings(
-                    &match_candidates,
-                    &query,
-                    true,
-                    usize::MAX,
-                    &cancel,
-                    background_executor,
-                )
-                .await;
-
-                Ok(string_matches
-                    .into_iter()
-                    .filter_map(|string_match| open_buffers.get(string_match.candidate_id))
-                    .cloned()
-                    .collect())
+                        .collect::<Vec<_>>();
+                    let mut processed_matches = HashSet::default();
+                    let file_queries = queries.iter().map(|query| {
+                        fuzzy::match_strings(
+                            &match_candidates,
+                            query,
+                            true,
+                            usize::MAX,
+                            &cancel,
+                            background_executor.clone(),
+                        )
+                    });
+
+                    join_all(file_queries)
+                        .await
+                        .into_iter()
+                        .flatten()
+                        .filter(|string_match| processed_matches.insert(string_match.candidate_id))
+                        .filter_map(|string_match| open_buffers.get(string_match.candidate_id))
+                        .cloned()
+                        .collect()
+                };
+                Ok(matched_items)
             })
             .await
     })

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

@@ -42,21 +42,26 @@ impl SlashCommand for TerminalSlashCommand {
 
     fn complete_argument(
         self: Arc<Self>,
-        _query: String,
+        arguments: &[String],
         _cancel: Arc<AtomicBool>,
         _workspace: Option<WeakView<Workspace>>,
         _cx: &mut WindowContext,
     ) -> Task<Result<Vec<ArgumentCompletion>>> {
-        Task::ready(Ok(vec![ArgumentCompletion {
-            label: LINE_COUNT_ARG.into(),
-            new_text: LINE_COUNT_ARG.to_string(),
-            run_command: true,
-        }]))
+        let completions = if arguments.iter().any(|arg| arg == LINE_COUNT_ARG) {
+            Vec::new()
+        } else {
+            vec![ArgumentCompletion {
+                label: LINE_COUNT_ARG.into(),
+                new_text: LINE_COUNT_ARG.to_string(),
+                run_command: false,
+            }]
+        };
+        Task::ready(Ok(completions))
     }
 
     fn run(
         self: Arc<Self>,
-        argument: Option<&str>,
+        arguments: &[String],
         workspace: WeakView<Workspace>,
         _delegate: Option<Arc<dyn LspAdapterDelegate>>,
         cx: &mut WindowContext,
@@ -75,9 +80,13 @@ impl SlashCommand for TerminalSlashCommand {
             return Task::ready(Err(anyhow::anyhow!("no active terminal")));
         };
 
-        let line_count = argument
-            .and_then(|a| parse_argument(a))
-            .unwrap_or(DEFAULT_CONTEXT_LINES);
+        let mut line_count = DEFAULT_CONTEXT_LINES;
+        if arguments.get(0).map(|s| s.as_str()) == Some(LINE_COUNT_ARG) {
+            if let Some(parsed_line_count) = arguments.get(1).and_then(|s| s.parse::<usize>().ok())
+            {
+                line_count = parsed_line_count;
+            }
+        }
 
         let lines = active_terminal
             .read(cx)
@@ -101,13 +110,3 @@ impl SlashCommand for TerminalSlashCommand {
         }))
     }
 }
-
-fn parse_argument(argument: &str) -> Option<usize> {
-    let mut args = argument.split(' ');
-    if args.next() == Some(LINE_COUNT_ARG) {
-        if let Some(line_count) = args.next().and_then(|s| s.parse::<usize>().ok()) {
-            return Some(line_count);
-        }
-    }
-    None
-}

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

@@ -42,7 +42,7 @@ impl SlashCommand for WorkflowSlashCommand {
 
     fn complete_argument(
         self: Arc<Self>,
-        _query: String,
+        _arguments: &[String],
         _cancel: Arc<AtomicBool>,
         _workspace: Option<WeakView<Workspace>>,
         _cx: &mut WindowContext,
@@ -52,7 +52,7 @@ impl SlashCommand for WorkflowSlashCommand {
 
     fn run(
         self: Arc<Self>,
-        _argument: Option<&str>,
+        _arguments: &[String],
         _workspace: WeakView<Workspace>,
         _delegate: Option<Arc<dyn LspAdapterDelegate>>,
         cx: &mut WindowContext,

crates/assistant_slash_command/src/assistant_slash_command.rs 🔗

@@ -34,7 +34,7 @@ pub trait SlashCommand: 'static + Send + Sync {
     fn menu_text(&self) -> String;
     fn complete_argument(
         self: Arc<Self>,
-        query: String,
+        arguments: &[String],
         cancel: Arc<AtomicBool>,
         workspace: Option<WeakView<Workspace>>,
         cx: &mut WindowContext,
@@ -42,7 +42,7 @@ pub trait SlashCommand: 'static + Send + Sync {
     fn requires_argument(&self) -> bool;
     fn run(
         self: Arc<Self>,
-        argument: Option<&str>,
+        arguments: &[String],
         workspace: WeakView<Workspace>,
         // TODO: We're just using the `LspAdapterDelegate` here because that is
         // what the extension API is already expecting.

crates/extension/src/extension_slash_command.rs 🔗

@@ -39,11 +39,12 @@ impl SlashCommand for ExtensionSlashCommand {
 
     fn complete_argument(
         self: Arc<Self>,
-        query: String,
+        arguments: &[String],
         _cancel: Arc<AtomicBool>,
         _workspace: Option<WeakView<Workspace>>,
         cx: &mut WindowContext,
     ) -> Task<Result<Vec<ArgumentCompletion>>> {
+        let arguments = arguments.to_owned();
         cx.background_executor().spawn(async move {
             self.extension
                 .call({
@@ -54,7 +55,7 @@ impl SlashCommand for ExtensionSlashCommand {
                                 .call_complete_slash_command_argument(
                                     store,
                                     &this.command,
-                                    query.as_ref(),
+                                    &arguments,
                                 )
                                 .await?
                                 .map_err(|e| anyhow!("{}", e))?;
@@ -79,12 +80,12 @@ impl SlashCommand for ExtensionSlashCommand {
 
     fn run(
         self: Arc<Self>,
-        argument: Option<&str>,
+        arguments: &[String],
         _workspace: WeakView<Workspace>,
         delegate: Option<Arc<dyn LspAdapterDelegate>>,
         cx: &mut WindowContext,
     ) -> Task<Result<SlashCommandOutput>> {
-        let argument = argument.map(|arg| arg.to_string());
+        let arguments = arguments.to_owned();
         let output = cx.background_executor().spawn(async move {
             self.extension
                 .call({
@@ -97,12 +98,7 @@ impl SlashCommand for ExtensionSlashCommand {
                                 None
                             };
                             let output = extension
-                                .call_run_slash_command(
-                                    store,
-                                    &this.command,
-                                    argument.as_deref(),
-                                    resource,
-                                )
+                                .call_run_slash_command(store, &this.command, &arguments, resource)
                                 .await?
                                 .map_err(|e| anyhow!("{}", e))?;
 

crates/extension/src/wasm_host/wit.rs 🔗

@@ -262,11 +262,11 @@ impl Extension {
         &self,
         store: &mut Store<WasmState>,
         command: &SlashCommand,
-        query: &str,
+        arguments: &[String],
     ) -> Result<Result<Vec<SlashCommandArgumentCompletion>, String>> {
         match self {
             Extension::V010(ext) => {
-                ext.call_complete_slash_command_argument(store, command, query)
+                ext.call_complete_slash_command_argument(store, command, arguments)
                     .await
             }
             Extension::V001(_) | Extension::V004(_) | Extension::V006(_) => Ok(Ok(Vec::new())),
@@ -277,12 +277,12 @@ impl Extension {
         &self,
         store: &mut Store<WasmState>,
         command: &SlashCommand,
-        argument: Option<&str>,
+        arguments: &[String],
         resource: Option<Resource<Arc<dyn LspAdapterDelegate>>>,
     ) -> Result<Result<SlashCommandOutput, String>> {
         match self {
             Extension::V010(ext) => {
-                ext.call_run_slash_command(store, command, argument, resource)
+                ext.call_run_slash_command(store, command, arguments, resource)
                     .await
             }
             Extension::V001(_) | Extension::V004(_) | Extension::V006(_) => {

crates/extension_api/src/extension_api.rs 🔗

@@ -114,7 +114,7 @@ pub trait Extension: Send + Sync {
     fn complete_slash_command_argument(
         &self,
         _command: SlashCommand,
-        _query: String,
+        _args: Vec<String>,
     ) -> Result<Vec<SlashCommandArgumentCompletion>, String> {
         Ok(Vec::new())
     }
@@ -123,7 +123,7 @@ pub trait Extension: Send + Sync {
     fn run_slash_command(
         &self,
         _command: SlashCommand,
-        _argument: Option<String>,
+        _args: Vec<String>,
         _worktree: Option<&Worktree>,
     ) -> Result<SlashCommandOutput, String> {
         Err("`run_slash_command` not implemented".to_string())
@@ -257,17 +257,17 @@ impl wit::Guest for Component {
 
     fn complete_slash_command_argument(
         command: SlashCommand,
-        query: String,
+        args: Vec<String>,
     ) -> Result<Vec<SlashCommandArgumentCompletion>, String> {
-        extension().complete_slash_command_argument(command, query)
+        extension().complete_slash_command_argument(command, args)
     }
 
     fn run_slash_command(
         command: SlashCommand,
-        argument: Option<String>,
+        args: Vec<String>,
         worktree: Option<&Worktree>,
     ) -> Result<SlashCommandOutput, String> {
-        extension().run_slash_command(command, argument, worktree)
+        extension().run_slash_command(command, args, worktree)
     }
 
     fn suggest_docs_packages(provider: String) -> Result<Vec<String>, String> {

crates/extension_api/wit/since_v0.1.0/extension.wit 🔗

@@ -130,10 +130,10 @@ world extension {
     export labels-for-symbols: func(language-server-id: string, symbols: list<symbol>) -> result<list<option<code-label>>, string>;
 
     /// Returns the completions that should be shown when completing the provided slash command with the given query.
-    export complete-slash-command-argument: func(command: slash-command, query: string) -> result<list<slash-command-argument-completion>, string>;
+    export complete-slash-command-argument: func(command: slash-command, args: list<string>) -> result<list<slash-command-argument-completion>, string>;
 
     /// Returns the output from running the provided slash command.
-    export run-slash-command: func(command: slash-command, argument: option<string>, worktree: option<borrow<worktree>>) -> result<slash-command-output, string>;
+    export run-slash-command: func(command: slash-command, args: list<string>, worktree: option<borrow<worktree>>) -> result<slash-command-output, string>;
 
     /// Returns a list of packages as suggestions to be included in the `/docs`
     /// search results.

extensions/gleam/src/gleam.rs 🔗

@@ -154,7 +154,7 @@ impl zed::Extension for GleamExtension {
     fn complete_slash_command_argument(
         &self,
         command: SlashCommand,
-        _query: String,
+        _arguments: Vec<String>,
     ) -> Result<Vec<SlashCommandArgumentCompletion>, String> {
         match command.name.as_str() {
             "gleam-project" => Ok(vec![
@@ -181,12 +181,12 @@ impl zed::Extension for GleamExtension {
     fn run_slash_command(
         &self,
         command: SlashCommand,
-        argument: Option<String>,
+        args: Vec<String>,
         worktree: Option<&zed::Worktree>,
     ) -> Result<SlashCommandOutput, String> {
         match command.name.as_str() {
             "gleam-docs" => {
-                let argument = argument.ok_or_else(|| "missing argument".to_string())?;
+                let argument = args.last().ok_or_else(|| "missing argument".to_string())?;
 
                 let mut components = argument.split('/');
                 let package_name = components