diff --git a/crates/assistant/src/assistant.rs b/crates/assistant/src/assistant.rs index 3b9941e9ed..adb560793f 100644 --- a/crates/assistant/src/assistant.rs +++ b/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>, 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); diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index be2ec6d58b..7ad125fb31 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/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, name: &str, - argument: Option<&str>, + arguments: &[String], insert_trailing_newline: bool, workspace: WeakView, cx: &mut ViewContext, ) { 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() diff --git a/crates/assistant/src/context.rs b/crates/assistant/src/context.rs index d0891faa66..b0d489924d 100644 --- a/crates/assistant/src/context.rs +++ b/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::>(); 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, + pub arguments: SmallVec<[String; 3]>, pub status: PendingSlashCommandStatus, pub source_range: Range, } @@ -3758,7 +3768,7 @@ mod tests { fn complete_argument( self: Arc, - _query: String, + _arguments: &[String], _cancel: Arc, _workspace: Option>, _cx: &mut WindowContext, @@ -3772,7 +3782,7 @@ mod tests { fn run( self: Arc, - _argument: Option<&str>, + _arguments: &[String], _workspace: WeakView, _delegate: Option>, _cx: &mut WindowContext, diff --git a/crates/assistant/src/slash_command.rs b/crates/assistant/src/slash_command.rs index 54f69279aa..25f0e4e89b 100644 --- a/crates/assistant/src/slash_command.rs +++ b/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, - /// The range within the line containing the command argument. - pub argument: Option>, + /// Ranges within the line containing the command arguments. + pub arguments: Vec>, } 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, argument_range: Range, 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 = 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, ) -> Task>> { - 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::>(); + (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. diff --git a/crates/assistant/src/slash_command/default_command.rs b/crates/assistant/src/slash_command/default_command.rs index c9afb8a474..18db87b322 100644 --- a/crates/assistant/src/slash_command/default_command.rs +++ b/crates/assistant/src/slash_command/default_command.rs @@ -32,7 +32,7 @@ impl SlashCommand for DefaultSlashCommand { fn complete_argument( self: Arc, - _query: String, + _arguments: &[String], _cancellation_flag: Arc, _workspace: Option>, _cx: &mut WindowContext, @@ -42,7 +42,7 @@ impl SlashCommand for DefaultSlashCommand { fn run( self: Arc, - _argument: Option<&str>, + _arguments: &[String], _workspace: WeakView, _delegate: Option>, cx: &mut WindowContext, diff --git a/crates/assistant/src/slash_command/diagnostics_command.rs b/crates/assistant/src/slash_command/diagnostics_command.rs index c4a251f384..01aefc5d2f 100644 --- a/crates/assistant/src/slash_command/diagnostics_command.rs +++ b/crates/assistant/src/slash_command/diagnostics_command.rs @@ -105,7 +105,7 @@ impl SlashCommand for DiagnosticsSlashCommand { fn complete_argument( self: Arc, - query: String, + arguments: &[String], cancellation_flag: Arc, workspace: Option>, 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, - argument: Option<&str>, + arguments: &[String], workspace: WeakView, _delegate: Option>, 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::>(); - 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] { diff --git a/crates/assistant/src/slash_command/docs_command.rs b/crates/assistant/src/slash_command/docs_command.rs index ecb2c658fc..dce36c67be 100644 --- a/crates/assistant/src/slash_command/docs_command.rs +++ b/crates/assistant/src/slash_command/docs_command.rs @@ -161,7 +161,7 @@ impl SlashCommand for DocsSlashCommand { fn complete_argument( self: Arc, - query: String, + arguments: &[String], _cancel: Arc, workspace: Option>, 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, - ) -> Vec { + fn build_completions(items: Vec) -> Vec { 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::>(); 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, - argument: Option<&str>, + arguments: &[String], _workspace: WeakView, _delegate: Option>, cx: &mut WindowContext, ) -> Task> { - 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(), diff --git a/crates/assistant/src/slash_command/fetch_command.rs b/crates/assistant/src/slash_command/fetch_command.rs index 9ff3382efd..8ecb6de759 100644 --- a/crates/assistant/src/slash_command/fetch_command.rs +++ b/crates/assistant/src/slash_command/fetch_command.rs @@ -117,7 +117,7 @@ impl SlashCommand for FetchSlashCommand { fn complete_argument( self: Arc, - _query: String, + _arguments: &[String], _cancel: Arc, _workspace: Option>, _cx: &mut WindowContext, @@ -127,12 +127,12 @@ impl SlashCommand for FetchSlashCommand { fn run( self: Arc, - argument: Option<&str>, + arguments: &[String], workspace: WeakView, _delegate: Option>, cx: &mut WindowContext, ) -> Task> { - let Some(argument) = argument else { + let Some(argument) = arguments.first() else { return Task::ready(Err(anyhow!("missing URL"))); }; let Some(workspace) = workspace.upgrade() else { diff --git a/crates/assistant/src/slash_command/file_command.rs b/crates/assistant/src/slash_command/file_command.rs index 093659c122..a905b4962a 100644 --- a/crates/assistant/src/slash_command/file_command.rs +++ b/crates/assistant/src/slash_command/file_command.rs @@ -122,7 +122,7 @@ impl SlashCommand for FileSlashCommand { fn complete_argument( self: Arc, - query: String, + arguments: &[String], cancellation_flag: Arc, workspace: Option>, 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, - argument: Option<&str>, + arguments: &[String], workspace: WeakView, _delegate: Option>, 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"))); }; diff --git a/crates/assistant/src/slash_command/now_command.rs b/crates/assistant/src/slash_command/now_command.rs index 3e6a053c97..eb6277a7d9 100644 --- a/crates/assistant/src/slash_command/now_command.rs +++ b/crates/assistant/src/slash_command/now_command.rs @@ -32,7 +32,7 @@ impl SlashCommand for NowSlashCommand { fn complete_argument( self: Arc, - _query: String, + _arguments: &[String], _cancel: Arc, _workspace: Option>, _cx: &mut WindowContext, @@ -42,7 +42,7 @@ impl SlashCommand for NowSlashCommand { fn run( self: Arc, - _argument: Option<&str>, + _arguments: &[String], _workspace: WeakView, _delegate: Option>, _cx: &mut WindowContext, diff --git a/crates/assistant/src/slash_command/project_command.rs b/crates/assistant/src/slash_command/project_command.rs index 8b31d29836..8182734e72 100644 --- a/crates/assistant/src/slash_command/project_command.rs +++ b/crates/assistant/src/slash_command/project_command.rs @@ -103,7 +103,7 @@ impl SlashCommand for ProjectSlashCommand { fn complete_argument( self: Arc, - _query: String, + _arguments: &[String], _cancel: Arc, _workspace: Option>, _cx: &mut WindowContext, @@ -117,7 +117,7 @@ impl SlashCommand for ProjectSlashCommand { fn run( self: Arc, - _argument: Option<&str>, + _arguments: &[String], workspace: WeakView, _delegate: Option>, cx: &mut WindowContext, diff --git a/crates/assistant/src/slash_command/prompt_command.rs b/crates/assistant/src/slash_command/prompt_command.rs index 235a138e33..1a48d94d7a 100644 --- a/crates/assistant/src/slash_command/prompt_command.rs +++ b/crates/assistant/src/slash_command/prompt_command.rs @@ -29,12 +29,13 @@ impl SlashCommand for PromptSlashCommand { fn complete_argument( self: Arc, - query: String, + arguments: &[String], _cancellation_flag: Arc, _workspace: Option>, cx: &mut WindowContext, ) -> Task>> { 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, - title: Option<&str>, + arguments: &[String], _workspace: WeakView, _delegate: Option>, cx: &mut WindowContext, ) -> Task> { - let Some(title) = title else { + let Some(title) = arguments.first() else { return Task::ready(Err(anyhow!("missing prompt name"))); }; diff --git a/crates/assistant/src/slash_command/search_command.rs b/crates/assistant/src/slash_command/search_command.rs index a8c8513684..4da8a5585f 100644 --- a/crates/assistant/src/slash_command/search_command.rs +++ b/crates/assistant/src/slash_command/search_command.rs @@ -49,7 +49,7 @@ impl SlashCommand for SearchSlashCommand { fn complete_argument( self: Arc, - _query: String, + _arguments: &[String], _cancel: Arc, _workspace: Option>, _cx: &mut WindowContext, @@ -59,7 +59,7 @@ impl SlashCommand for SearchSlashCommand { fn run( self: Arc, - argument: Option<&str>, + arguments: &[String], workspace: WeakView, _delegate: Option>, 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::() { limit = Some(count); diff --git a/crates/assistant/src/slash_command/symbols_command.rs b/crates/assistant/src/slash_command/symbols_command.rs index 6bfe933bb0..c9582f2882 100644 --- a/crates/assistant/src/slash_command/symbols_command.rs +++ b/crates/assistant/src/slash_command/symbols_command.rs @@ -26,7 +26,7 @@ impl SlashCommand for OutlineSlashCommand { fn complete_argument( self: Arc, - _query: String, + _arguments: &[String], _cancel: Arc, _workspace: Option>, _cx: &mut WindowContext, @@ -40,7 +40,7 @@ impl SlashCommand for OutlineSlashCommand { fn run( self: Arc, - _argument: Option<&str>, + _arguments: &[String], workspace: WeakView, _delegate: Option>, cx: &mut WindowContext, diff --git a/crates/assistant/src/slash_command/tabs_command.rs b/crates/assistant/src/slash_command/tab_command.rs similarity index 54% rename from crates/assistant/src/slash_command/tabs_command.rs rename to crates/assistant/src/slash_command/tab_command.rs index 848659d530..6bc96ce34b 100644 --- a/crates/assistant/src/slash_command/tabs_command.rs +++ b/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, - query: String, + arguments: &[String], cancel: Arc, workspace: Option>, cx: &mut WindowContext, ) -> Task>> { - 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::>(); + 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::>()) + .into_iter() + .chain(tab_completion_items) + .collect::>()) }) } fn run( self: Arc, - argument: Option<&str>, + arguments: &[String], workspace: WeakView, _delegate: Option>, cx: &mut WindowContext, ) -> Task> { - 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>, - mut query: String, + queries: &[String], cancel: Arc, - use_active_tab_for_empty_query: bool, + strict_match: bool, cx: &mut WindowContext, ) -> Task, 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)) }) - }) - .collect::>(); - let string_matches = fuzzy::match_strings( - &match_candidates, - &query, - true, - usize::MAX, - &cancel, - background_executor, - ) - .await; + .fold(HashMap::default(), |mut candidates, (id, path_string)| { + candidates + .entry(path_string) + .or_insert_with(|| Vec::new()) + .push(id); + candidates + }); - Ok(string_matches - .into_iter() - .filter_map(|string_match| open_buffers.get(string_match.candidate_id)) - .cloned() - .collect()) + 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::>(); + 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 }) diff --git a/crates/assistant/src/slash_command/terminal_command.rs b/crates/assistant/src/slash_command/terminal_command.rs index 8eaad4068f..84e57bb93a 100644 --- a/crates/assistant/src/slash_command/terminal_command.rs +++ b/crates/assistant/src/slash_command/terminal_command.rs @@ -42,21 +42,26 @@ impl SlashCommand for TerminalSlashCommand { fn complete_argument( self: Arc, - _query: String, + arguments: &[String], _cancel: Arc, _workspace: Option>, _cx: &mut WindowContext, ) -> Task>> { - 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, - argument: Option<&str>, + arguments: &[String], workspace: WeakView, _delegate: Option>, 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::().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 { - let mut args = argument.split(' '); - if args.next() == Some(LINE_COUNT_ARG) { - if let Some(line_count) = args.next().and_then(|s| s.parse::().ok()) { - return Some(line_count); - } - } - None -} diff --git a/crates/assistant/src/slash_command/workflow_command.rs b/crates/assistant/src/slash_command/workflow_command.rs index 97596c6aee..f588fe848d 100644 --- a/crates/assistant/src/slash_command/workflow_command.rs +++ b/crates/assistant/src/slash_command/workflow_command.rs @@ -42,7 +42,7 @@ impl SlashCommand for WorkflowSlashCommand { fn complete_argument( self: Arc, - _query: String, + _arguments: &[String], _cancel: Arc, _workspace: Option>, _cx: &mut WindowContext, @@ -52,7 +52,7 @@ impl SlashCommand for WorkflowSlashCommand { fn run( self: Arc, - _argument: Option<&str>, + _arguments: &[String], _workspace: WeakView, _delegate: Option>, cx: &mut WindowContext, diff --git a/crates/assistant_slash_command/src/assistant_slash_command.rs b/crates/assistant_slash_command/src/assistant_slash_command.rs index f8fddb225d..b37e7a1c33 100644 --- a/crates/assistant_slash_command/src/assistant_slash_command.rs +++ b/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, - query: String, + arguments: &[String], cancel: Arc, workspace: Option>, cx: &mut WindowContext, @@ -42,7 +42,7 @@ pub trait SlashCommand: 'static + Send + Sync { fn requires_argument(&self) -> bool; fn run( self: Arc, - argument: Option<&str>, + arguments: &[String], workspace: WeakView, // TODO: We're just using the `LspAdapterDelegate` here because that is // what the extension API is already expecting. diff --git a/crates/extension/src/extension_slash_command.rs b/crates/extension/src/extension_slash_command.rs index d8a5acef29..fffc9307f8 100644 --- a/crates/extension/src/extension_slash_command.rs +++ b/crates/extension/src/extension_slash_command.rs @@ -39,11 +39,12 @@ impl SlashCommand for ExtensionSlashCommand { fn complete_argument( self: Arc, - query: String, + arguments: &[String], _cancel: Arc, _workspace: Option>, cx: &mut WindowContext, ) -> Task>> { + 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, - argument: Option<&str>, + arguments: &[String], _workspace: WeakView, delegate: Option>, cx: &mut WindowContext, ) -> Task> { - 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))?; diff --git a/crates/extension/src/wasm_host/wit.rs b/crates/extension/src/wasm_host/wit.rs index 721b6ea0ba..8b72d49a7d 100644 --- a/crates/extension/src/wasm_host/wit.rs +++ b/crates/extension/src/wasm_host/wit.rs @@ -262,11 +262,11 @@ impl Extension { &self, store: &mut Store, command: &SlashCommand, - query: &str, + arguments: &[String], ) -> Result, 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, command: &SlashCommand, - argument: Option<&str>, + arguments: &[String], resource: Option>>, ) -> Result> { 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(_) => { diff --git a/crates/extension_api/src/extension_api.rs b/crates/extension_api/src/extension_api.rs index ce0476cf00..fb85859c5c 100644 --- a/crates/extension_api/src/extension_api.rs +++ b/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, ) -> Result, String> { Ok(Vec::new()) } @@ -123,7 +123,7 @@ pub trait Extension: Send + Sync { fn run_slash_command( &self, _command: SlashCommand, - _argument: Option, + _args: Vec, _worktree: Option<&Worktree>, ) -> Result { 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, ) -> Result, String> { - extension().complete_slash_command_argument(command, query) + extension().complete_slash_command_argument(command, args) } fn run_slash_command( command: SlashCommand, - argument: Option, + args: Vec, worktree: Option<&Worktree>, ) -> Result { - extension().run_slash_command(command, argument, worktree) + extension().run_slash_command(command, args, worktree) } fn suggest_docs_packages(provider: String) -> Result, String> { diff --git a/crates/extension_api/wit/since_v0.1.0/extension.wit b/crates/extension_api/wit/since_v0.1.0/extension.wit index e42c2b7eb1..c7599f93ff 100644 --- a/crates/extension_api/wit/since_v0.1.0/extension.wit +++ b/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) -> result>, 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, string>; + export complete-slash-command-argument: func(command: slash-command, args: list) -> result, string>; /// Returns the output from running the provided slash command. - export run-slash-command: func(command: slash-command, argument: option, worktree: option>) -> result; + export run-slash-command: func(command: slash-command, args: list, worktree: option>) -> result; /// Returns a list of packages as suggestions to be included in the `/docs` /// search results. diff --git a/extensions/gleam/src/gleam.rs b/extensions/gleam/src/gleam.rs index b48ba13c5f..df04d9989f 100644 --- a/extensions/gleam/src/gleam.rs +++ b/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, ) -> Result, 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, + args: Vec, worktree: Option<&zed::Worktree>, ) -> Result { 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