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:
parent
88a12b60a9
commit
8fe2de1737
23 changed files with 332 additions and 263 deletions
|
@ -32,7 +32,7 @@ impl SlashCommand for DefaultSlashCommand {
|
|||
|
||||
fn complete_argument(
|
||||
self: Arc<Self>,
|
||||
_query: String,
|
||||
_arguments: &[String],
|
||||
_cancellation_flag: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut WindowContext,
|
||||
|
@ -42,7 +42,7 @@ impl SlashCommand for DefaultSlashCommand {
|
|||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
_argument: Option<&str>,
|
||||
_arguments: &[String],
|
||||
_workspace: WeakView<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
|
|
|
@ -105,7 +105,7 @@ impl SlashCommand for DiagnosticsSlashCommand {
|
|||
|
||||
fn complete_argument(
|
||||
self: Arc<Self>,
|
||||
query: String,
|
||||
arguments: &[String],
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut WindowContext,
|
||||
|
@ -113,7 +113,7 @@ impl SlashCommand for DiagnosticsSlashCommand {
|
|||
let Some(workspace) = workspace.and_then(|workspace| workspace.upgrade()) else {
|
||||
return Task::ready(Err(anyhow!("workspace was dropped")));
|
||||
};
|
||||
let query = query.split_whitespace().last().unwrap_or("").to_string();
|
||||
let query = arguments.last().cloned().unwrap_or_default();
|
||||
|
||||
let paths = self.search_paths(query.clone(), cancellation_flag.clone(), &workspace, cx);
|
||||
let executor = cx.background_executor().clone();
|
||||
|
@ -157,7 +157,7 @@ impl SlashCommand for DiagnosticsSlashCommand {
|
|||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
argument: Option<&str>,
|
||||
arguments: &[String],
|
||||
workspace: WeakView<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
|
@ -166,7 +166,7 @@ impl SlashCommand for DiagnosticsSlashCommand {
|
|||
return Task::ready(Err(anyhow!("workspace was dropped")));
|
||||
};
|
||||
|
||||
let options = Options::parse(argument);
|
||||
let options = Options::parse(arguments);
|
||||
|
||||
let task = collect_diagnostics(workspace.read(cx).project().clone(), options, cx);
|
||||
|
||||
|
@ -244,25 +244,20 @@ struct Options {
|
|||
const INCLUDE_WARNINGS_ARGUMENT: &str = "--include-warnings";
|
||||
|
||||
impl Options {
|
||||
fn parse(arguments_line: Option<&str>) -> Self {
|
||||
arguments_line
|
||||
.map(|arguments_line| {
|
||||
let args = arguments_line.split_whitespace().collect::<Vec<_>>();
|
||||
let mut include_warnings = false;
|
||||
let mut path_matcher = None;
|
||||
for arg in args {
|
||||
if arg == INCLUDE_WARNINGS_ARGUMENT {
|
||||
include_warnings = true;
|
||||
} else {
|
||||
path_matcher = PathMatcher::new(&[arg.to_owned()]).log_err();
|
||||
}
|
||||
}
|
||||
Self {
|
||||
include_warnings,
|
||||
path_matcher,
|
||||
}
|
||||
})
|
||||
.unwrap_or_default()
|
||||
fn parse(arguments: &[String]) -> Self {
|
||||
let mut include_warnings = false;
|
||||
let mut path_matcher = None;
|
||||
for arg in arguments {
|
||||
if arg == INCLUDE_WARNINGS_ARGUMENT {
|
||||
include_warnings = true;
|
||||
} else {
|
||||
path_matcher = PathMatcher::new(&[arg.to_owned()]).log_err();
|
||||
}
|
||||
}
|
||||
Self {
|
||||
include_warnings,
|
||||
path_matcher,
|
||||
}
|
||||
}
|
||||
|
||||
fn match_candidates_for_args() -> [StringMatchCandidate; 1] {
|
||||
|
|
|
@ -161,7 +161,7 @@ impl SlashCommand for DocsSlashCommand {
|
|||
|
||||
fn complete_argument(
|
||||
self: Arc<Self>,
|
||||
query: String,
|
||||
arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut WindowContext,
|
||||
|
@ -169,21 +169,18 @@ impl SlashCommand for DocsSlashCommand {
|
|||
self.ensure_rust_doc_providers_are_registered(workspace, cx);
|
||||
|
||||
let indexed_docs_registry = IndexedDocsRegistry::global(cx);
|
||||
let args = DocsSlashCommandArgs::parse(&query);
|
||||
let args = DocsSlashCommandArgs::parse(arguments);
|
||||
let store = args
|
||||
.provider()
|
||||
.ok_or_else(|| anyhow!("no docs provider specified"))
|
||||
.and_then(|provider| IndexedDocsStore::try_global(provider, cx));
|
||||
cx.background_executor().spawn(async move {
|
||||
fn build_completions(
|
||||
provider: ProviderId,
|
||||
items: Vec<String>,
|
||||
) -> Vec<ArgumentCompletion> {
|
||||
fn build_completions(items: Vec<String>) -> Vec<ArgumentCompletion> {
|
||||
items
|
||||
.into_iter()
|
||||
.map(|item| ArgumentCompletion {
|
||||
label: item.clone().into(),
|
||||
new_text: format!("{provider} {item}"),
|
||||
new_text: item.to_string(),
|
||||
run_command: true,
|
||||
})
|
||||
.collect()
|
||||
|
@ -225,7 +222,7 @@ impl SlashCommand for DocsSlashCommand {
|
|||
let suggested_packages = store.clone().suggest_packages().await?;
|
||||
let search_results = store.search(package).await;
|
||||
|
||||
let mut items = build_completions(provider.clone(), search_results);
|
||||
let mut items = build_completions(search_results);
|
||||
let workspace_crate_completions = suggested_packages
|
||||
.into_iter()
|
||||
.filter(|package_name| {
|
||||
|
@ -235,8 +232,8 @@ impl SlashCommand for DocsSlashCommand {
|
|||
})
|
||||
.map(|package_name| ArgumentCompletion {
|
||||
label: format!("{package_name} (unindexed)").into(),
|
||||
new_text: format!("{provider} {package_name}"),
|
||||
run_command: true,
|
||||
new_text: format!("{package_name}"),
|
||||
run_command: false,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
items.extend(workspace_crate_completions);
|
||||
|
@ -255,14 +252,10 @@ impl SlashCommand for DocsSlashCommand {
|
|||
|
||||
Ok(items)
|
||||
}
|
||||
DocsSlashCommandArgs::SearchItemDocs {
|
||||
provider,
|
||||
item_path,
|
||||
..
|
||||
} => {
|
||||
DocsSlashCommandArgs::SearchItemDocs { item_path, .. } => {
|
||||
let store = store?;
|
||||
let items = store.search(item_path).await;
|
||||
Ok(build_completions(provider, items))
|
||||
Ok(build_completions(items))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -270,16 +263,16 @@ impl SlashCommand for DocsSlashCommand {
|
|||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
argument: Option<&str>,
|
||||
arguments: &[String],
|
||||
_workspace: WeakView<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<SlashCommandOutput>> {
|
||||
let Some(argument) = argument else {
|
||||
return Task::ready(Err(anyhow!("missing argument")));
|
||||
if arguments.is_empty() {
|
||||
return Task::ready(Err(anyhow!("missing an argument")));
|
||||
};
|
||||
|
||||
let args = DocsSlashCommandArgs::parse(argument);
|
||||
let args = DocsSlashCommandArgs::parse(arguments);
|
||||
let executor = cx.background_executor().clone();
|
||||
let task = cx.background_executor().spawn({
|
||||
let store = args
|
||||
|
@ -379,12 +372,18 @@ pub(crate) enum DocsSlashCommandArgs {
|
|||
}
|
||||
|
||||
impl DocsSlashCommandArgs {
|
||||
pub fn parse(argument: &str) -> Self {
|
||||
let Some((provider, argument)) = argument.split_once(' ') else {
|
||||
pub fn parse(arguments: &[String]) -> Self {
|
||||
let Some(provider) = arguments
|
||||
.get(0)
|
||||
.cloned()
|
||||
.filter(|arg| !arg.trim().is_empty())
|
||||
else {
|
||||
return Self::NoProvider;
|
||||
};
|
||||
|
||||
let provider = ProviderId(provider.into());
|
||||
let Some(argument) = arguments.get(1) else {
|
||||
return Self::NoProvider;
|
||||
};
|
||||
|
||||
if let Some((package, rest)) = argument.split_once(is_item_path_delimiter) {
|
||||
if rest.trim().is_empty() {
|
||||
|
@ -444,16 +443,16 @@ mod tests {
|
|||
#[test]
|
||||
fn test_parse_docs_slash_command_args() {
|
||||
assert_eq!(
|
||||
DocsSlashCommandArgs::parse(""),
|
||||
DocsSlashCommandArgs::parse(&["".to_string()]),
|
||||
DocsSlashCommandArgs::NoProvider
|
||||
);
|
||||
assert_eq!(
|
||||
DocsSlashCommandArgs::parse("rustdoc"),
|
||||
DocsSlashCommandArgs::parse(&["rustdoc".to_string()]),
|
||||
DocsSlashCommandArgs::NoProvider
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
DocsSlashCommandArgs::parse("rustdoc "),
|
||||
DocsSlashCommandArgs::parse(&["rustdoc".to_string(), "".to_string()]),
|
||||
DocsSlashCommandArgs::SearchPackageDocs {
|
||||
provider: ProviderId("rustdoc".into()),
|
||||
package: "".into(),
|
||||
|
@ -461,7 +460,7 @@ mod tests {
|
|||
}
|
||||
);
|
||||
assert_eq!(
|
||||
DocsSlashCommandArgs::parse("gleam "),
|
||||
DocsSlashCommandArgs::parse(&["gleam".to_string(), "".to_string()]),
|
||||
DocsSlashCommandArgs::SearchPackageDocs {
|
||||
provider: ProviderId("gleam".into()),
|
||||
package: "".into(),
|
||||
|
@ -470,7 +469,7 @@ mod tests {
|
|||
);
|
||||
|
||||
assert_eq!(
|
||||
DocsSlashCommandArgs::parse("rustdoc gpui"),
|
||||
DocsSlashCommandArgs::parse(&["rustdoc".to_string(), "gpui".to_string()]),
|
||||
DocsSlashCommandArgs::SearchPackageDocs {
|
||||
provider: ProviderId("rustdoc".into()),
|
||||
package: "gpui".into(),
|
||||
|
@ -478,7 +477,7 @@ mod tests {
|
|||
}
|
||||
);
|
||||
assert_eq!(
|
||||
DocsSlashCommandArgs::parse("gleam gleam_stdlib"),
|
||||
DocsSlashCommandArgs::parse(&["gleam".to_string(), "gleam_stdlib".to_string()]),
|
||||
DocsSlashCommandArgs::SearchPackageDocs {
|
||||
provider: ProviderId("gleam".into()),
|
||||
package: "gleam_stdlib".into(),
|
||||
|
@ -488,7 +487,7 @@ mod tests {
|
|||
|
||||
// Adding an item path delimiter indicates we can start indexing.
|
||||
assert_eq!(
|
||||
DocsSlashCommandArgs::parse("rustdoc gpui:"),
|
||||
DocsSlashCommandArgs::parse(&["rustdoc".to_string(), "gpui:".to_string()]),
|
||||
DocsSlashCommandArgs::SearchPackageDocs {
|
||||
provider: ProviderId("rustdoc".into()),
|
||||
package: "gpui".into(),
|
||||
|
@ -496,7 +495,7 @@ mod tests {
|
|||
}
|
||||
);
|
||||
assert_eq!(
|
||||
DocsSlashCommandArgs::parse("gleam gleam_stdlib/"),
|
||||
DocsSlashCommandArgs::parse(&["gleam".to_string(), "gleam_stdlib/".to_string()]),
|
||||
DocsSlashCommandArgs::SearchPackageDocs {
|
||||
provider: ProviderId("gleam".into()),
|
||||
package: "gleam_stdlib".into(),
|
||||
|
@ -505,7 +504,10 @@ mod tests {
|
|||
);
|
||||
|
||||
assert_eq!(
|
||||
DocsSlashCommandArgs::parse("rustdoc gpui::foo::bar::Baz"),
|
||||
DocsSlashCommandArgs::parse(&[
|
||||
"rustdoc".to_string(),
|
||||
"gpui::foo::bar::Baz".to_string()
|
||||
]),
|
||||
DocsSlashCommandArgs::SearchItemDocs {
|
||||
provider: ProviderId("rustdoc".into()),
|
||||
package: "gpui".into(),
|
||||
|
@ -513,7 +515,10 @@ mod tests {
|
|||
}
|
||||
);
|
||||
assert_eq!(
|
||||
DocsSlashCommandArgs::parse("gleam gleam_stdlib/gleam/int"),
|
||||
DocsSlashCommandArgs::parse(&[
|
||||
"gleam".to_string(),
|
||||
"gleam_stdlib/gleam/int".to_string()
|
||||
]),
|
||||
DocsSlashCommandArgs::SearchItemDocs {
|
||||
provider: ProviderId("gleam".into()),
|
||||
package: "gleam_stdlib".into(),
|
||||
|
|
|
@ -117,7 +117,7 @@ impl SlashCommand for FetchSlashCommand {
|
|||
|
||||
fn complete_argument(
|
||||
self: Arc<Self>,
|
||||
_query: String,
|
||||
_arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut WindowContext,
|
||||
|
@ -127,12 +127,12 @@ impl SlashCommand for FetchSlashCommand {
|
|||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
argument: Option<&str>,
|
||||
arguments: &[String],
|
||||
workspace: WeakView<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<SlashCommandOutput>> {
|
||||
let Some(argument) = argument else {
|
||||
let Some(argument) = arguments.first() else {
|
||||
return Task::ready(Err(anyhow!("missing URL")));
|
||||
};
|
||||
let Some(workspace) = workspace.upgrade() else {
|
||||
|
|
|
@ -122,7 +122,7 @@ impl SlashCommand for FileSlashCommand {
|
|||
|
||||
fn complete_argument(
|
||||
self: Arc<Self>,
|
||||
query: String,
|
||||
arguments: &[String],
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut WindowContext,
|
||||
|
@ -131,7 +131,12 @@ impl SlashCommand for FileSlashCommand {
|
|||
return Task::ready(Err(anyhow!("workspace was dropped")));
|
||||
};
|
||||
|
||||
let paths = self.search_paths(query, cancellation_flag, &workspace, cx);
|
||||
let paths = self.search_paths(
|
||||
arguments.last().cloned().unwrap_or_default(),
|
||||
cancellation_flag,
|
||||
&workspace,
|
||||
cx,
|
||||
);
|
||||
let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
|
||||
cx.background_executor().spawn(async move {
|
||||
Ok(paths
|
||||
|
@ -168,7 +173,7 @@ impl SlashCommand for FileSlashCommand {
|
|||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
argument: Option<&str>,
|
||||
arguments: &[String],
|
||||
workspace: WeakView<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
|
@ -177,7 +182,7 @@ impl SlashCommand for FileSlashCommand {
|
|||
return Task::ready(Err(anyhow!("workspace was dropped")));
|
||||
};
|
||||
|
||||
let Some(argument) = argument else {
|
||||
let Some(argument) = arguments.first() else {
|
||||
return Task::ready(Err(anyhow!("missing path")));
|
||||
};
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ impl SlashCommand for NowSlashCommand {
|
|||
|
||||
fn complete_argument(
|
||||
self: Arc<Self>,
|
||||
_query: String,
|
||||
_arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut WindowContext,
|
||||
|
@ -42,7 +42,7 @@ impl SlashCommand for NowSlashCommand {
|
|||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
_argument: Option<&str>,
|
||||
_arguments: &[String],
|
||||
_workspace: WeakView<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
_cx: &mut WindowContext,
|
||||
|
|
|
@ -103,7 +103,7 @@ impl SlashCommand for ProjectSlashCommand {
|
|||
|
||||
fn complete_argument(
|
||||
self: Arc<Self>,
|
||||
_query: String,
|
||||
_arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut WindowContext,
|
||||
|
@ -117,7 +117,7 @@ impl SlashCommand for ProjectSlashCommand {
|
|||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
_argument: Option<&str>,
|
||||
_arguments: &[String],
|
||||
workspace: WeakView<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
|
|
|
@ -29,12 +29,13 @@ impl SlashCommand for PromptSlashCommand {
|
|||
|
||||
fn complete_argument(
|
||||
self: Arc<Self>,
|
||||
query: String,
|
||||
arguments: &[String],
|
||||
_cancellation_flag: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
let store = PromptStore::global(cx);
|
||||
let query = arguments.last().cloned().unwrap_or_default();
|
||||
cx.background_executor().spawn(async move {
|
||||
let prompts = store.await?.search(query).await;
|
||||
Ok(prompts
|
||||
|
@ -53,12 +54,12 @@ impl SlashCommand for PromptSlashCommand {
|
|||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
title: Option<&str>,
|
||||
arguments: &[String],
|
||||
_workspace: WeakView<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<SlashCommandOutput>> {
|
||||
let Some(title) = title else {
|
||||
let Some(title) = arguments.first() else {
|
||||
return Task::ready(Err(anyhow!("missing prompt name")));
|
||||
};
|
||||
|
||||
|
|
|
@ -49,7 +49,7 @@ impl SlashCommand for SearchSlashCommand {
|
|||
|
||||
fn complete_argument(
|
||||
self: Arc<Self>,
|
||||
_query: String,
|
||||
_arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut WindowContext,
|
||||
|
@ -59,7 +59,7 @@ impl SlashCommand for SearchSlashCommand {
|
|||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
argument: Option<&str>,
|
||||
arguments: &[String],
|
||||
workspace: WeakView<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
|
@ -67,13 +67,13 @@ impl SlashCommand for SearchSlashCommand {
|
|||
let Some(workspace) = workspace.upgrade() else {
|
||||
return Task::ready(Err(anyhow::anyhow!("workspace was dropped")));
|
||||
};
|
||||
let Some(argument) = argument else {
|
||||
if arguments.is_empty() {
|
||||
return Task::ready(Err(anyhow::anyhow!("missing search query")));
|
||||
};
|
||||
|
||||
let mut limit = None;
|
||||
let mut query = String::new();
|
||||
for part in argument.split(' ') {
|
||||
for part in arguments {
|
||||
if let Some(parameter) = part.strip_prefix("--") {
|
||||
if let Ok(count) = parameter.parse::<usize>() {
|
||||
limit = Some(count);
|
||||
|
|
|
@ -26,7 +26,7 @@ impl SlashCommand for OutlineSlashCommand {
|
|||
|
||||
fn complete_argument(
|
||||
self: Arc<Self>,
|
||||
_query: String,
|
||||
_arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut WindowContext,
|
||||
|
@ -40,7 +40,7 @@ impl SlashCommand for OutlineSlashCommand {
|
|||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
_argument: Option<&str>,
|
||||
_arguments: &[String],
|
||||
workspace: WeakView<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
|
|
|
@ -5,8 +5,9 @@ use super::{
|
|||
};
|
||||
use anyhow::{Context, Result};
|
||||
use assistant_slash_command::ArgumentCompletion;
|
||||
use collections::HashMap;
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::Editor;
|
||||
use futures::future::join_all;
|
||||
use gpui::{Entity, Task, WeakView};
|
||||
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||
use std::{
|
||||
|
@ -17,13 +18,13 @@ use std::{
|
|||
use ui::WindowContext;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub(crate) struct TabsSlashCommand;
|
||||
pub(crate) struct TabSlashCommand;
|
||||
|
||||
const ALL_TABS_COMPLETION_ITEM: &str = "all";
|
||||
|
||||
impl SlashCommand for TabsSlashCommand {
|
||||
impl SlashCommand for TabSlashCommand {
|
||||
fn name(&self) -> String {
|
||||
"tabs".into()
|
||||
"tab".into()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
|
@ -40,51 +41,66 @@ impl SlashCommand for TabsSlashCommand {
|
|||
|
||||
fn complete_argument(
|
||||
self: Arc<Self>,
|
||||
query: String,
|
||||
arguments: &[String],
|
||||
cancel: Arc<AtomicBool>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
let all_tabs_completion_item = if ALL_TABS_COMPLETION_ITEM.contains(&query) {
|
||||
Some(ArgumentCompletion {
|
||||
let mut has_all_tabs_completion_item = false;
|
||||
let argument_set = arguments
|
||||
.iter()
|
||||
.filter(|argument| {
|
||||
if has_all_tabs_completion_item || ALL_TABS_COMPLETION_ITEM == argument.as_str() {
|
||||
has_all_tabs_completion_item = true;
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
})
|
||||
.cloned()
|
||||
.collect::<HashSet<_>>();
|
||||
if has_all_tabs_completion_item {
|
||||
return Task::ready(Ok(Vec::new()));
|
||||
}
|
||||
let current_query = arguments.last().cloned().unwrap_or_default();
|
||||
let tab_items_search =
|
||||
tab_items_for_queries(workspace, &[current_query], cancel, false, cx);
|
||||
cx.spawn(|_| async move {
|
||||
let tab_items = tab_items_search.await?;
|
||||
let run_command = tab_items.len() == 1;
|
||||
let tab_completion_items = tab_items.into_iter().filter_map(|(path, ..)| {
|
||||
let path_string = path.as_deref()?.to_string_lossy().to_string();
|
||||
if argument_set.contains(&path_string) {
|
||||
return None;
|
||||
}
|
||||
Some(ArgumentCompletion {
|
||||
label: path_string.clone().into(),
|
||||
new_text: path_string,
|
||||
run_command,
|
||||
})
|
||||
});
|
||||
|
||||
Ok(Some(ArgumentCompletion {
|
||||
label: ALL_TABS_COMPLETION_ITEM.into(),
|
||||
new_text: ALL_TABS_COMPLETION_ITEM.to_owned(),
|
||||
run_command: true,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let tab_items_search = tab_items_for_query(workspace, query, cancel, false, cx);
|
||||
cx.spawn(|_| async move {
|
||||
let tab_completion_items =
|
||||
tab_items_search
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter_map(|(path, ..)| {
|
||||
let path_string = path.as_deref()?.to_string_lossy().to_string();
|
||||
Some(ArgumentCompletion {
|
||||
label: path_string.clone().into(),
|
||||
new_text: path_string,
|
||||
run_command: true,
|
||||
})
|
||||
});
|
||||
Ok(all_tabs_completion_item
|
||||
.into_iter()
|
||||
.chain(tab_completion_items)
|
||||
.collect::<Vec<_>>())
|
||||
.into_iter()
|
||||
.chain(tab_completion_items)
|
||||
.collect::<Vec<_>>())
|
||||
})
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
argument: Option<&str>,
|
||||
arguments: &[String],
|
||||
workspace: WeakView<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<SlashCommandOutput>> {
|
||||
let tab_items_search = tab_items_for_query(
|
||||
let tab_items_search = tab_items_for_queries(
|
||||
Some(workspace),
|
||||
argument.map(ToOwned::to_owned).unwrap_or_default(),
|
||||
arguments,
|
||||
Arc::new(AtomicBool::new(false)),
|
||||
true,
|
||||
cx,
|
||||
|
@ -129,20 +145,21 @@ impl SlashCommand for TabsSlashCommand {
|
|||
}
|
||||
}
|
||||
|
||||
fn tab_items_for_query(
|
||||
fn tab_items_for_queries(
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
mut query: String,
|
||||
queries: &[String],
|
||||
cancel: Arc<AtomicBool>,
|
||||
use_active_tab_for_empty_query: bool,
|
||||
strict_match: bool,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<anyhow::Result<Vec<(Option<PathBuf>, BufferSnapshot, usize)>>> {
|
||||
let empty_query = queries.is_empty() || queries.iter().all(|query| query.trim().is_empty());
|
||||
let queries = queries.to_owned();
|
||||
cx.spawn(|mut cx| async move {
|
||||
query.make_ascii_lowercase();
|
||||
let mut open_buffers =
|
||||
workspace
|
||||
.context("no workspace")?
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
if use_active_tab_for_empty_query && query.trim().is_empty() {
|
||||
if strict_match && empty_query {
|
||||
let active_editor = workspace
|
||||
.active_item(cx)
|
||||
.context("no active item")?
|
||||
|
@ -189,38 +206,73 @@ fn tab_items_for_query(
|
|||
cx.background_executor()
|
||||
.spawn(async move {
|
||||
open_buffers.sort_by_key(|(_, _, timestamp)| *timestamp);
|
||||
let query = query.trim();
|
||||
if query.is_empty() || query == ALL_TABS_COMPLETION_ITEM {
|
||||
if empty_query
|
||||
|| queries
|
||||
.iter()
|
||||
.any(|query| query == ALL_TABS_COMPLETION_ITEM)
|
||||
{
|
||||
return Ok(open_buffers);
|
||||
}
|
||||
|
||||
let match_candidates = open_buffers
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(id, (full_path, ..))| {
|
||||
let path_string = full_path.as_deref()?.to_string_lossy().to_string();
|
||||
Some(fuzzy::StringMatchCandidate {
|
||||
id,
|
||||
char_bag: path_string.as_str().into(),
|
||||
string: path_string,
|
||||
let matched_items = if strict_match {
|
||||
let match_candidates = open_buffers
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(id, (full_path, ..))| {
|
||||
let path_string = full_path.as_deref()?.to_string_lossy().to_string();
|
||||
Some((id, path_string))
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
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::<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
|
||||
})
|
|
@ -42,21 +42,26 @@ impl SlashCommand for TerminalSlashCommand {
|
|||
|
||||
fn complete_argument(
|
||||
self: Arc<Self>,
|
||||
_query: String,
|
||||
arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut WindowContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Ok(vec![ArgumentCompletion {
|
||||
label: LINE_COUNT_ARG.into(),
|
||||
new_text: LINE_COUNT_ARG.to_string(),
|
||||
run_command: true,
|
||||
}]))
|
||||
let completions = if arguments.iter().any(|arg| arg == LINE_COUNT_ARG) {
|
||||
Vec::new()
|
||||
} else {
|
||||
vec![ArgumentCompletion {
|
||||
label: LINE_COUNT_ARG.into(),
|
||||
new_text: LINE_COUNT_ARG.to_string(),
|
||||
run_command: false,
|
||||
}]
|
||||
};
|
||||
Task::ready(Ok(completions))
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
argument: Option<&str>,
|
||||
arguments: &[String],
|
||||
workspace: WeakView<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
|
@ -75,9 +80,13 @@ impl SlashCommand for TerminalSlashCommand {
|
|||
return Task::ready(Err(anyhow::anyhow!("no active terminal")));
|
||||
};
|
||||
|
||||
let line_count = argument
|
||||
.and_then(|a| parse_argument(a))
|
||||
.unwrap_or(DEFAULT_CONTEXT_LINES);
|
||||
let mut line_count = DEFAULT_CONTEXT_LINES;
|
||||
if arguments.get(0).map(|s| s.as_str()) == Some(LINE_COUNT_ARG) {
|
||||
if let Some(parsed_line_count) = arguments.get(1).and_then(|s| s.parse::<usize>().ok())
|
||||
{
|
||||
line_count = parsed_line_count;
|
||||
}
|
||||
}
|
||||
|
||||
let lines = active_terminal
|
||||
.read(cx)
|
||||
|
@ -101,13 +110,3 @@ impl SlashCommand for TerminalSlashCommand {
|
|||
}))
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_argument(argument: &str) -> Option<usize> {
|
||||
let mut args = argument.split(' ');
|
||||
if args.next() == Some(LINE_COUNT_ARG) {
|
||||
if let Some(line_count) = args.next().and_then(|s| s.parse::<usize>().ok()) {
|
||||
return Some(line_count);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@ impl SlashCommand for WorkflowSlashCommand {
|
|||
|
||||
fn complete_argument(
|
||||
self: Arc<Self>,
|
||||
_query: String,
|
||||
_arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut WindowContext,
|
||||
|
@ -52,7 +52,7 @@ impl SlashCommand for WorkflowSlashCommand {
|
|||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
_argument: Option<&str>,
|
||||
_arguments: &[String],
|
||||
_workspace: WeakView<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue