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",
|
"tempfile",
|
||||||
"terminal",
|
"terminal",
|
||||||
"text",
|
"text",
|
||||||
"unicase",
|
|
||||||
"unindent",
|
"unindent",
|
||||||
"util",
|
"util",
|
||||||
"which 6.0.2",
|
"which 6.0.2",
|
||||||
|
@ -437,7 +437,7 @@
|
|||||||
"context": "Editor && showing_completions",
|
"context": "Editor && showing_completions",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"enter": "editor::ConfirmCompletion",
|
"enter": "editor::ConfirmCompletion",
|
||||||
"tab": "editor::ConfirmCompletion"
|
"tab": "editor::ComposeCompletion"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -474,7 +474,7 @@
|
|||||||
"context": "Editor && showing_completions",
|
"context": "Editor && showing_completions",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"enter": "editor::ConfirmCompletion",
|
"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 gpui::{AppContext, Model, Task, ViewContext, WeakView, WindowContext};
|
||||||
use language::{Anchor, Buffer, CodeLabel, Documentation, HighlightId, LanguageServerId, ToPoint};
|
use language::{Anchor, Buffer, CodeLabel, Documentation, HighlightId, LanguageServerId, ToPoint};
|
||||||
use parking_lot::{Mutex, RwLock};
|
use parking_lot::{Mutex, RwLock};
|
||||||
|
use project::CompletionIntent;
|
||||||
use rope::Point;
|
use rope::Point;
|
||||||
use std::{
|
use std::{
|
||||||
ops::Range,
|
ops::Range,
|
||||||
@ -106,20 +107,24 @@ impl SlashCommandCompletionProvider {
|
|||||||
let command_range = command_range.clone();
|
let command_range = command_range.clone();
|
||||||
let editor = editor.clone();
|
let editor = editor.clone();
|
||||||
let workspace = workspace.clone();
|
let workspace = workspace.clone();
|
||||||
Arc::new(move |cx: &mut WindowContext| {
|
Arc::new(
|
||||||
editor
|
move |intent: CompletionIntent, cx: &mut WindowContext| {
|
||||||
.update(cx, |editor, cx| {
|
if intent.is_complete() {
|
||||||
editor.run_command(
|
editor
|
||||||
command_range.clone(),
|
.update(cx, |editor, cx| {
|
||||||
&command_name,
|
editor.run_command(
|
||||||
None,
|
command_range.clone(),
|
||||||
true,
|
&command_name,
|
||||||
workspace.clone(),
|
None,
|
||||||
cx,
|
true,
|
||||||
);
|
workspace.clone(),
|
||||||
})
|
cx,
|
||||||
.ok();
|
);
|
||||||
}) as Arc<_>
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) as Arc<_>
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -151,7 +156,6 @@ impl SlashCommandCompletionProvider {
|
|||||||
let mut flag = self.cancel_flag.lock();
|
let mut flag = self.cancel_flag.lock();
|
||||||
flag.store(true, SeqCst);
|
flag.store(true, SeqCst);
|
||||||
*flag = new_cancel_flag.clone();
|
*flag = new_cancel_flag.clone();
|
||||||
|
|
||||||
let commands = SlashCommandRegistry::global(cx);
|
let commands = SlashCommandRegistry::global(cx);
|
||||||
if let Some(command) = commands.command(command_name) {
|
if let Some(command) = commands.command(command_name) {
|
||||||
let completions = command.complete_argument(
|
let completions = command.complete_argument(
|
||||||
@ -177,19 +181,21 @@ impl SlashCommandCompletionProvider {
|
|||||||
let command_range = command_range.clone();
|
let command_range = command_range.clone();
|
||||||
let command_name = command_name.clone();
|
let command_name = command_name.clone();
|
||||||
let command_argument = command_argument.new_text.clone();
|
let command_argument = command_argument.new_text.clone();
|
||||||
move |cx: &mut WindowContext| {
|
move |intent: CompletionIntent, cx: &mut WindowContext| {
|
||||||
editor
|
if intent.is_complete() {
|
||||||
.update(cx, |editor, cx| {
|
editor
|
||||||
editor.run_command(
|
.update(cx, |editor, cx| {
|
||||||
command_range.clone(),
|
editor.run_command(
|
||||||
&command_name,
|
command_range.clone(),
|
||||||
Some(&command_argument),
|
&command_name,
|
||||||
true,
|
Some(&command_argument),
|
||||||
workspace.clone(),
|
true,
|
||||||
cx,
|
workspace.clone(),
|
||||||
);
|
cx,
|
||||||
})
|
);
|
||||||
.ok();
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}) as Arc<_>
|
}) as Arc<_>
|
||||||
})
|
})
|
||||||
@ -204,7 +210,7 @@ impl SlashCommandCompletionProvider {
|
|||||||
|
|
||||||
project::Completion {
|
project::Completion {
|
||||||
old_range: argument_range.clone(),
|
old_range: argument_range.clone(),
|
||||||
label: CodeLabel::plain(command_argument.label, None),
|
label: command_argument.label,
|
||||||
new_text,
|
new_text,
|
||||||
documentation: None,
|
documentation: None,
|
||||||
server_id: LanguageServerId(0),
|
server_id: LanguageServerId(0),
|
||||||
|
@ -43,6 +43,7 @@ impl DiagnosticsSlashCommand {
|
|||||||
worktree_id: entry.worktree_id.to_usize(),
|
worktree_id: entry.worktree_id.to_usize(),
|
||||||
path: entry.path.clone(),
|
path: entry.path.clone(),
|
||||||
path_prefix: path_prefix.clone(),
|
path_prefix: path_prefix.clone(),
|
||||||
|
is_dir: false, // Diagnostics can't be produced for directories
|
||||||
distance_to_relative_ancestor: 0,
|
distance_to_relative_ancestor: 0,
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
@ -146,7 +147,7 @@ impl SlashCommand for DiagnosticsSlashCommand {
|
|||||||
Ok(matches
|
Ok(matches
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|completion| ArgumentCompletion {
|
.map(|completion| ArgumentCompletion {
|
||||||
label: completion.clone(),
|
label: completion.clone().into(),
|
||||||
new_text: completion,
|
new_text: completion,
|
||||||
run_command: true,
|
run_command: true,
|
||||||
})
|
})
|
||||||
@ -168,58 +169,66 @@ impl SlashCommand for DiagnosticsSlashCommand {
|
|||||||
let options = Options::parse(argument);
|
let options = Options::parse(argument);
|
||||||
|
|
||||||
let task = collect_diagnostics(workspace.read(cx).project().clone(), options, cx);
|
let task = collect_diagnostics(workspace.read(cx).project().clone(), options, cx);
|
||||||
|
|
||||||
cx.spawn(move |_| async move {
|
cx.spawn(move |_| async move {
|
||||||
let Some((text, sections)) = task.await? else {
|
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 {
|
Ok(SlashCommandOutput {
|
||||||
text,
|
text,
|
||||||
sections: 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(),
|
|
||||||
run_commands_in_text: false,
|
run_commands_in_text: false,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -182,7 +182,7 @@ impl SlashCommand for DocsSlashCommand {
|
|||||||
items
|
items
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|item| ArgumentCompletion {
|
.map(|item| ArgumentCompletion {
|
||||||
label: item.clone(),
|
label: item.clone().into(),
|
||||||
new_text: format!("{provider} {item}"),
|
new_text: format!("{provider} {item}"),
|
||||||
run_command: true,
|
run_command: true,
|
||||||
})
|
})
|
||||||
@ -194,7 +194,7 @@ impl SlashCommand for DocsSlashCommand {
|
|||||||
let providers = indexed_docs_registry.list_providers();
|
let providers = indexed_docs_registry.list_providers();
|
||||||
if providers.is_empty() {
|
if providers.is_empty() {
|
||||||
return Ok(vec![ArgumentCompletion {
|
return Ok(vec![ArgumentCompletion {
|
||||||
label: "No available docs providers.".to_string(),
|
label: "No available docs providers.".into(),
|
||||||
new_text: String::new(),
|
new_text: String::new(),
|
||||||
run_command: false,
|
run_command: false,
|
||||||
}]);
|
}]);
|
||||||
@ -203,7 +203,7 @@ impl SlashCommand for DocsSlashCommand {
|
|||||||
Ok(providers
|
Ok(providers
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|provider| ArgumentCompletion {
|
.map(|provider| ArgumentCompletion {
|
||||||
label: provider.to_string(),
|
label: provider.to_string().into(),
|
||||||
new_text: provider.to_string(),
|
new_text: provider.to_string(),
|
||||||
run_command: false,
|
run_command: false,
|
||||||
})
|
})
|
||||||
@ -231,10 +231,10 @@ impl SlashCommand for DocsSlashCommand {
|
|||||||
.filter(|package_name| {
|
.filter(|package_name| {
|
||||||
!items
|
!items
|
||||||
.iter()
|
.iter()
|
||||||
.any(|item| item.label.as_str() == package_name.as_ref())
|
.any(|item| item.label.text() == package_name.as_ref())
|
||||||
})
|
})
|
||||||
.map(|package_name| ArgumentCompletion {
|
.map(|package_name| ArgumentCompletion {
|
||||||
label: format!("{package_name} (unindexed)"),
|
label: format!("{package_name} (unindexed)").into(),
|
||||||
new_text: format!("{provider} {package_name}"),
|
new_text: format!("{provider} {package_name}"),
|
||||||
run_command: true,
|
run_command: true,
|
||||||
})
|
})
|
||||||
@ -246,7 +246,8 @@ impl SlashCommand for DocsSlashCommand {
|
|||||||
label: format!(
|
label: format!(
|
||||||
"Enter a {package_term} name.",
|
"Enter a {package_term} name.",
|
||||||
package_term = package_term(&provider)
|
package_term = package_term(&provider)
|
||||||
),
|
)
|
||||||
|
.into(),
|
||||||
new_text: provider.to_string(),
|
new_text: provider.to_string(),
|
||||||
run_command: false,
|
run_command: false,
|
||||||
}]);
|
}]);
|
||||||
|
@ -3,7 +3,7 @@ use anyhow::{anyhow, Result};
|
|||||||
use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
|
use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
|
||||||
use fuzzy::PathMatch;
|
use fuzzy::PathMatch;
|
||||||
use gpui::{AppContext, Model, Task, View, WeakView};
|
use gpui::{AppContext, Model, Task, View, WeakView};
|
||||||
use language::{BufferSnapshot, LineEnding, LspAdapterDelegate};
|
use language::{BufferSnapshot, CodeLabel, HighlightId, LineEnding, LspAdapterDelegate};
|
||||||
use project::{PathMatchCandidateSet, Project};
|
use project::{PathMatchCandidateSet, Project};
|
||||||
use std::{
|
use std::{
|
||||||
fmt::Write,
|
fmt::Write,
|
||||||
@ -29,11 +29,30 @@ impl FileSlashCommand {
|
|||||||
let workspace = workspace.read(cx);
|
let workspace = workspace.read(cx);
|
||||||
let project = workspace.project().read(cx);
|
let project = workspace.project().read(cx);
|
||||||
let entries = workspace.recent_navigation_history(Some(10), 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();
|
let path_prefix: Arc<str> = Arc::default();
|
||||||
Task::ready(
|
Task::ready(
|
||||||
entries
|
entries
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|(entry, _)| {
|
.filter_map(|(entry, is_dir)| {
|
||||||
let worktree = project.worktree_for_id(entry.worktree_id, cx)?;
|
let worktree = project.worktree_for_id(entry.worktree_id, cx)?;
|
||||||
let mut full_path = PathBuf::from(worktree.read(cx).root_name());
|
let mut full_path = PathBuf::from(worktree.read(cx).root_name());
|
||||||
full_path.push(&entry.path);
|
full_path.push(&entry.path);
|
||||||
@ -44,6 +63,7 @@ impl FileSlashCommand {
|
|||||||
path: full_path.into(),
|
path: full_path.into(),
|
||||||
path_prefix: path_prefix.clone(),
|
path_prefix: path_prefix.clone(),
|
||||||
distance_to_relative_ancestor: 0,
|
distance_to_relative_ancestor: 0,
|
||||||
|
is_dir,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
@ -54,6 +74,7 @@ impl FileSlashCommand {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|worktree| {
|
.map(|worktree| {
|
||||||
let worktree = worktree.read(cx);
|
let worktree = worktree.read(cx);
|
||||||
|
|
||||||
PathMatchCandidateSet {
|
PathMatchCandidateSet {
|
||||||
snapshot: worktree.snapshot(),
|
snapshot: worktree.snapshot(),
|
||||||
include_ignored: worktree
|
include_ignored: worktree
|
||||||
@ -111,22 +132,35 @@ impl SlashCommand for FileSlashCommand {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let paths = self.search_paths(query, cancellation_flag, &workspace, cx);
|
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 {
|
cx.background_executor().spawn(async move {
|
||||||
Ok(paths
|
Ok(paths
|
||||||
.await
|
.await
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|path_match| {
|
.filter_map(|path_match| {
|
||||||
let text = format!(
|
let text = format!(
|
||||||
"{}{}",
|
"{}{}",
|
||||||
path_match.path_prefix,
|
path_match.path_prefix,
|
||||||
path_match.path.to_string_lossy()
|
path_match.path.to_string_lossy()
|
||||||
);
|
);
|
||||||
|
|
||||||
ArgumentCompletion {
|
let mut label = CodeLabel::default();
|
||||||
label: text.clone(),
|
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,
|
new_text: text,
|
||||||
run_command: true,
|
run_command: true,
|
||||||
}
|
})
|
||||||
})
|
})
|
||||||
.collect())
|
.collect())
|
||||||
})
|
})
|
||||||
|
@ -42,7 +42,7 @@ impl SlashCommand for PromptSlashCommand {
|
|||||||
.filter_map(|prompt| {
|
.filter_map(|prompt| {
|
||||||
let prompt_title = prompt.title?.to_string();
|
let prompt_title = prompt.title?.to_string();
|
||||||
Some(ArgumentCompletion {
|
Some(ArgumentCompletion {
|
||||||
label: prompt_title.clone(),
|
label: prompt_title.clone().into(),
|
||||||
new_text: prompt_title,
|
new_text: prompt_title,
|
||||||
run_command: true,
|
run_command: true,
|
||||||
})
|
})
|
||||||
|
@ -47,7 +47,7 @@ impl SlashCommand for TabsSlashCommand {
|
|||||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||||
let all_tabs_completion_item = if ALL_TABS_COMPLETION_ITEM.contains(&query) {
|
let all_tabs_completion_item = if ALL_TABS_COMPLETION_ITEM.contains(&query) {
|
||||||
Some(ArgumentCompletion {
|
Some(ArgumentCompletion {
|
||||||
label: ALL_TABS_COMPLETION_ITEM.to_owned(),
|
label: ALL_TABS_COMPLETION_ITEM.into(),
|
||||||
new_text: ALL_TABS_COMPLETION_ITEM.to_owned(),
|
new_text: ALL_TABS_COMPLETION_ITEM.to_owned(),
|
||||||
run_command: true,
|
run_command: true,
|
||||||
})
|
})
|
||||||
@ -63,7 +63,7 @@ impl SlashCommand for TabsSlashCommand {
|
|||||||
.filter_map(|(path, ..)| {
|
.filter_map(|(path, ..)| {
|
||||||
let path_string = path.as_deref()?.to_string_lossy().to_string();
|
let path_string = path.as_deref()?.to_string_lossy().to_string();
|
||||||
Some(ArgumentCompletion {
|
Some(ArgumentCompletion {
|
||||||
label: path_string.clone(),
|
label: path_string.clone().into(),
|
||||||
new_text: path_string,
|
new_text: path_string,
|
||||||
run_command: true,
|
run_command: true,
|
||||||
})
|
})
|
||||||
|
@ -48,7 +48,7 @@ impl SlashCommand for TerminalSlashCommand {
|
|||||||
_cx: &mut WindowContext,
|
_cx: &mut WindowContext,
|
||||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||||
Task::ready(Ok(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(),
|
new_text: LINE_COUNT_ARG.to_string(),
|
||||||
run_command: true,
|
run_command: true,
|
||||||
}]))
|
}]))
|
||||||
|
@ -18,7 +18,7 @@ pub fn init(cx: &mut AppContext) {
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ArgumentCompletion {
|
pub struct ArgumentCompletion {
|
||||||
/// The label to display for this completion.
|
/// 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.
|
/// The new text that should be inserted into the command when this completion is accepted.
|
||||||
pub new_text: String,
|
pub new_text: String,
|
||||||
/// Whether the command should be run when accepting this completion.
|
/// Whether the command should be run when accepting this completion.
|
||||||
|
@ -64,6 +64,12 @@ pub struct ConfirmCompletion {
|
|||||||
pub item_ix: Option<usize>,
|
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)]
|
#[derive(PartialEq, Clone, Deserialize, Default)]
|
||||||
pub struct ConfirmCodeAction {
|
pub struct ConfirmCodeAction {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@ -140,6 +146,7 @@ impl_actions!(
|
|||||||
[
|
[
|
||||||
ConfirmCodeAction,
|
ConfirmCodeAction,
|
||||||
ConfirmCompletion,
|
ConfirmCompletion,
|
||||||
|
ComposeCompletion,
|
||||||
ExpandExcerpts,
|
ExpandExcerpts,
|
||||||
ExpandExcerptsUp,
|
ExpandExcerptsUp,
|
||||||
ExpandExcerptsDown,
|
ExpandExcerptsDown,
|
||||||
|
@ -114,7 +114,7 @@ use ordered_float::OrderedFloat;
|
|||||||
use parking_lot::{Mutex, RwLock};
|
use parking_lot::{Mutex, RwLock};
|
||||||
use project::project_settings::{GitGutterSetting, ProjectSettings};
|
use project::project_settings::{GitGutterSetting, ProjectSettings};
|
||||||
use project::{
|
use project::{
|
||||||
CodeAction, Completion, FormatTrigger, Item, Location, Project, ProjectPath,
|
CodeAction, Completion, CompletionIntent, FormatTrigger, Item, Location, Project, ProjectPath,
|
||||||
ProjectTransaction, TaskSourceKind, WorktreeId,
|
ProjectTransaction, TaskSourceKind, WorktreeId,
|
||||||
};
|
};
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
@ -4213,6 +4213,23 @@ impl Editor {
|
|||||||
action: &ConfirmCompletion,
|
action: &ConfirmCompletion,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> Option<Task<Result<()>>> {
|
) -> 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 _;
|
use language::ToOffset as _;
|
||||||
|
|
||||||
let completions_menu = if let ContextMenu::Completions(menu) = self.hide_context_menu(cx)? {
|
let completions_menu = if let ContextMenu::Completions(menu) = self.hide_context_menu(cx)? {
|
||||||
@ -4223,7 +4240,7 @@ impl Editor {
|
|||||||
|
|
||||||
let mat = completions_menu
|
let mat = completions_menu
|
||||||
.matches
|
.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 buffer_handle = completions_menu.buffer;
|
||||||
let completions = completions_menu.completions.read();
|
let completions = completions_menu.completions.read();
|
||||||
let completion = completions.get(mat.candidate_id)?;
|
let completion = completions.get(mat.candidate_id)?;
|
||||||
@ -4358,7 +4375,7 @@ impl Editor {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if let Some(confirm) = completion.confirm.as_ref() {
|
if let Some(confirm) = completion.confirm.as_ref() {
|
||||||
(confirm)(cx);
|
(confirm)(intent, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
if completion.show_new_completions_on_confirm {
|
if completion.show_new_completions_on_confirm {
|
||||||
|
@ -372,6 +372,13 @@ impl EditorElement {
|
|||||||
cx.propagate();
|
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| {
|
register_action(view, cx, |editor, action, cx| {
|
||||||
if let Some(task) = editor.confirm_code_action(action, cx) {
|
if let Some(task) = editor.confirm_code_action(action, cx) {
|
||||||
task.detach_and_log_err(cx);
|
task.detach_and_log_err(cx);
|
||||||
|
@ -63,7 +63,7 @@ impl SlashCommand for ExtensionSlashCommand {
|
|||||||
completions
|
completions
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|completion| ArgumentCompletion {
|
.map(|completion| ArgumentCompletion {
|
||||||
label: completion.label,
|
label: completion.label.into(),
|
||||||
new_text: completion.new_text,
|
new_text: completion.new_text,
|
||||||
run_command: completion.run_command,
|
run_command: completion.run_command,
|
||||||
})
|
})
|
||||||
|
@ -318,6 +318,7 @@ fn matching_history_item_paths<'a>(
|
|||||||
.chain(currently_opened)
|
.chain(currently_opened)
|
||||||
.filter_map(|found_path| {
|
.filter_map(|found_path| {
|
||||||
let candidate = PathMatchCandidate {
|
let candidate = PathMatchCandidate {
|
||||||
|
is_dir: false, // You can't open directories as project items
|
||||||
path: &found_path.project.path,
|
path: &found_path.project.path,
|
||||||
// Only match history items names, otherwise their paths may match too many queries, producing false positives.
|
// 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,
|
// 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(),
|
positions: Vec::new(),
|
||||||
worktree_id: worktree_id.to_usize(),
|
worktree_id: worktree_id.to_usize(),
|
||||||
path,
|
path,
|
||||||
|
is_dir: false, // File finder doesn't support directories
|
||||||
path_prefix: "".into(),
|
path_prefix: "".into(),
|
||||||
distance_to_relative_ancestor: usize::MAX,
|
distance_to_relative_ancestor: usize::MAX,
|
||||||
};
|
};
|
||||||
@ -688,6 +690,7 @@ impl FileFinderDelegate {
|
|||||||
worktree_id: worktree.read(cx).id().to_usize(),
|
worktree_id: worktree.read(cx).id().to_usize(),
|
||||||
path: Arc::from(relative_path),
|
path: Arc::from(relative_path),
|
||||||
path_prefix: "".into(),
|
path_prefix: "".into(),
|
||||||
|
is_dir: false, // File finder doesn't support directories
|
||||||
distance_to_relative_ancestor: usize::MAX,
|
distance_to_relative_ancestor: usize::MAX,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@ -1001,6 +1004,7 @@ mod tests {
|
|||||||
path: Arc::from(Path::new("b0.5")),
|
path: Arc::from(Path::new("b0.5")),
|
||||||
path_prefix: Arc::default(),
|
path_prefix: Arc::default(),
|
||||||
distance_to_relative_ancestor: 0,
|
distance_to_relative_ancestor: 0,
|
||||||
|
is_dir: false,
|
||||||
}),
|
}),
|
||||||
ProjectPanelOrdMatch(PathMatch {
|
ProjectPanelOrdMatch(PathMatch {
|
||||||
score: 1.0,
|
score: 1.0,
|
||||||
@ -1009,6 +1013,7 @@ mod tests {
|
|||||||
path: Arc::from(Path::new("c1.0")),
|
path: Arc::from(Path::new("c1.0")),
|
||||||
path_prefix: Arc::default(),
|
path_prefix: Arc::default(),
|
||||||
distance_to_relative_ancestor: 0,
|
distance_to_relative_ancestor: 0,
|
||||||
|
is_dir: false,
|
||||||
}),
|
}),
|
||||||
ProjectPanelOrdMatch(PathMatch {
|
ProjectPanelOrdMatch(PathMatch {
|
||||||
score: 1.0,
|
score: 1.0,
|
||||||
@ -1017,6 +1022,7 @@ mod tests {
|
|||||||
path: Arc::from(Path::new("a1.0")),
|
path: Arc::from(Path::new("a1.0")),
|
||||||
path_prefix: Arc::default(),
|
path_prefix: Arc::default(),
|
||||||
distance_to_relative_ancestor: 0,
|
distance_to_relative_ancestor: 0,
|
||||||
|
is_dir: false,
|
||||||
}),
|
}),
|
||||||
ProjectPanelOrdMatch(PathMatch {
|
ProjectPanelOrdMatch(PathMatch {
|
||||||
score: 0.5,
|
score: 0.5,
|
||||||
@ -1025,6 +1031,7 @@ mod tests {
|
|||||||
path: Arc::from(Path::new("a0.5")),
|
path: Arc::from(Path::new("a0.5")),
|
||||||
path_prefix: Arc::default(),
|
path_prefix: Arc::default(),
|
||||||
distance_to_relative_ancestor: 0,
|
distance_to_relative_ancestor: 0,
|
||||||
|
is_dir: false,
|
||||||
}),
|
}),
|
||||||
ProjectPanelOrdMatch(PathMatch {
|
ProjectPanelOrdMatch(PathMatch {
|
||||||
score: 1.0,
|
score: 1.0,
|
||||||
@ -1033,6 +1040,7 @@ mod tests {
|
|||||||
path: Arc::from(Path::new("b1.0")),
|
path: Arc::from(Path::new("b1.0")),
|
||||||
path_prefix: Arc::default(),
|
path_prefix: Arc::default(),
|
||||||
distance_to_relative_ancestor: 0,
|
distance_to_relative_ancestor: 0,
|
||||||
|
is_dir: false,
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
file_finder_sorted_output.sort_by(|a, b| b.cmp(a));
|
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: Arc::from(Path::new("a1.0")),
|
||||||
path_prefix: Arc::default(),
|
path_prefix: Arc::default(),
|
||||||
distance_to_relative_ancestor: 0,
|
distance_to_relative_ancestor: 0,
|
||||||
|
is_dir: false,
|
||||||
}),
|
}),
|
||||||
ProjectPanelOrdMatch(PathMatch {
|
ProjectPanelOrdMatch(PathMatch {
|
||||||
score: 1.0,
|
score: 1.0,
|
||||||
@ -1055,6 +1064,7 @@ mod tests {
|
|||||||
path: Arc::from(Path::new("b1.0")),
|
path: Arc::from(Path::new("b1.0")),
|
||||||
path_prefix: Arc::default(),
|
path_prefix: Arc::default(),
|
||||||
distance_to_relative_ancestor: 0,
|
distance_to_relative_ancestor: 0,
|
||||||
|
is_dir: false,
|
||||||
}),
|
}),
|
||||||
ProjectPanelOrdMatch(PathMatch {
|
ProjectPanelOrdMatch(PathMatch {
|
||||||
score: 1.0,
|
score: 1.0,
|
||||||
@ -1063,6 +1073,7 @@ mod tests {
|
|||||||
path: Arc::from(Path::new("c1.0")),
|
path: Arc::from(Path::new("c1.0")),
|
||||||
path_prefix: Arc::default(),
|
path_prefix: Arc::default(),
|
||||||
distance_to_relative_ancestor: 0,
|
distance_to_relative_ancestor: 0,
|
||||||
|
is_dir: false,
|
||||||
}),
|
}),
|
||||||
ProjectPanelOrdMatch(PathMatch {
|
ProjectPanelOrdMatch(PathMatch {
|
||||||
score: 0.5,
|
score: 0.5,
|
||||||
@ -1071,6 +1082,7 @@ mod tests {
|
|||||||
path: Arc::from(Path::new("a0.5")),
|
path: Arc::from(Path::new("a0.5")),
|
||||||
path_prefix: Arc::default(),
|
path_prefix: Arc::default(),
|
||||||
distance_to_relative_ancestor: 0,
|
distance_to_relative_ancestor: 0,
|
||||||
|
is_dir: false,
|
||||||
}),
|
}),
|
||||||
ProjectPanelOrdMatch(PathMatch {
|
ProjectPanelOrdMatch(PathMatch {
|
||||||
score: 0.5,
|
score: 0.5,
|
||||||
@ -1079,6 +1091,7 @@ mod tests {
|
|||||||
path: Arc::from(Path::new("b0.5")),
|
path: Arc::from(Path::new("b0.5")),
|
||||||
path_prefix: Arc::default(),
|
path_prefix: Arc::default(),
|
||||||
distance_to_relative_ancestor: 0,
|
distance_to_relative_ancestor: 0,
|
||||||
|
is_dir: false,
|
||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use futures::channel::oneshot;
|
use futures::channel::oneshot;
|
||||||
use fuzzy::StringMatchCandidate;
|
use fuzzy::StringMatchCandidate;
|
||||||
use picker::{Picker, PickerDelegate};
|
use picker::{Picker, PickerDelegate};
|
||||||
use project::{compare_paths, DirectoryLister};
|
use project::DirectoryLister;
|
||||||
use std::{
|
use std::{
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
sync::{
|
sync::{
|
||||||
@ -11,7 +11,7 @@ use std::{
|
|||||||
};
|
};
|
||||||
use ui::{prelude::*, LabelLike, ListItemSpacing};
|
use ui::{prelude::*, LabelLike, ListItemSpacing};
|
||||||
use ui::{ListItem, ViewContext};
|
use ui::{ListItem, ViewContext};
|
||||||
use util::maybe;
|
use util::{maybe, paths::compare_paths};
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
pub(crate) struct OpenPathPrompt;
|
pub(crate) struct OpenPathPrompt;
|
||||||
|
@ -445,6 +445,7 @@ mod tests {
|
|||||||
let lowercase_path = path.to_lowercase().chars().collect::<Vec<_>>();
|
let lowercase_path = path.to_lowercase().chars().collect::<Vec<_>>();
|
||||||
let char_bag = CharBag::from(lowercase_path.as_slice());
|
let char_bag = CharBag::from(lowercase_path.as_slice());
|
||||||
path_entries.push(PathMatchCandidate {
|
path_entries.push(PathMatchCandidate {
|
||||||
|
is_dir: false,
|
||||||
char_bag,
|
char_bag,
|
||||||
path: &path_arcs[i],
|
path: &path_arcs[i],
|
||||||
});
|
});
|
||||||
@ -468,6 +469,7 @@ mod tests {
|
|||||||
path: Arc::from(candidate.path),
|
path: Arc::from(candidate.path),
|
||||||
path_prefix: "".into(),
|
path_prefix: "".into(),
|
||||||
distance_to_relative_ancestor: usize::MAX,
|
distance_to_relative_ancestor: usize::MAX,
|
||||||
|
is_dir: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ use crate::{
|
|||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct PathMatchCandidate<'a> {
|
pub struct PathMatchCandidate<'a> {
|
||||||
|
pub is_dir: bool,
|
||||||
pub path: &'a Path,
|
pub path: &'a Path,
|
||||||
pub char_bag: CharBag,
|
pub char_bag: CharBag,
|
||||||
}
|
}
|
||||||
@ -24,6 +25,7 @@ pub struct PathMatch {
|
|||||||
pub worktree_id: usize,
|
pub worktree_id: usize,
|
||||||
pub path: Arc<Path>,
|
pub path: Arc<Path>,
|
||||||
pub path_prefix: Arc<str>,
|
pub path_prefix: Arc<str>,
|
||||||
|
pub is_dir: bool,
|
||||||
/// Number of steps removed from a shared parent with the relative path
|
/// Number of steps removed from a shared parent with the relative path
|
||||||
/// Used to order closer paths first in the search list
|
/// Used to order closer paths first in the search list
|
||||||
pub distance_to_relative_ancestor: usize,
|
pub distance_to_relative_ancestor: usize,
|
||||||
@ -119,6 +121,7 @@ pub fn match_fixed_path_set(
|
|||||||
score,
|
score,
|
||||||
worktree_id,
|
worktree_id,
|
||||||
positions: Vec::new(),
|
positions: Vec::new(),
|
||||||
|
is_dir: candidate.is_dir,
|
||||||
path: Arc::from(candidate.path),
|
path: Arc::from(candidate.path),
|
||||||
path_prefix: Arc::default(),
|
path_prefix: Arc::default(),
|
||||||
distance_to_relative_ancestor: usize::MAX,
|
distance_to_relative_ancestor: usize::MAX,
|
||||||
@ -195,6 +198,7 @@ pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
|
|||||||
worktree_id,
|
worktree_id,
|
||||||
positions: Vec::new(),
|
positions: Vec::new(),
|
||||||
path: Arc::from(candidate.path),
|
path: Arc::from(candidate.path),
|
||||||
|
is_dir: candidate.is_dir,
|
||||||
path_prefix: candidate_set.prefix(),
|
path_prefix: candidate_set.prefix(),
|
||||||
distance_to_relative_ancestor: relative_to.as_ref().map_or(
|
distance_to_relative_ancestor: relative_to.as_ref().map_or(
|
||||||
usize::MAX,
|
usize::MAX,
|
||||||
|
@ -1560,6 +1560,22 @@ impl CodeLabel {
|
|||||||
self.runs.push((start_ix..end_ix, highlight));
|
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 {
|
impl Ord for LanguageMatcher {
|
||||||
|
@ -69,7 +69,6 @@ snippet_provider.workspace = true
|
|||||||
terminal.workspace = true
|
terminal.workspace = true
|
||||||
text.workspace = true
|
text.workspace = true
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
unicase.workspace = true
|
|
||||||
which.workspace = true
|
which.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
@ -114,10 +114,9 @@ use task::{
|
|||||||
};
|
};
|
||||||
use terminals::Terminals;
|
use terminals::Terminals;
|
||||||
use text::{Anchor, BufferId, LineEnding};
|
use text::{Anchor, BufferId, LineEnding};
|
||||||
use unicase::UniCase;
|
|
||||||
use util::{
|
use util::{
|
||||||
debug_panic, defer, maybe, merge_json_value_into, parse_env_output, post_inc,
|
debug_panic, defer, maybe, merge_json_value_into, parse_env_output, paths::compare_paths,
|
||||||
NumericPrefixWithSuffix, ResultExt, TryFutureExt as _,
|
post_inc, ResultExt, TryFutureExt as _,
|
||||||
};
|
};
|
||||||
use worktree::{CreatedEntry, Snapshot, Traversal};
|
use worktree::{CreatedEntry, Snapshot, Traversal};
|
||||||
use worktree_store::{WorktreeStore, WorktreeStoreEvent};
|
use worktree_store::{WorktreeStore, WorktreeStoreEvent};
|
||||||
@ -413,6 +412,28 @@ pub struct InlayHint {
|
|||||||
pub resolve_state: ResolveState,
|
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
|
/// A completion provided by a language server
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Completion {
|
pub struct Completion {
|
||||||
@ -429,7 +450,7 @@ pub struct Completion {
|
|||||||
/// The raw completion provided by the language server.
|
/// The raw completion provided by the language server.
|
||||||
pub lsp_completion: lsp::CompletionItem,
|
pub lsp_completion: lsp::CompletionItem,
|
||||||
/// An optional callback to invoke when this completion is confirmed.
|
/// 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.
|
/// If true, the editor will show a new completion menu after this completion is confirmed.
|
||||||
pub show_new_completions_on_confirm: bool,
|
pub show_new_completions_on_confirm: bool,
|
||||||
}
|
}
|
||||||
@ -11011,10 +11032,12 @@ impl<'a> Iterator for PathMatchCandidateSetIter<'a> {
|
|||||||
fn next(&mut self) -> Option<Self::Item> {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
self.traversal.next().map(|entry| match entry.kind {
|
self.traversal.next().map(|entry| match entry.kind {
|
||||||
EntryKind::Dir => fuzzy::PathMatchCandidate {
|
EntryKind::Dir => fuzzy::PathMatchCandidate {
|
||||||
|
is_dir: true,
|
||||||
path: &entry.path,
|
path: &entry.path,
|
||||||
char_bag: CharBag::from_iter(entry.path.to_string_lossy().to_lowercase().chars()),
|
char_bag: CharBag::from_iter(entry.path.to_string_lossy().to_lowercase().chars()),
|
||||||
},
|
},
|
||||||
EntryKind::File(char_bag) => fuzzy::PathMatchCandidate {
|
EntryKind::File(char_bag) => fuzzy::PathMatchCandidate {
|
||||||
|
is_dir: false,
|
||||||
path: &entry.path,
|
path: &entry.path,
|
||||||
char_bag,
|
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::sync::OnceLock;
|
||||||
use std::{
|
use std::{
|
||||||
ffi::OsStr,
|
ffi::OsStr,
|
||||||
@ -8,6 +9,9 @@ use std::{
|
|||||||
use globset::{Glob, GlobSet, GlobSetBuilder};
|
use globset::{Glob, GlobSet, GlobSetBuilder};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use unicase::UniCase;
|
||||||
|
|
||||||
|
use crate::{maybe, NumericPrefixWithSuffix};
|
||||||
|
|
||||||
/// Returns the path to the user's home directory.
|
/// Returns the path to the user's home directory.
|
||||||
pub fn home_dir() -> &'static PathBuf {
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
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]
|
#[test]
|
||||||
fn path_with_position_parsing_positive() {
|
fn path_with_position_parsing_positive() {
|
||||||
let input_and_expected = [
|
let input_and_expected = [
|
||||||
|
Loading…
Reference in New Issue
Block a user