From 0cbab311a1db066c5ffef110e9504b6352da6534 Mon Sep 17 00:00:00 2001
From: AidanV <84053180+AidanV@users.noreply.github.com>
Date: Thu, 16 Oct 2025 21:19:01 -0700
Subject: [PATCH] vim: Add vim command filename autocomplete (#36332)
Release Notes:
- Adds filename autocomplete for vim commands:
- write
- edit
- split
- vsplit
- tabedit
- tabnew
- Makes command palette interceptor async
---------
Co-authored-by: Conrad Irwin
---
Cargo.lock | 2 +
crates/command_palette/src/command_palette.rs | 89 ++--
crates/command_palette_hooks/Cargo.toml | 1 +
.../src/command_palette_hooks.rs | 84 ++--
crates/vim/Cargo.toml | 1 +
crates/vim/src/command.rs | 423 +++++++++++++-----
crates/vim/src/state.rs | 10 +-
7 files changed, 426 insertions(+), 184 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index 22c5eeccb7..20cae72db4 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3755,6 +3755,7 @@ dependencies = [
"collections",
"derive_more",
"gpui",
+ "workspace",
"workspace-hack",
]
@@ -18743,6 +18744,7 @@ dependencies = [
"editor",
"env_logger 0.11.8",
"futures 0.3.31",
+ "fuzzy",
"git_ui",
"gpui",
"indoc",
diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs
index 227d246f04..e718fe9929 100644
--- a/crates/command_palette/src/command_palette.rs
+++ b/crates/command_palette/src/command_palette.rs
@@ -9,7 +9,8 @@ use std::{
use client::parse_zed_link;
use command_palette_hooks::{
- CommandInterceptResult, CommandPaletteFilter, CommandPaletteInterceptor,
+ CommandInterceptItem, CommandInterceptResult, CommandPaletteFilter,
+ GlobalCommandPaletteInterceptor,
};
use fuzzy::{StringMatch, StringMatchCandidate};
@@ -81,14 +82,17 @@ impl CommandPalette {
let Some(previous_focus_handle) = window.focused(cx) else {
return;
};
+
+ let entity = cx.weak_entity();
workspace.toggle_modal(window, cx, move |window, cx| {
- CommandPalette::new(previous_focus_handle, query, window, cx)
+ CommandPalette::new(previous_focus_handle, query, entity, window, cx)
});
}
fn new(
previous_focus_handle: FocusHandle,
query: &str,
+ entity: WeakEntity,
window: &mut Window,
cx: &mut Context,
) -> Self {
@@ -109,8 +113,12 @@ impl CommandPalette {
})
.collect();
- let delegate =
- CommandPaletteDelegate::new(cx.entity().downgrade(), commands, previous_focus_handle);
+ let delegate = CommandPaletteDelegate::new(
+ cx.entity().downgrade(),
+ entity,
+ commands,
+ previous_focus_handle,
+ );
let picker = cx.new(|cx| {
let picker = Picker::uniform_list(delegate, window, cx);
@@ -146,6 +154,7 @@ impl Render for CommandPalette {
pub struct CommandPaletteDelegate {
latest_query: String,
command_palette: WeakEntity,
+ workspace: WeakEntity,
all_commands: Vec,
commands: Vec,
matches: Vec,
@@ -153,7 +162,7 @@ pub struct CommandPaletteDelegate {
previous_focus_handle: FocusHandle,
updating_matches: Option<(
Task<()>,
- postage::dispatch::Receiver<(Vec, Vec)>,
+ postage::dispatch::Receiver<(Vec, Vec, CommandInterceptResult)>,
)>,
}
@@ -174,11 +183,13 @@ impl Clone for Command {
impl CommandPaletteDelegate {
fn new(
command_palette: WeakEntity,
+ workspace: WeakEntity,
commands: Vec,
previous_focus_handle: FocusHandle,
) -> Self {
Self {
command_palette,
+ workspace,
all_commands: commands.clone(),
matches: vec![],
commands,
@@ -194,30 +205,19 @@ impl CommandPaletteDelegate {
query: String,
mut commands: Vec,
mut matches: Vec,
- cx: &mut Context>,
+ intercept_result: CommandInterceptResult,
+ _: &mut Context>,
) {
self.updating_matches.take();
- self.latest_query = query.clone();
-
- let mut intercept_results = CommandPaletteInterceptor::try_global(cx)
- .map(|interceptor| interceptor.intercept(&query, cx))
- .unwrap_or_default();
-
- if parse_zed_link(&query, cx).is_some() {
- intercept_results = vec![CommandInterceptResult {
- action: OpenZedUrl { url: query.clone() }.boxed_clone(),
- string: query,
- positions: vec![],
- }]
- }
+ self.latest_query = query;
let mut new_matches = Vec::new();
- for CommandInterceptResult {
+ for CommandInterceptItem {
action,
string,
positions,
- } in intercept_results
+ } in intercept_result.results
{
if let Some(idx) = matches
.iter()
@@ -236,7 +236,9 @@ impl CommandPaletteDelegate {
score: 0.0,
})
}
- new_matches.append(&mut matches);
+ if !intercept_result.exclusive {
+ new_matches.append(&mut matches);
+ }
self.commands = commands;
self.matches = new_matches;
if self.matches.is_empty() {
@@ -295,12 +297,22 @@ impl PickerDelegate for CommandPaletteDelegate {
if let Some(alias) = settings.command_aliases.get(&query) {
query = alias.to_string();
}
+
+ let workspace = self.workspace.clone();
+
+ let intercept_task = GlobalCommandPaletteInterceptor::intercept(&query, workspace, cx);
+
let (mut tx, mut rx) = postage::dispatch::channel(1);
+
+ let query_str = query.as_str();
+ let is_zed_link = parse_zed_link(query_str, cx).is_some();
+
let task = cx.background_spawn({
let mut commands = self.all_commands.clone();
let hit_counts = self.hit_counts();
let executor = cx.background_executor().clone();
- let query = normalize_action_query(query.as_str());
+ let query = normalize_action_query(query_str);
+ let query_for_link = query_str.to_string();
async move {
commands.sort_by_key(|action| {
(
@@ -326,13 +338,34 @@ impl PickerDelegate for CommandPaletteDelegate {
)
.await;
- tx.send((commands, matches)).await.log_err();
+ let intercept_result = if is_zed_link {
+ CommandInterceptResult {
+ results: vec![CommandInterceptItem {
+ action: OpenZedUrl {
+ url: query_for_link.clone(),
+ }
+ .boxed_clone(),
+ string: query_for_link,
+ positions: vec![],
+ }],
+ exclusive: false,
+ }
+ } else if let Some(task) = intercept_task {
+ task.await
+ } else {
+ CommandInterceptResult::default()
+ };
+
+ tx.send((commands, matches, intercept_result))
+ .await
+ .log_err();
}
});
+
self.updating_matches = Some((task, rx.clone()));
cx.spawn_in(window, async move |picker, cx| {
- let Some((commands, matches)) = rx.recv().await else {
+ let Some((commands, matches, intercept_result)) = rx.recv().await else {
return;
};
@@ -340,7 +373,7 @@ impl PickerDelegate for CommandPaletteDelegate {
.update(cx, |picker, cx| {
picker
.delegate
- .matches_updated(query, commands, matches, cx)
+ .matches_updated(query, commands, matches, intercept_result, cx)
})
.log_err();
})
@@ -361,8 +394,8 @@ impl PickerDelegate for CommandPaletteDelegate {
.background_executor()
.block_with_timeout(duration, rx.clone().recv())
{
- Ok(Some((commands, matches))) => {
- self.matches_updated(query, commands, matches, cx);
+ Ok(Some((commands, matches, interceptor_result))) => {
+ self.matches_updated(query, commands, matches, interceptor_result, cx);
true
}
_ => {
diff --git a/crates/command_palette_hooks/Cargo.toml b/crates/command_palette_hooks/Cargo.toml
index dd0b44c57d..872d005691 100644
--- a/crates/command_palette_hooks/Cargo.toml
+++ b/crates/command_palette_hooks/Cargo.toml
@@ -17,3 +17,4 @@ collections.workspace = true
derive_more.workspace = true
gpui.workspace = true
workspace-hack.workspace = true
+workspace.workspace = true
diff --git a/crates/command_palette_hooks/src/command_palette_hooks.rs b/crates/command_palette_hooks/src/command_palette_hooks.rs
index 4923b811c5..bd8f9375b7 100644
--- a/crates/command_palette_hooks/src/command_palette_hooks.rs
+++ b/crates/command_palette_hooks/src/command_palette_hooks.rs
@@ -2,16 +2,16 @@
#![deny(missing_docs)]
-use std::any::TypeId;
+use std::{any::TypeId, rc::Rc};
use collections::HashSet;
use derive_more::{Deref, DerefMut};
-use gpui::{Action, App, BorrowAppContext, Global};
+use gpui::{Action, App, BorrowAppContext, Global, Task, WeakEntity};
+use workspace::Workspace;
/// Initializes the command palette hooks.
pub fn init(cx: &mut App) {
cx.set_global(GlobalCommandPaletteFilter::default());
- cx.set_global(GlobalCommandPaletteInterceptor::default());
}
/// A filter for the command palette.
@@ -94,7 +94,7 @@ impl CommandPaletteFilter {
/// The result of intercepting a command palette command.
#[derive(Debug)]
-pub struct CommandInterceptResult {
+pub struct CommandInterceptItem {
/// The action produced as a result of the interception.
pub action: Box,
/// The display string to show in the command palette for this result.
@@ -104,50 +104,50 @@ pub struct CommandInterceptResult {
pub positions: Vec,
}
-/// An interceptor for the command palette.
-#[derive(Default)]
-pub struct CommandPaletteInterceptor(
- Option Vec>>,
-);
+/// The result of intercepting a command palette command.
+#[derive(Default, Debug)]
+pub struct CommandInterceptResult {
+ /// The items
+ pub results: Vec,
+ /// Whether or not to continue to show the normal matches
+ pub exclusive: bool,
+}
-#[derive(Default)]
-struct GlobalCommandPaletteInterceptor(CommandPaletteInterceptor);
+/// An interceptor for the command palette.
+#[derive(Clone)]
+pub struct GlobalCommandPaletteInterceptor(
+ Rc, &mut App) -> Task>,
+);
impl Global for GlobalCommandPaletteInterceptor {}
-impl CommandPaletteInterceptor {
- /// Returns the global [`CommandPaletteInterceptor`], if one is set.
- pub fn try_global(cx: &App) -> Option<&CommandPaletteInterceptor> {
- cx.try_global::()
- .map(|interceptor| &interceptor.0)
- }
-
- /// Updates the global [`CommandPaletteInterceptor`] using the given closure.
- pub fn update_global(cx: &mut App, update: F) -> R
- where
- F: FnOnce(&mut Self, &mut App) -> R,
- {
- cx.update_global(|this: &mut GlobalCommandPaletteInterceptor, cx| update(&mut this.0, cx))
- }
-
- /// Intercepts the given query from the command palette.
- pub fn intercept(&self, query: &str, cx: &App) -> Vec {
- if let Some(handler) = self.0.as_ref() {
- (handler)(query, cx)
- } else {
- Vec::new()
- }
- }
-
- /// Clears the global interceptor.
- pub fn clear(&mut self) {
- self.0 = None;
- }
-
+impl GlobalCommandPaletteInterceptor {
/// Sets the global interceptor.
///
/// This will override the previous interceptor, if it exists.
- pub fn set(&mut self, handler: Box Vec>) {
- self.0 = Some(handler);
+ pub fn set(
+ cx: &mut App,
+ interceptor: impl Fn(&str, WeakEntity, &mut App) -> Task
+ + 'static,
+ ) {
+ cx.set_global(Self(Rc::new(interceptor)));
+ }
+
+ /// Clears the global interceptor.
+ pub fn clear(cx: &mut App) {
+ if cx.has_global::() {
+ cx.remove_global::();
+ }
+ }
+
+ /// Intercepts the given query from the command palette.
+ pub fn intercept(
+ query: &str,
+ workspace: WeakEntity,
+ cx: &mut App,
+ ) -> Option> {
+ let interceptor = cx.try_global::()?;
+ let handler = interceptor.0.clone();
+ Some(handler(query, workspace, cx))
}
}
diff --git a/crates/vim/Cargo.toml b/crates/vim/Cargo.toml
index ad84eecd91..8550550c9e 100644
--- a/crates/vim/Cargo.toml
+++ b/crates/vim/Cargo.toml
@@ -26,6 +26,7 @@ db.workspace = true
editor.workspace = true
env_logger.workspace = true
futures.workspace = true
+fuzzy.workspace = true
gpui.workspace = true
itertools.workspace = true
language.workspace = true
diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs
index ef19d41ed8..f5a170055d 100644
--- a/crates/vim/src/command.rs
+++ b/crates/vim/src/command.rs
@@ -1,13 +1,15 @@
use anyhow::{Result, anyhow};
use collections::{HashMap, HashSet};
-use command_palette_hooks::CommandInterceptResult;
+use command_palette_hooks::{CommandInterceptItem, CommandInterceptResult};
use editor::{
Bias, Editor, EditorSettings, SelectionEffects, ToPoint,
actions::{SortLinesCaseInsensitive, SortLinesCaseSensitive},
display_map::ToDisplayPoint,
};
use futures::AsyncWriteExt as _;
-use gpui::{Action, App, AppContext as _, Context, Global, Keystroke, Task, Window, actions};
+use gpui::{
+ Action, App, AppContext as _, Context, Global, Keystroke, Task, WeakEntity, Window, actions,
+};
use itertools::Itertools;
use language::Point;
use multi_buffer::MultiBufferRow;
@@ -20,7 +22,7 @@ use settings::{Settings, SettingsStore};
use std::{
iter::Peekable,
ops::{Deref, Range},
- path::Path,
+ path::{Path, PathBuf},
process::Stdio,
str::Chars,
sync::OnceLock,
@@ -28,8 +30,12 @@ use std::{
};
use task::{HideStrategy, RevealStrategy, SpawnInTerminal, TaskId};
use ui::ActiveTheme;
-use util::{ResultExt, rel_path::RelPath};
-use workspace::{Item, SaveIntent, notifications::NotifyResultExt};
+use util::{
+ ResultExt,
+ paths::PathStyle,
+ rel_path::{RelPath, RelPathBuf},
+};
+use workspace::{Item, SaveIntent, Workspace, notifications::NotifyResultExt};
use workspace::{SplitDirection, notifications::DetachAndPromptErr};
use zed_actions::{OpenDocs, RevealTarget};
@@ -85,7 +91,7 @@ pub enum VimOption {
}
impl VimOption {
- fn possible_commands(query: &str) -> Vec {
+ fn possible_commands(query: &str) -> Vec {
let mut prefix_of_options = Vec::new();
let mut options = query.split(" ").collect::>();
let prefix = options.pop().unwrap_or_default();
@@ -102,7 +108,7 @@ impl VimOption {
let mut options = prefix_of_options.clone();
options.push(possible);
- CommandInterceptResult {
+ CommandInterceptItem {
string: format!(
":set {}",
options.iter().map(|opt| opt.to_string()).join(" ")
@@ -725,6 +731,13 @@ struct VimCommand {
>,
>,
has_count: bool,
+ has_filename: bool,
+}
+
+struct ParsedQuery {
+ args: String,
+ has_bang: bool,
+ has_space: bool,
}
impl VimCommand {
@@ -760,6 +773,15 @@ impl VimCommand {
self
}
+ fn filename(
+ mut self,
+ f: impl Fn(Box, String) -> Option> + Send + Sync + 'static,
+ ) -> Self {
+ self.args = Some(Box::new(f));
+ self.has_filename = true;
+ self
+ }
+
fn range(
mut self,
f: impl Fn(Box, &CommandRange) -> Option> + Send + Sync + 'static,
@@ -773,14 +795,80 @@ impl VimCommand {
self
}
- fn parse(
- &self,
- query: &str,
- range: &Option,
- cx: &App,
- ) -> Option> {
+ fn generate_filename_completions(
+ parsed_query: &ParsedQuery,
+ workspace: WeakEntity,
+ cx: &mut App,
+ ) -> Task> {
+ let ParsedQuery {
+ args,
+ has_bang: _,
+ has_space: _,
+ } = parsed_query;
+ let Some(workspace) = workspace.upgrade() else {
+ return Task::ready(Vec::new());
+ };
+
+ let (task, args_path) = workspace.update(cx, |workspace, cx| {
+ let prefix = workspace
+ .project()
+ .read(cx)
+ .visible_worktrees(cx)
+ .map(|worktree| worktree.read(cx).abs_path().to_path_buf())
+ .next()
+ .or_else(std::env::home_dir)
+ .unwrap_or_else(|| PathBuf::from(""));
+
+ let rel_path = match RelPath::new(Path::new(&args), PathStyle::local()) {
+ Ok(path) => path.to_rel_path_buf(),
+ Err(_) => {
+ return (Task::ready(Ok(Vec::new())), RelPathBuf::new());
+ }
+ };
+
+ let rel_path = if args.ends_with(PathStyle::local().separator()) {
+ rel_path
+ } else {
+ rel_path
+ .parent()
+ .map(|rel_path| rel_path.to_rel_path_buf())
+ .unwrap_or(RelPathBuf::new())
+ };
+
+ let task = workspace.project().update(cx, |project, cx| {
+ let path = prefix
+ .join(rel_path.as_std_path())
+ .to_string_lossy()
+ .to_string();
+ project.list_directory(path, cx)
+ });
+
+ (task, rel_path)
+ });
+
+ cx.background_spawn(async move {
+ let directories = task.await.unwrap_or_default();
+ directories
+ .iter()
+ .map(|dir| {
+ let path = RelPath::new(dir.path.as_path(), PathStyle::local())
+ .map(|cow| cow.into_owned())
+ .unwrap_or(RelPathBuf::new());
+ let mut path_string = args_path
+ .join(&path)
+ .display(PathStyle::local())
+ .to_string();
+ if dir.is_dir {
+ path_string.push_str(PathStyle::local().separator());
+ }
+ path_string
+ })
+ .collect()
+ })
+ }
+
+ fn get_parsed_query(&self, query: String) -> Option {
let rest = query
- .to_string()
.strip_prefix(self.prefix)?
.to_string()
.chars()
@@ -789,6 +877,7 @@ impl VimCommand {
.filter_map(|e| e.left())
.collect::();
let has_bang = rest.starts_with('!');
+ let has_space = rest.starts_with("! ") || rest.starts_with(' ');
let args = if has_bang {
rest.strip_prefix('!')?.trim().to_string()
} else if rest.is_empty() {
@@ -796,7 +885,24 @@ impl VimCommand {
} else {
rest.strip_prefix(' ')?.trim().to_string()
};
+ Some(ParsedQuery {
+ args,
+ has_bang,
+ has_space,
+ })
+ }
+ fn parse(
+ &self,
+ query: &str,
+ range: &Option,
+ cx: &App,
+ ) -> Option> {
+ let ParsedQuery {
+ args,
+ has_bang,
+ has_space: _,
+ } = self.get_parsed_query(query.to_string())?;
let action = if has_bang && self.bang_action.is_some() {
self.bang_action.as_ref().unwrap().boxed_clone()
} else if let Some(action) = self.action.as_ref() {
@@ -1056,18 +1162,43 @@ fn generate_commands(_: &App) -> Vec {
.bang(workspace::Save {
save_intent: Some(SaveIntent::Overwrite),
})
- .args(|action, args| {
+ .filename(|action, filename| {
Some(
VimSave {
save_intent: action
.as_any()
.downcast_ref::()
.and_then(|action| action.save_intent),
- filename: args,
+ filename,
}
.boxed_clone(),
)
}),
+ VimCommand::new(("e", "dit"), editor::actions::ReloadFile)
+ .bang(editor::actions::ReloadFile)
+ .filename(|_, filename| Some(VimEdit { filename }.boxed_clone())),
+ VimCommand::new(("sp", "lit"), workspace::SplitHorizontal).filename(|_, filename| {
+ Some(
+ VimSplit {
+ vertical: false,
+ filename,
+ }
+ .boxed_clone(),
+ )
+ }),
+ VimCommand::new(("vs", "plit"), workspace::SplitVertical).filename(|_, filename| {
+ Some(
+ VimSplit {
+ vertical: true,
+ filename,
+ }
+ .boxed_clone(),
+ )
+ }),
+ VimCommand::new(("tabe", "dit"), workspace::NewFile)
+ .filename(|_action, filename| Some(VimEdit { filename }.boxed_clone())),
+ VimCommand::new(("tabnew", ""), workspace::NewFile)
+ .filename(|_action, filename| Some(VimEdit { filename }.boxed_clone())),
VimCommand::new(
("q", "uit"),
workspace::CloseActiveItem {
@@ -1164,24 +1295,6 @@ fn generate_commands(_: &App) -> Vec {
save_intent: Some(SaveIntent::Overwrite),
}),
VimCommand::new(("cq", "uit"), zed_actions::Quit),
- VimCommand::new(("sp", "lit"), workspace::SplitHorizontal).args(|_, args| {
- Some(
- VimSplit {
- vertical: false,
- filename: args,
- }
- .boxed_clone(),
- )
- }),
- VimCommand::new(("vs", "plit"), workspace::SplitVertical).args(|_, args| {
- Some(
- VimSplit {
- vertical: true,
- filename: args,
- }
- .boxed_clone(),
- )
- }),
VimCommand::new(
("bd", "elete"),
workspace::CloseActiveItem {
@@ -1224,10 +1337,6 @@ fn generate_commands(_: &App) -> Vec {
VimCommand::str(("ls", ""), "tab_switcher::ToggleAll"),
VimCommand::new(("new", ""), workspace::NewFileSplitHorizontal),
VimCommand::new(("vne", "w"), workspace::NewFileSplitVertical),
- VimCommand::new(("tabe", "dit"), workspace::NewFile)
- .args(|_action, args| Some(VimEdit { filename: args }.boxed_clone())),
- VimCommand::new(("tabnew", ""), workspace::NewFile)
- .args(|_action, args| Some(VimEdit { filename: args }.boxed_clone())),
VimCommand::new(("tabn", "ext"), workspace::ActivateNextItem).count(),
VimCommand::new(("tabp", "revious"), workspace::ActivatePreviousItem).count(),
VimCommand::new(("tabN", "ext"), workspace::ActivatePreviousItem).count(),
@@ -1327,9 +1436,6 @@ fn generate_commands(_: &App) -> Vec {
VimCommand::new(("$", ""), EndOfDocument),
VimCommand::new(("%", ""), EndOfDocument),
VimCommand::new(("0", ""), StartOfDocument),
- VimCommand::new(("e", "dit"), editor::actions::ReloadFile)
- .bang(editor::actions::ReloadFile)
- .args(|_, args| Some(VimEdit { filename: args }.boxed_clone())),
VimCommand::new(("ex", ""), editor::actions::ReloadFile).bang(editor::actions::ReloadFile),
VimCommand::new(("cpp", "link"), editor::actions::CopyPermalinkToLine).range(act_on_range),
VimCommand::str(("opt", "ions"), "zed::OpenDefaultSettings"),
@@ -1383,18 +1489,30 @@ fn wrap_count(action: Box, range: &CommandRange) -> Option Vec {
- // NOTE: We also need to support passing arguments to commands like :w
- // (ideally with filename autocompletion).
+pub fn command_interceptor(
+ mut input: &str,
+ workspace: WeakEntity,
+ cx: &mut App,
+) -> Task {
while input.starts_with(':') {
input = &input[1..];
}
let (range, query) = VimCommand::parse_range(input);
let range_prefix = input[0..(input.len() - query.len())].to_string();
- let query = query.as_str().trim();
+ let has_trailing_space = query.ends_with(" ");
+ let mut query = query.as_str().trim();
- let action = if range.is_some() && query.is_empty() {
+ let on_matching_lines = (query.starts_with('g') || query.starts_with('v'))
+ .then(|| {
+ let (pattern, range, search, invert) = OnMatchingLines::parse(query, &range)?;
+ let start_idx = query.len() - pattern.len();
+ query = query[start_idx..].trim();
+ Some((range, search, invert))
+ })
+ .flatten();
+
+ let mut action = if range.is_some() && query.is_empty() {
Some(
GoToLine {
range: range.clone().unwrap(),
@@ -1418,7 +1536,10 @@ pub fn command_interceptor(mut input: &str, cx: &App) -> Vec Vec = positions.iter().map(|&pos| pos + offset).collect();
+ positions.splice(0..0, no_args_positions.clone());
+ let string = format!("{display_string} {string}");
+ let action = match cx
+ .update(|cx| commands(cx).get(cmd_idx)?.parse(&string[1..], &range, cx))
+ {
+ Ok(Some(action)) => action,
+ _ => continue,
+ };
+ results.push(CommandInterceptItem {
+ action,
+ string,
+ positions,
+ });
+ }
+ CommandInterceptResult {
+ results,
+ exclusive: true,
+ }
+ })
+ } else {
+ Task::ready(CommandInterceptResult {
+ results,
+ exclusive: false,
+ })
}
- Vec::default()
}
fn generate_positions(string: &str, query: &str) -> Vec {
@@ -1530,19 +1731,40 @@ impl OnMatchingLines {
// but we do flip \( and \) to ( and ) (and vice-versa) in the pattern,
// and convert \0..\9 to $0..$9 in the replacement so that common idioms work.
pub(crate) fn parse(
- mut chars: Peekable,
- invert: bool,
- range: CommandRange,
- cx: &App,
- ) -> Option {
- let delimiter = chars.next().filter(|c| {
+ query: &str,
+ range: &Option,
+ ) -> Option<(String, CommandRange, String, bool)> {
+ let mut global = "global".chars().peekable();
+ let mut query_chars = query.chars().peekable();
+ let mut invert = false;
+ if query_chars.peek() == Some(&'v') {
+ invert = true;
+ query_chars.next();
+ }
+ while global
+ .peek()
+ .is_some_and(|char| Some(char) == query_chars.peek())
+ {
+ global.next();
+ query_chars.next();
+ }
+ if !invert && query_chars.peek() == Some(&'!') {
+ invert = true;
+ query_chars.next();
+ }
+ let range = range.clone().unwrap_or(CommandRange {
+ start: Position::Line { row: 0, offset: 0 },
+ end: Some(Position::LastLine { offset: 0 }),
+ });
+
+ let delimiter = query_chars.next().filter(|c| {
!c.is_alphanumeric() && *c != '"' && *c != '|' && *c != '\'' && *c != '!'
})?;
let mut search = String::new();
let mut escaped = false;
- for c in chars.by_ref() {
+ for c in query_chars.by_ref() {
if escaped {
escaped = false;
// unescape escaped parens
@@ -1563,21 +1785,7 @@ impl OnMatchingLines {
}
}
- let command: String = chars.collect();
-
- let action = WrappedAction(
- command_interceptor(&command, cx)
- .first()?
- .action
- .boxed_clone(),
- );
-
- Some(Self {
- range,
- search,
- invert,
- action,
- })
+ Some((query_chars.collect::(), range, search, invert))
}
pub fn run(&self, vim: &mut Vim, window: &mut Window, cx: &mut Context) {
@@ -2184,7 +2392,8 @@ mod test {
assert_eq!(fs.load(path).await.unwrap().replace("\r\n", "\n"), "oops\n");
assert!(!cx.has_pending_prompt());
- cx.simulate_keystrokes(": w ! enter");
+ cx.simulate_keystrokes(": w !");
+ cx.simulate_keystrokes("enter");
assert!(!cx.has_pending_prompt());
assert_eq!(fs.load(path).await.unwrap().replace("\r\n", "\n"), "@@\n");
}
@@ -2342,7 +2551,7 @@ mod test {
}
#[gpui::test]
- async fn test_w_command(cx: &mut TestAppContext) {
+ async fn test_command_write_filename(cx: &mut TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
cx.workspace(|workspace, _, cx| {
diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs
index 88a100fc2a..98a2fe0b17 100644
--- a/crates/vim/src/state.rs
+++ b/crates/vim/src/state.rs
@@ -6,7 +6,7 @@ use crate::{ToggleMarksView, ToggleRegistersView, UseSystemClipboard, Vim, VimAd
use crate::{motion::Motion, object::Object};
use anyhow::Result;
use collections::HashMap;
-use command_palette_hooks::{CommandPaletteFilter, CommandPaletteInterceptor};
+use command_palette_hooks::{CommandPaletteFilter, GlobalCommandPaletteInterceptor};
use db::{
sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection},
sqlez_macros::sql,
@@ -718,9 +718,7 @@ impl VimGlobals {
CommandPaletteFilter::update_global(cx, |filter, _| {
filter.show_namespace(Vim::NAMESPACE);
});
- CommandPaletteInterceptor::update_global(cx, |interceptor, _| {
- interceptor.set(Box::new(command_interceptor));
- });
+ GlobalCommandPaletteInterceptor::set(cx, command_interceptor);
for window in cx.windows() {
if let Some(workspace) = window.downcast::() {
workspace
@@ -735,9 +733,7 @@ impl VimGlobals {
} else {
KeyBinding::set_vim_mode(cx, false);
*Vim::globals(cx) = VimGlobals::default();
- CommandPaletteInterceptor::update_global(cx, |interceptor, _| {
- interceptor.clear();
- });
+ GlobalCommandPaletteInterceptor::clear(cx);
CommandPaletteFilter::update_global(cx, |filter, _| {
filter.hide_namespace(Vim::NAMESPACE);
});