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

* 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>
This commit is contained in:
Kirill Bulatov 2024-08-14 17:11:51 +03:00 committed by GitHub
parent 88a12b60a9
commit 8fe2de1737
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 332 additions and 263 deletions

View file

@ -37,7 +37,7 @@ use serde::{Deserialize, Serialize};
use settings::{update_settings_file, Settings, SettingsStore}; use settings::{update_settings_file, Settings, SettingsStore};
use slash_command::{ use slash_command::{
default_command, diagnostics_command, docs_command, fetch_command, file_command, now_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, terminal_command, workflow_command,
}; };
use std::sync::Arc; 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); let slash_command_registry = SlashCommandRegistry::global(cx);
slash_command_registry.register_command(file_command::FileSlashCommand, true); slash_command_registry.register_command(file_command::FileSlashCommand, true);
slash_command_registry.register_command(symbols_command::OutlineSlashCommand, 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(project_command::ProjectSlashCommand, true);
slash_command_registry.register_command(prompt_command::PromptSlashCommand, true); slash_command_registry.register_command(prompt_command::PromptSlashCommand, true);
slash_command_registry.register_command(default_command::DefaultSlashCommand, false); slash_command_registry.register_command(default_command::DefaultSlashCommand, false);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -130,10 +130,10 @@ world extension {
export labels-for-symbols: func(language-server-id: string, symbols: list<symbol>) -> result<list<option<code-label>>, string>; 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. /// 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. /// 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` /// Returns a list of packages as suggestions to be included in the `/docs`
/// search results. /// search results.

View file

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