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

1
Cargo.lock generated
View File

@ -8085,7 +8085,6 @@ dependencies = [
"tempfile",
"terminal",
"text",
"unicase",
"unindent",
"util",
"which 6.0.2",

View File

@ -437,7 +437,7 @@
"context": "Editor && showing_completions",
"bindings": {
"enter": "editor::ConfirmCompletion",
"tab": "editor::ConfirmCompletion"
"tab": "editor::ComposeCompletion"
}
},
{

View File

@ -474,7 +474,7 @@
"context": "Editor && showing_completions",
"bindings": {
"enter": "editor::ConfirmCompletion",
"tab": "editor::ConfirmCompletion"
"tab": "editor::ComposeCompletion"
}
},
{

View File

@ -6,6 +6,7 @@ use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{AppContext, Model, Task, ViewContext, WeakView, WindowContext};
use language::{Anchor, Buffer, CodeLabel, Documentation, HighlightId, LanguageServerId, ToPoint};
use parking_lot::{Mutex, RwLock};
use project::CompletionIntent;
use rope::Point;
use std::{
ops::Range,
@ -106,20 +107,24 @@ impl SlashCommandCompletionProvider {
let command_range = command_range.clone();
let editor = editor.clone();
let workspace = workspace.clone();
Arc::new(move |cx: &mut WindowContext| {
editor
.update(cx, |editor, cx| {
editor.run_command(
command_range.clone(),
&command_name,
None,
true,
workspace.clone(),
cx,
);
})
.ok();
}) as Arc<_>
Arc::new(
move |intent: CompletionIntent, cx: &mut WindowContext| {
if intent.is_complete() {
editor
.update(cx, |editor, cx| {
editor.run_command(
command_range.clone(),
&command_name,
None,
true,
workspace.clone(),
cx,
);
})
.ok();
}
},
) as Arc<_>
})
},
);
@ -151,7 +156,6 @@ impl SlashCommandCompletionProvider {
let mut flag = self.cancel_flag.lock();
flag.store(true, SeqCst);
*flag = new_cancel_flag.clone();
let commands = SlashCommandRegistry::global(cx);
if let Some(command) = commands.command(command_name) {
let completions = command.complete_argument(
@ -177,19 +181,21 @@ impl SlashCommandCompletionProvider {
let command_range = command_range.clone();
let command_name = command_name.clone();
let command_argument = command_argument.new_text.clone();
move |cx: &mut WindowContext| {
editor
.update(cx, |editor, cx| {
editor.run_command(
command_range.clone(),
&command_name,
Some(&command_argument),
true,
workspace.clone(),
cx,
);
})
.ok();
move |intent: CompletionIntent, cx: &mut WindowContext| {
if intent.is_complete() {
editor
.update(cx, |editor, cx| {
editor.run_command(
command_range.clone(),
&command_name,
Some(&command_argument),
true,
workspace.clone(),
cx,
);
})
.ok();
}
}
}) as Arc<_>
})
@ -204,7 +210,7 @@ impl SlashCommandCompletionProvider {
project::Completion {
old_range: argument_range.clone(),
label: CodeLabel::plain(command_argument.label, None),
label: command_argument.label,
new_text,
documentation: None,
server_id: LanguageServerId(0),

View File

@ -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,
})
})

View File

@ -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,
}]);

View File

@ -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())
})

View File

@ -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,
})

View File

@ -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,
})

View File

@ -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,
}]))

View File

