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:
Mikayla Maki 2024-08-13 23:06:07 -07:00 committed by GitHub
parent 5cb4de4ec6
commit 97469cd049
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 326 additions and 190 deletions

View file

@ -114,10 +114,9 @@ use task::{
};
use terminals::Terminals;
use text::{Anchor, BufferId, LineEnding};
use unicase::UniCase;
use util::{
debug_panic, defer, maybe, merge_json_value_into, parse_env_output, post_inc,
NumericPrefixWithSuffix, ResultExt, TryFutureExt as _,
debug_panic, defer, maybe, merge_json_value_into, parse_env_output, paths::compare_paths,
post_inc, ResultExt, TryFutureExt as _,
};
use worktree::{CreatedEntry, Snapshot, Traversal};
use worktree_store::{WorktreeStore, WorktreeStoreEvent};
@ -413,6 +412,28 @@ pub struct InlayHint {
pub resolve_state: ResolveState,
}
/// The user's intent behind a given completion confirmation
#[derive(PartialEq, Eq, Hash, Debug, Clone, Copy)]
pub enum CompletionIntent {
/// The user intends to 'commit' this result, if possible
/// completion confirmations should run side effects
Complete,
/// The user intends to continue 'composing' this completion
/// completion confirmations should not run side effects and
/// let the user continue composing their action
Compose,
}
impl CompletionIntent {
pub fn is_complete(&self) -> bool {
self == &Self::Complete
}
pub fn is_compose(&self) -> bool {
self == &Self::Compose
}
}
/// A completion provided by a language server
#[derive(Clone)]
pub struct Completion {
@ -429,7 +450,7 @@ pub struct Completion {
/// The raw completion provided by the language server.
pub lsp_completion: lsp::CompletionItem,
/// An optional callback to invoke when this completion is confirmed.
pub confirm: Option<Arc<dyn Send + Sync + Fn(&mut WindowContext)>>,
pub confirm: Option<Arc<dyn Send + Sync + Fn(CompletionIntent, &mut WindowContext)>>,
/// If true, the editor will show a new completion menu after this completion is confirmed.
pub show_new_completions_on_confirm: bool,
}
@ -11011,10 +11032,12 @@ impl<'a> Iterator for PathMatchCandidateSetIter<'a> {
fn next(&mut self) -> Option<Self::Item> {
self.traversal.next().map(|entry| match entry.kind {
EntryKind::Dir => fuzzy::PathMatchCandidate {
is_dir: true,
path: &entry.path,
char_bag: CharBag::from_iter(entry.path.to_string_lossy().to_lowercase().chars()),
},
EntryKind::File(char_bag) => fuzzy::PathMatchCandidate {
is_dir: false,
path: &entry.path,
char_bag,
},
@ -11565,86 +11588,3 @@ fn sort_search_matches(search_matches: &mut Vec<SearchMatchCandidate>, cx: &AppC
}),
});
}
pub fn compare_paths(
(path_a, a_is_file): (&Path, bool),
(path_b, b_is_file): (&Path, bool),
) -> cmp::Ordering {
let mut components_a = path_a.components().peekable();
let mut components_b = path_b.components().peekable();
loop {
match (components_a.next(), components_b.next()) {
(Some(component_a), Some(component_b)) => {
let a_is_file = components_a.peek().is_none() && a_is_file;
let b_is_file = components_b.peek().is_none() && b_is_file;
let ordering = a_is_file.cmp(&b_is_file).then_with(|| {
let maybe_numeric_ordering = maybe!({
let path_a = Path::new(component_a.as_os_str());
let num_and_remainder_a = if a_is_file {
path_a.file_stem()
} else {
path_a.file_name()
}
.and_then(|s| s.to_str())
.and_then(NumericPrefixWithSuffix::from_numeric_prefixed_str)?;
let path_b = Path::new(component_b.as_os_str());
let num_and_remainder_b = if b_is_file {
path_b.file_stem()
} else {
path_b.file_name()
}
.and_then(|s| s.to_str())
.and_then(NumericPrefixWithSuffix::from_numeric_prefixed_str)?;
num_and_remainder_a.partial_cmp(&num_and_remainder_b)
});
maybe_numeric_ordering.unwrap_or_else(|| {
let name_a = UniCase::new(component_a.as_os_str().to_string_lossy());
let name_b = UniCase::new(component_b.as_os_str().to_string_lossy());
name_a.cmp(&name_b)
})
});
if !ordering.is_eq() {
return ordering;
}
}
(Some(_), None) => break cmp::Ordering::Greater,
(None, Some(_)) => break cmp::Ordering::Less,
(None, None) => break cmp::Ordering::Equal,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn compare_paths_with_dots() {
let mut paths = vec![
(Path::new("test_dirs"), false),
(Path::new("test_dirs/1.46"), false),
(Path::new("test_dirs/1.46/bar_1"), true),
(Path::new("test_dirs/1.46/bar_2"), true),
(Path::new("test_dirs/1.45"), false),
(Path::new("test_dirs/1.45/foo_2"), true),
(Path::new("test_dirs/1.45/foo_1"), true),
];
paths.sort_by(|&a, &b| compare_paths(a, b));
assert_eq!(
paths,
vec![
(Path::new("test_dirs"), false),
(Path::new("test_dirs/1.45"), false),
(Path::new("test_dirs/1.45/foo_1"), true),
(Path::new("test_dirs/1.45/foo_2"), true),
(Path::new("test_dirs/1.46"), false),
(Path::new("test_dirs/1.46/bar_1"), true),
(Path::new("test_dirs/1.46/bar_2"), true),
]
);
}
}