mirror of
https://github.com/zed-industries/zed.git
synced 2024-09-19 02:17:35 +03:00
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
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -8085,7 +8085,6 @@ dependencies = [
|
||||
"tempfile",
|
||||
"terminal",
|
||||
"text",
|
||||
"unicase",
|
||||
"unindent",
|
||||
"util",
|
||||
"which 6.0.2",
|
||||
|
@ -437,7 +437,7 @@
|
||||
"context": "Editor && showing_completions",
|
||||
"bindings": {
|
||||
"enter": "editor::ConfirmCompletion",
|
||||
"tab": "editor::ConfirmCompletion"
|
||||
"tab": "editor::ComposeCompletion"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -474,7 +474,7 @@
|
||||
"context": "Editor && showing_completions",
|
||||
"bindings": {
|
||||
"enter": "editor::ConfirmCompletion",
|
||||
"tab": "editor::ConfirmCompletion"
|
||||
"tab": "editor::ComposeCompletion"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -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),
|
||||
|
@ -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,
|
||||
}]))
|
||||
|
@ -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.
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
})
|
||||
|
@ -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,
|
||||
}),
|
||||
]
|
||||
);
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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]
|
||||
|
@ -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),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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 = [
|
||||
|
Loading…
Reference in New Issue
Block a user