@ -18,7 +18,7 @@ pub fn init(cx: &mut AppContext) {
#[derive(Debug)]
pub struct ArgumentCompletion {
/// The label to display for this completion.
pub label: String,
pub label: CodeLabel,
/// The new text that should be inserted into the command when this completion is accepted.
pub new_text: String,
/// Whether the command should be run when accepting this completion.

View File

@ -64,6 +64,12 @@ pub struct ConfirmCompletion {
pub item_ix: Option<usize>,
}
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct ComposeCompletion {
#[serde(default)]
pub item_ix: Option<usize>,
}
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct ConfirmCodeAction {
#[serde(default)]
@ -140,6 +146,7 @@ impl_actions!(
[
ConfirmCodeAction,
ConfirmCompletion,
ComposeCompletion,
ExpandExcerpts,
ExpandExcerptsUp,
ExpandExcerptsDown,

View File

@ -114,7 +114,7 @@ use ordered_float::OrderedFloat;
use parking_lot::{Mutex, RwLock};
use project::project_settings::{GitGutterSetting, ProjectSettings};
use project::{
CodeAction, Completion, FormatTrigger, Item, Location, Project, ProjectPath,
CodeAction, Completion, CompletionIntent, FormatTrigger, Item, Location, Project, ProjectPath,
ProjectTransaction, TaskSourceKind, WorktreeId,
};
use rand::prelude::*;
@ -4213,6 +4213,23 @@ impl Editor {
action: &ConfirmCompletion,
cx: &mut ViewContext<Self>,
) -> Option<Task<Result<()>>> {
self.do_completion(action.item_ix, CompletionIntent::Complete, cx)
}
pub fn compose_completion(
&mut self,
action: &ComposeCompletion,
cx: &mut ViewContext<Self>,
) -> Option<Task<Result<()>>> {
self.do_completion(action.item_ix, CompletionIntent::Compose, cx)
}
fn do_completion(
&mut self,
item_ix: Option<usize>,
intent: CompletionIntent,
cx: &mut ViewContext<Editor>,
) -> Option<Task<std::result::Result<(), anyhow::Error>>> {
use language::ToOffset as _;
let completions_menu = if let ContextMenu::Completions(menu) = self.hide_context_menu(cx)? {
@ -4223,7 +4240,7 @@ impl Editor {
let mat = completions_menu
.matches
.get(action.item_ix.unwrap_or(completions_menu.selected_item))?;
.get(item_ix.unwrap_or(completions_menu.selected_item))?;
let buffer_handle = completions_menu.buffer;
let completions = completions_menu.completions.read();
let completion = completions.get(mat.candidate_id)?;
@ -4358,7 +4375,7 @@ impl Editor {
});
if let Some(confirm) = completion.confirm.as_ref() {
(confirm)(cx);
(confirm)(intent, cx);
}
if completion.show_new_completions_on_confirm {

View File

@ -372,6 +372,13 @@ impl EditorElement {
cx.propagate();
}
});
register_action(view, cx, |editor, action, cx| {
if let Some(task) = editor.compose_completion(action, cx) {
task.detach_and_log_err(cx);
} else {
cx.propagate();
}
});
register_action(view, cx, |editor, action, cx| {
if let Some(task) = editor.confirm_code_action(action, cx) {
task.detach_and_log_err(cx);

View File

@ -63,7 +63,7 @@ impl SlashCommand for ExtensionSlashCommand {
completions
.into_iter()
.map(|completion| ArgumentCompletion {
label: completion.label,
label: completion.label.into(),
new_text: completion.new_text,
run_command: completion.run_command,
})

View File

@ -318,6 +318,7 @@ fn matching_history_item_paths<'a>(
.chain(currently_opened)
.filter_map(|found_path| {
let candidate = PathMatchCandidate {
is_dir: false, // You can't open directories as project items
path: &found_path.project.path,
// Only match history items names, otherwise their paths may match too many queries, producing false positives.
// E.g. `foo` would match both `something/foo/bar.rs` and `something/foo/foo.rs` and if the former is a history item,
@ -588,6 +589,7 @@ impl FileFinderDelegate {
positions: Vec::new(),
worktree_id: worktree_id.to_usize(),
path,
is_dir: false, // File finder doesn't support directories
path_prefix: "".into(),
distance_to_relative_ancestor: usize::MAX,
};
@ -688,6 +690,7 @@ impl FileFinderDelegate {
worktree_id: worktree.read(cx).id().to_usize(),
path: Arc::from(relative_path),
path_prefix: "".into(),
is_dir: false, // File finder doesn't support directories
distance_to_relative_ancestor: usize::MAX,
}));
}
@ -1001,6 +1004,7 @@ mod tests {
path: Arc::from(Path::new("b0.5")),
path_prefix: Arc::default(),
distance_to_relative_ancestor: 0,
is_dir: false,
}),
ProjectPanelOrdMatch(PathMatch {
score: 1.0,
@ -1009,6 +1013,7 @@ mod tests {
path: Arc::from(Path::new("c1.0")),
path_prefix: Arc::default(),
distance_to_relative_ancestor: 0,
is_dir: false,
}),
ProjectPanelOrdMatch(PathMatch {
score: 1.0,
@ -1017,6 +1022,7 @@ mod tests {
path: Arc::from(Path::new("a1.0")),
path_prefix: Arc::default(),
distance_to_relative_ancestor: 0,
is_dir: false,
}),
ProjectPanelOrdMatch(PathMatch {
score: 0.5,
@ -1025,6 +1031,7 @@ mod tests {
path: Arc::from(Path::new("a0.5")),
path_prefix: Arc::default(),
distance_to_relative_ancestor: 0,
is_dir: false,
}),
ProjectPanelOrdMatch(PathMatch {
score: 1.0,
@ -1033,6 +1040,7 @@ mod tests {
path: Arc::from(Path::new("b1.0")),
path_prefix: Arc::default(),
distance_to_relative_ancestor: 0,
is_dir: false,
}),
];
file_finder_sorted_output.sort_by(|a, b| b.cmp(a));
@ -1047,6 +1055,7 @@ mod tests {
path: Arc::from(Path::new("a1.0")),
path_prefix: Arc::default(),
distance_to_relative_ancestor: 0,
is_dir: false,
}),
ProjectPanelOrdMatch(PathMatch {
score: 1.0,
@ -1055,6 +1064,7 @@ mod tests {
path: Arc::from(Path::new("b1.0")),
path_prefix: Arc::default(),
distance_to_relative_ancestor: 0,
is_dir: false,
}),
ProjectPanelOrdMatch(PathMatch {
score: 1.0,
@ -1063,6 +1073,7 @@ mod tests {
path: Arc::from(Path::new("c1.0")),
path_prefix: Arc::default(),
distance_to_relative_ancestor: 0,
is_dir: false,
}),
ProjectPanelOrdMatch(PathMatch {
score: 0.5,
@ -1071,6 +1082,7 @@ mod tests {
path: Arc::from(Path::new("a0.5")),
path_prefix: Arc::default(),
distance_to_relative_ancestor: 0,
is_dir: false,
}),
ProjectPanelOrdMatch(PathMatch {
score: 0.5,
@ -1079,6 +1091,7 @@ mod tests {
path: Arc::from(Path::new("b0.5")),
path_prefix: Arc::default(),
distance_to_relative_ancestor: 0,
is_dir: false,
}),
]
);

View File

@ -1,7 +1,7 @@
use futures::channel::oneshot;
use fuzzy::StringMatchCandidate;
use picker::{Picker, PickerDelegate};
use project::{compare_paths, DirectoryLister};
use project::DirectoryLister;
use std::{
path::{Path, PathBuf},
sync::{
@ -11,7 +11,7 @@ use std::{
};
use ui::{prelude::*, LabelLike, ListItemSpacing};
use ui::{ListItem, ViewContext};
use util::maybe;
use util::{maybe, paths::compare_paths};
use workspace::Workspace;
pub(crate) struct OpenPathPrompt;

View File

@ -445,6 +445,7 @@ mod tests {
let lowercase_path = path.to_lowercase().chars().collect::<Vec<_>>();
let char_bag = CharBag::from(lowercase_path.as_slice());
path_entries.push(PathMatchCandidate {
is_dir: false,
char_bag,
path: &path_arcs[i],
});
@ -468,6 +469,7 @@ mod tests {
path: Arc::from(candidate.path),
path_prefix: "".into(),
distance_to_relative_ancestor: usize::MAX,
is_dir: false,
},
);

View File

@ -13,6 +13,7 @@ use crate::{
#[derive(Clone, Debug)]
pub struct PathMatchCandidate<'a> {
pub is_dir: bool,
pub path: &'a Path,
pub char_bag: CharBag,
}
@ -24,6 +25,7 @@ pub struct PathMatch {
pub worktree_id: usize,
pub path: Arc<Path>,
pub path_prefix: Arc<str>,
pub is_dir: bool,
/// Number of steps removed from a shared parent with the relative path
/// Used to order closer paths first in the search list
pub distance_to_relative_ancestor: usize,
@ -119,6 +121,7 @@ pub fn match_fixed_path_set(
score,
worktree_id,
positions: Vec::new(),
is_dir: candidate.is_dir,
path: Arc::from(candidate.path),
path_prefix: Arc::default(),
distance_to_relative_ancestor: usize::MAX,
@ -195,6 +198,7 @@ pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
worktree_id,
positions: Vec::new(),
path: Arc::from(candidate.path),
is_dir: candidate.is_dir,
path_prefix: candidate_set.prefix(),
distance_to_relative_ancestor: relative_to.as_ref().map_or(
usize::MAX,

View File

@ -1560,6 +1560,22 @@ impl CodeLabel {
self.runs.push((start_ix..end_ix, highlight));
}
}
pub fn text(&self) -> &str {
self.text.as_str()
}
}
impl From<String> for CodeLabel {
fn from(value: String) -> Self {
Self::plain(value, None)
}
}
impl From<&str> for CodeLabel {
fn from(value: &str) -> Self {
Self::plain(value.to_string(), None)
}
}
impl Ord for LanguageMatcher {

View File

@ -69,7 +69,6 @@ snippet_provider.workspace = true
terminal.workspace = true
text.workspace = true
util.workspace = true
unicase.workspace = true
which.workspace = true
[dev-dependencies]

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),
]
);
}
}

View File

@ -1,3 +1,4 @@
use std::cmp;
use std::sync::OnceLock;
use std::{
ffi::OsStr,
@ -8,6 +9,9 @@ use std::{
use globset::{Glob, GlobSet, GlobSetBuilder};
use regex::Regex;
use serde::{Deserialize, Serialize};
use unicase::UniCase;
use crate::{maybe, NumericPrefixWithSuffix};
/// Returns the path to the user's home directory.
pub fn home_dir() -> &'static PathBuf {
@ -266,10 +270,88 @@ impl PathMatcher {
}
}
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),
]
);
}
#[test]
fn path_with_position_parsing_positive() {
let input_and_expected = [