diff --git a/Cargo.lock b/Cargo.lock index 22c5eeccb71451acf1cb7cbd9313d248dabb7a17..20cae72db4edcd2d27fdfe03bcaced18f974761f 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 227d246f04cecf8a5c58f2361d0b543ff678eac6..e718fe99297802ea90d425aef5063f3b55a86579 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 dd0b44c57dafe0266737e6c589f8cc6f763f2f4d..872d005691cd64290d75e126a59a5cdf1a109572 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 4923b811c570ea9413ff1e9c94aba4fbf1205a2b..bd8f9375b77ec9372a1657724a41dcb851537ece 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, } +/// 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, +} + /// An interceptor for the command palette. -#[derive(Default)] -pub struct CommandPaletteInterceptor( - Option Vec>>, +#[derive(Clone)] +pub struct GlobalCommandPaletteInterceptor( + Rc, &mut App) -> Task>, ); -#[derive(Default)] -struct GlobalCommandPaletteInterceptor(CommandPaletteInterceptor); - 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() - } +impl GlobalCommandPaletteInterceptor { + /// Sets the global interceptor. + /// + /// This will override the previous interceptor, if it exists. + 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(&mut self) { - self.0 = None; + pub fn clear(cx: &mut App) { + if cx.has_global::() { + cx.remove_global::(); + } } - /// 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); + /// 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 ad84eecd91ddfc4b300b437936aba0ac21b4e41c..8550550c9eae2bcf8e0f8c3a1cc65a48e7031ad2 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 ef19d41ed88f7f6a9dfc64521041a41d2238da31..f5a170055d3604f023c4c31fa5a413a8e52cbc66 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 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 action = if range.is_some() && query.is_empty() { + 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 88a100fc2abb90005256548395959c596167c148..98a2fe0b17fb27bc7201fb82a904b0d6c4ade1cc 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); });