Cargo.lock 🔗
@@ -8085,7 +8085,6 @@ dependencies = [
"tempfile",
"terminal",
"text",
- "unicase",
"unindent",
"util",
"which 6.0.2",
Mikayla Maki created
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
Cargo.lock | 1
assets/keymaps/default-linux.json | 2
assets/keymaps/default-macos.json | 2
crates/assistant/src/slash_command.rs | 64 +-
crates/assistant/src/slash_command/diagnostics_command.rs | 91 ++-
crates/assistant/src/slash_command/docs_command.rs | 13
crates/assistant/src/slash_command/file_command.rs | 46 +
crates/assistant/src/slash_command/prompt_command.rs | 2
crates/assistant/src/slash_command/tabs_command.rs | 4
crates/assistant/src/slash_command/terminal_command.rs | 2
crates/assistant_slash_command/src/assistant_slash_command.rs | 2
crates/editor/src/actions.rs | 7
crates/editor/src/editor.rs | 23
crates/editor/src/element.rs | 7
crates/extension/src/extension_slash_command.rs | 2
crates/file_finder/src/file_finder.rs | 13
crates/file_finder/src/open_path_prompt.rs | 4
crates/fuzzy/src/matcher.rs | 2
crates/fuzzy/src/paths.rs | 4
crates/language/src/language.rs | 16
crates/project/Cargo.toml | 1
crates/project/src/project.rs | 114 +---
crates/util/src/paths.rs | 82 +++
23 files changed, 320 insertions(+), 184 deletions(-)
@@ -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,
+ });
};
- 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
+ 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();
}
- },
- 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.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 {
- write!(label, " {} warnings", summary.warning_count)
- .unwrap();
+ label.push_str(",");
}
}
- label.into()
+ if summary.warning_count > 0 {
+ write!(label, " {} warnings", summary.warning_count).unwrap();
+ }
}
- PlaceholderType::File(file_path) => file_path.into(),
- PlaceholderType::Diagnostic(_, message) => message.into(),
- },
- })
- .collect(),
+
+ label.into()
+ }
+ PlaceholderType::File(file_path) => file_path.into(),
+ PlaceholderType::Diagnostic(_, message) => message.into(),
+ },
+ })
+ .collect();
+
+ Ok(SlashCommandOutput {
+ text,
+ 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 = [