Improve slash commands (#16195)
This PR: - Makes slash commands easier to compose by adding a concept, `CompletionIntent`. When using `tab` on a completion in the assistant panel, that completion item will be expanded but the associated command will not be run. Using `enter` will still either run the completion item or continue command composition as before. - Fixes a bug where running `/diagnostics` on a project with no diagnostics will delete the entire command, rather than rendering an empty header. - Improves the autocomplete rendering for files, showing when directories are selected and re-arranging the results to have the file name or trailing directory show first. <img width="642" alt="Screenshot 2024-08-13 at 8 12 43 PM" src="https://github.com/user-attachments/assets/97c96cd2-741f-4f15-ad03-7cf78129a71c"> Release Notes: - N/A
This commit is contained in:
parent
5cb4de4ec6
commit
97469cd049
23 changed files with 326 additions and 190 deletions
|
@ -43,6 +43,7 @@ impl DiagnosticsSlashCommand {
|
|||
worktree_id: entry.worktree_id.to_usize(),
|
||||
path: entry.path.clone(),
|
||||
path_prefix: path_prefix.clone(),
|
||||
is_dir: false, // Diagnostics can't be produced for directories
|
||||
distance_to_relative_ancestor: 0,
|
||||
})
|
||||
.collect(),
|
||||
|
@ -146,7 +147,7 @@ impl SlashCommand for DiagnosticsSlashCommand {
|
|||
Ok(matches
|
||||
.into_iter()
|
||||
.map(|completion| ArgumentCompletion {
|
||||
label: completion.clone(),
|
||||
label: completion.clone().into(),
|
||||
new_text: completion,
|
||||
run_command: true,
|
||||
})
|
||||
|
@ -168,58 +169,66 @@ impl SlashCommand for DiagnosticsSlashCommand {
|
|||
let options = Options::parse(argument);
|
||||
|
||||
let task = collect_diagnostics(workspace.read(cx).project().clone(), options, cx);
|
||||
|
||||
cx.spawn(move |_| async move {
|
||||
let Some((text, sections)) = task.await? else {
|
||||
return Ok(SlashCommandOutput::default());
|
||||
return Ok(SlashCommandOutput {
|
||||
sections: vec![SlashCommandOutputSection {
|
||||
range: 0..1,
|
||||
icon: IconName::Library,
|
||||
label: "No Diagnostics".into(),
|
||||
}],
|
||||
text: "\n".to_string(),
|
||||
run_commands_in_text: true,
|
||||
});
|
||||
};
|
||||
|
||||
let sections = sections
|
||||
.into_iter()
|
||||
.map(|(range, placeholder_type)| SlashCommandOutputSection {
|
||||
range,
|
||||
icon: match placeholder_type {
|
||||
PlaceholderType::Root(_, _) => IconName::ExclamationTriangle,
|
||||
PlaceholderType::File(_) => IconName::File,
|
||||
PlaceholderType::Diagnostic(DiagnosticType::Error, _) => IconName::XCircle,
|
||||
PlaceholderType::Diagnostic(DiagnosticType::Warning, _) => {
|
||||
IconName::ExclamationTriangle
|
||||
}
|
||||
},
|
||||
label: match placeholder_type {
|
||||
PlaceholderType::Root(summary, source) => {
|
||||
let mut label = String::new();
|
||||
label.push_str("Diagnostics");
|
||||
if let Some(source) = source {
|
||||
write!(label, " ({})", source).unwrap();
|
||||
}
|
||||
|
||||
if summary.error_count > 0 || summary.warning_count > 0 {
|
||||
label.push(':');
|
||||
|
||||
if summary.error_count > 0 {
|
||||
write!(label, " {} errors", summary.error_count).unwrap();
|
||||
if summary.warning_count > 0 {
|
||||
label.push_str(",");
|
||||
}
|
||||
}
|
||||
|
||||
if summary.warning_count > 0 {
|
||||
write!(label, " {} warnings", summary.warning_count).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
label.into()
|
||||
}
|
||||
PlaceholderType::File(file_path) => file_path.into(),
|
||||
PlaceholderType::Diagnostic(_, message) => message.into(),
|
||||
},
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(SlashCommandOutput {
|
||||
text,
|
||||
sections: sections
|
||||
.into_iter()
|
||||
.map(|(range, placeholder_type)| SlashCommandOutputSection {
|
||||
range,
|
||||
icon: match placeholder_type {
|
||||
PlaceholderType::Root(_, _) => IconName::ExclamationTriangle,
|
||||
PlaceholderType::File(_) => IconName::File,
|
||||
PlaceholderType::Diagnostic(DiagnosticType::Error, _) => {
|
||||
IconName::XCircle
|
||||
}
|
||||
PlaceholderType::Diagnostic(DiagnosticType::Warning, _) => {
|
||||
IconName::ExclamationTriangle
|
||||
}
|
||||
},
|
||||
label: match placeholder_type {
|
||||
PlaceholderType::Root(summary, source) => {
|
||||
let mut label = String::new();
|
||||
label.push_str("Diagnostics");
|
||||
if let Some(source) = source {
|
||||
write!(label, " ({})", source).unwrap();
|
||||
}
|
||||
|
||||
if summary.error_count > 0 || summary.warning_count > 0 {
|
||||
label.push(':');
|
||||
|
||||
if summary.error_count > 0 {
|
||||
write!(label, " {} errors", summary.error_count).unwrap();
|
||||
if summary.warning_count > 0 {
|
||||
label.push_str(",");
|
||||
}
|
||||
}
|
||||
|
||||
if summary.warning_count > 0 {
|
||||
write!(label, " {} warnings", summary.warning_count)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
label.into()
|
||||
}
|
||||
PlaceholderType::File(file_path) => file_path.into(),
|
||||
PlaceholderType::Diagnostic(_, message) => message.into(),
|
||||
},
|
||||
})
|
||||
.collect(),
|
||||
sections,
|
||||
run_commands_in_text: false,
|
||||
})
|
||||
})
|
||||
|
|
|
@ -182,7 +182,7 @@ impl SlashCommand for DocsSlashCommand {
|
|||
items
|
||||
.into_iter()
|
||||
.map(|item| ArgumentCompletion {
|
||||
label: item.clone(),
|
||||
label: item.clone().into(),
|
||||
new_text: format!("{provider} {item}"),
|
||||
run_command: true,
|
||||
})
|
||||
|
@ -194,7 +194,7 @@ impl SlashCommand for DocsSlashCommand {
|
|||
let providers = indexed_docs_registry.list_providers();
|
||||
if providers.is_empty() {
|
||||
return Ok(vec![ArgumentCompletion {
|
||||
label: "No available docs providers.".to_string(),
|
||||
label: "No available docs providers.".into(),
|
||||
new_text: String::new(),
|
||||
run_command: false,
|
||||
}]);
|
||||
|
@ -203,7 +203,7 @@ impl SlashCommand for DocsSlashCommand {
|
|||
Ok(providers
|
||||
.into_iter()
|
||||
.map(|provider| ArgumentCompletion {
|
||||
label: provider.to_string(),
|
||||
label: provider.to_string().into(),
|
||||
new_text: provider.to_string(),
|
||||
run_command: false,
|
||||
})
|
||||
|
@ -231,10 +231,10 @@ impl SlashCommand for DocsSlashCommand {
|
|||
.filter(|package_name| {
|
||||
!items
|
||||
.iter()
|
||||
.any(|item| item.label.as_str() == package_name.as_ref())
|
||||
.any(|item| item.label.text() == package_name.as_ref())
|
||||
})
|
||||
.map(|package_name| ArgumentCompletion {
|
||||
label: format!("{package_name} (unindexed)"),
|
||||
label: format!("{package_name} (unindexed)").into(),
|
||||
new_text: format!("{provider} {package_name}"),
|
||||
run_command: true,
|
||||
})
|
||||
|
@ -246,7 +246,8 @@ impl SlashCommand for DocsSlashCommand {
|
|||
label: format!(
|
||||
"Enter a {package_term} name.",
|
||||
package_term = package_term(&provider)
|
||||
),
|
||||
)
|
||||
.into(),
|
||||
new_text: provider.to_string(),
|
||||
run_command: false,
|
||||
}]);
|
||||
|
|
|
@ -3,7 +3,7 @@ use anyhow::{anyhow, Result};
|
|||
use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
|
||||
use fuzzy::PathMatch;
|
||||
use gpui::{AppContext, Model, Task, View, WeakView};
|
||||
use language::{BufferSnapshot, LineEnding, LspAdapterDelegate};
|
||||
use language::{BufferSnapshot, CodeLabel, HighlightId, LineEnding, LspAdapterDelegate};
|
||||
use project::{PathMatchCandidateSet, Project};
|
||||
use std::{
|
||||
fmt::Write,
|
||||
|
@ -29,11 +29,30 @@ impl FileSlashCommand {
|
|||
let workspace = workspace.read(cx);
|
||||
let project = workspace.project().read(cx);
|
||||
let entries = workspace.recent_navigation_history(Some(10), cx);
|
||||
|
||||
let entries = entries
|
||||
.into_iter()
|
||||
.map(|entries| (entries.0, false))
|
||||
.chain(project.worktrees(cx).flat_map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
let id = worktree.id();
|
||||
worktree.child_entries(Path::new("")).map(move |entry| {
|
||||
(
|
||||
project::ProjectPath {
|
||||
worktree_id: id,
|
||||
path: entry.path.clone(),
|
||||
},
|
||||
entry.kind.is_dir(),
|
||||
)
|
||||
})
|
||||
}))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let path_prefix: Arc<str> = Arc::default();
|
||||
Task::ready(
|
||||
entries
|
||||
.into_iter()
|
||||
.filter_map(|(entry, _)| {
|
||||
.filter_map(|(entry, is_dir)| {
|
||||
let worktree = project.worktree_for_id(entry.worktree_id, cx)?;
|
||||
let mut full_path = PathBuf::from(worktree.read(cx).root_name());
|
||||
full_path.push(&entry.path);
|
||||
|
@ -44,6 +63,7 @@ impl FileSlashCommand {
|
|||
path: full_path.into(),
|
||||
path_prefix: path_prefix.clone(),
|
||||
distance_to_relative_ancestor: 0,
|
||||
is_dir,
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
|
@ -54,6 +74,7 @@ impl FileSlashCommand {
|
|||
.into_iter()
|
||||
.map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
|
||||
PathMatchCandidateSet {
|
||||
snapshot: worktree.snapshot(),
|
||||
include_ignored: worktree
|
||||
|
@ -111,22 +132,35 @@ impl SlashCommand for FileSlashCommand {
|
|||
};
|
||||
|
||||
let paths = self.search_paths(query, cancellation_flag, &workspace, cx);
|
||||
let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
|
||||
cx.background_executor().spawn(async move {
|
||||
Ok(paths
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|path_match| {
|
||||
.filter_map(|path_match| {
|
||||
let text = format!(
|
||||
"{}{}",
|
||||
path_match.path_prefix,
|
||||
path_match.path.to_string_lossy()
|
||||
);
|
||||
|
||||
ArgumentCompletion {
|
||||
label: text.clone(),
|
||||
let mut label = CodeLabel::default();
|
||||
let file_name = path_match.path.file_name()?.to_string_lossy();
|
||||
let label_text = if path_match.is_dir {
|
||||
format!("{}/ ", file_name)
|
||||
} else {
|
||||
format!("{} ", file_name)
|
||||
};
|
||||
|
||||
label.push_str(label_text.as_str(), None);
|
||||
label.push_str(&text, comment_id);
|
||||
label.filter_range = 0..file_name.len();
|
||||
|
||||
Some(ArgumentCompletion {
|
||||
label,
|
||||
new_text: text,
|
||||
run_command: true,
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect())
|
||||
})
|
||||
|
|
|
@ -42,7 +42,7 @@ impl SlashCommand for PromptSlashCommand {
|
|||
.filter_map(|prompt| {
|
||||
let prompt_title = prompt.title?.to_string();
|
||||
Some(ArgumentCompletion {
|
||||
label: prompt_title.clone(),
|
||||
label: prompt_title.clone().into(),
|
||||
new_text: prompt_title,
|
||||
run_command: true,
|
||||
})
|
||||
|
|
|
@ -47,7 +47,7 @@ impl SlashCommand for TabsSlashCommand {
|
|||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
let all_tabs_completion_item = if ALL_TABS_COMPLETION_ITEM.contains(&query) {
|
||||
Some(ArgumentCompletion {
|
||||
label: ALL_TABS_COMPLETION_ITEM.to_owned(),
|
||||
label: ALL_TABS_COMPLETION_ITEM.into(),
|
||||
new_text: ALL_TABS_COMPLETION_ITEM.to_owned(),
|
||||
run_command: true,
|
||||
})
|
||||
|
@ -63,7 +63,7 @@ impl SlashCommand for TabsSlashCommand {
|
|||
.filter_map(|(path, ..)| {
|
||||
let path_string = path.as_deref()?.to_string_lossy().to_string();
|
||||
Some(ArgumentCompletion {
|
||||
label: path_string.clone(),
|
||||
label: path_string.clone().into(),
|
||||
new_text: path_string,
|
||||
run_command: true,
|
||||
})
|
||||
|
|
|
@ -48,7 +48,7 @@ impl SlashCommand for TerminalSlashCommand {
|
|||
_cx: &mut WindowContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Ok(vec![ArgumentCompletion {
|
||||
label: LINE_COUNT_ARG.to_string(),
|
||||
label: LINE_COUNT_ARG.into(),
|
||||
new_text: LINE_COUNT_ARG.to_string(),
|
||||
run_command: true,
|
||||
}]))
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue