From ea3a1745f56ed819aca7c3b235e123a5bedff0b9 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 7 Sep 2023 22:48:01 -0600 Subject: [PATCH] Add vim-specific interactions to command This mostly adds the commonly requested set (:wq and friends) and a few that I use frequently : to go to a line number :vsp / :sp to create a split :cn / :cp to go to diagnostics --- assets/keymaps/vim.json | 1 + crates/command_palette/src/command_palette.rs | 47 +++- crates/vim/src/command.rs | 219 ++++++++++++++++++ crates/vim/src/vim.rs | 9 + crates/workspace/src/pane.rs | 32 ++- crates/workspace/src/workspace.rs | 103 ++++++-- crates/zed/src/menus.rs | 21 +- 7 files changed, 406 insertions(+), 26 deletions(-) create mode 100644 crates/vim/src/command.rs diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 9e69240d27..1a6a752e23 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -18,6 +18,7 @@ } } ], + ":": "command_palette::Toggle", "h": "vim::Left", "left": "vim::Left", "backspace": "vim::Backspace", diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index 4f9bb231ce..06f76bbc5c 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -18,6 +18,15 @@ actions!(command_palette, [Toggle]); pub type CommandPalette = Picker; +pub type CommandPaletteInterceptor = + Box Option>; + +pub struct CommandInterceptResult { + pub action: Box, + pub string: String, + pub positions: Vec, +} + pub struct CommandPaletteDelegate { actions: Vec, matches: Vec, @@ -136,7 +145,7 @@ impl PickerDelegate for CommandPaletteDelegate { char_bag: command.name.chars().collect(), }) .collect::>(); - let matches = if query.is_empty() { + let mut matches = if query.is_empty() { candidates .into_iter() .enumerate() @@ -158,6 +167,40 @@ impl PickerDelegate for CommandPaletteDelegate { ) .await }; + let intercept_result = cx.read(|cx| { + if cx.has_global::() { + cx.global::()(&query, cx) + } else { + None + } + }); + if let Some(CommandInterceptResult { + action, + string, + positions, + }) = intercept_result + { + if let Some(idx) = matches + .iter() + .position(|m| actions[m.candidate_id].action.id() == action.id()) + { + matches.remove(idx); + } + actions.push(Command { + name: string.clone(), + action, + keystrokes: vec![], + }); + matches.insert( + 0, + StringMatch { + candidate_id: actions.len() - 1, + string, + positions, + score: 0.0, + }, + ) + } picker .update(&mut cx, |picker, _| { let delegate = picker.delegate_mut(); @@ -254,7 +297,7 @@ impl PickerDelegate for CommandPaletteDelegate { } } -fn humanize_action_name(name: &str) -> String { +pub fn humanize_action_name(name: &str) -> String { let capacity = name.len() + name.chars().filter(|c| c.is_uppercase()).count(); let mut result = String::with_capacity(capacity); for char in name.chars() { diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs new file mode 100644 index 0000000000..a1f4777ec6 --- /dev/null +++ b/crates/vim/src/command.rs @@ -0,0 +1,219 @@ +use command_palette::{humanize_action_name, CommandInterceptResult}; +use gpui::{actions, impl_actions, Action, AppContext, AsyncAppContext, ViewContext}; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; +use workspace::{SaveBehavior, Workspace}; + +use crate::{ + motion::{motion, Motion}, + normal::JoinLines, + Vim, +}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct GoToLine { + pub line: u32, +} + +impl_actions!(vim, [GoToLine]); + +pub fn init(cx: &mut AppContext) { + cx.add_action(|_: &mut Workspace, action: &GoToLine, cx| { + Vim::update(cx, |vim, cx| { + vim.push_operator(crate::state::Operator::Number(action.line as usize), cx) + }); + motion(Motion::StartOfDocument, cx) + }); +} + +pub fn command_interceptor(mut query: &str, _: &AppContext) -> Option { + while query.starts_with(":") { + query = &query[1..]; + } + + let (name, action) = match query { + // :w + "w" | "wr" | "wri" | "writ" | "write" => ( + "write", + workspace::Save { + save_behavior: Some(SaveBehavior::PromptOnConflict), + } + .boxed_clone(), + ), + "w!" | "wr!" | "wri!" | "writ!" | "write!" => ( + "write", + workspace::Save { + save_behavior: Some(SaveBehavior::SilentlyOverwrite), + } + .boxed_clone(), + ), + + // :q + "q" | "qu" | "qui" | "quit" => ( + "quit", + workspace::CloseActiveItem { + save_behavior: Some(SaveBehavior::PromptOnWrite), + } + .boxed_clone(), + ), + "q!" | "qu!" | "qui!" | "quit!" => ( + "quit!", + workspace::CloseActiveItem { + save_behavior: Some(SaveBehavior::DontSave), + } + .boxed_clone(), + ), + + // :wq + "wq" => ( + "wq", + workspace::CloseActiveItem { + save_behavior: Some(SaveBehavior::PromptOnConflict), + } + .boxed_clone(), + ), + "wq!" => ( + "wq!", + workspace::CloseActiveItem { + save_behavior: Some(SaveBehavior::SilentlyOverwrite), + } + .boxed_clone(), + ), + // :x + "x" | "xi" | "xit" | "exi" | "exit" => ( + "exit", + workspace::CloseActiveItem { + save_behavior: Some(SaveBehavior::PromptOnConflict), + } + .boxed_clone(), + ), + "x!" | "xi!" | "xit!" | "exi!" | "exit!" => ( + "xit", + workspace::CloseActiveItem { + save_behavior: Some(SaveBehavior::SilentlyOverwrite), + } + .boxed_clone(), + ), + + // :wa + "wa" | "wal" | "wall" => ( + "wall", + workspace::SaveAll { + save_behavior: Some(SaveBehavior::PromptOnConflict), + } + .boxed_clone(), + ), + "wa!" | "wal!" | "wall!" => ( + "wall!", + workspace::SaveAll { + save_behavior: Some(SaveBehavior::SilentlyOverwrite), + } + .boxed_clone(), + ), + + // :qa + "qa" | "qal" | "qall" | "quita" | "quital" | "quitall" => ( + "quitall", + workspace::CloseAllItemsAndPanes { + save_behavior: Some(SaveBehavior::PromptOnWrite), + } + .boxed_clone(), + ), + "qa!" | "qal!" | "qall!" | "quita!" | "quital!" | "quitall!" => ( + "quitall!", + workspace::CloseAllItemsAndPanes { + save_behavior: Some(SaveBehavior::DontSave), + } + .boxed_clone(), + ), + + // :cq + "cq" | "cqu" | "cqui" | "cquit" | "cq!" | "cqu!" | "cqui!" | "cquit!" => ( + "cquit!", + workspace::CloseAllItemsAndPanes { + save_behavior: Some(SaveBehavior::DontSave), + } + .boxed_clone(), + ), + + // :xa + "xa" | "xal" | "xall" => ( + "xall", + workspace::CloseAllItemsAndPanes { + save_behavior: Some(SaveBehavior::PromptOnConflict), + } + .boxed_clone(), + ), + "xa!" | "xal!" | "xall!" => ( + "zall!", + workspace::CloseAllItemsAndPanes { + save_behavior: Some(SaveBehavior::SilentlyOverwrite), + } + .boxed_clone(), + ), + + // :wqa + "wqa" | "wqal" | "wqall" => ( + "wqall", + workspace::CloseAllItemsAndPanes { + save_behavior: Some(SaveBehavior::PromptOnConflict), + } + .boxed_clone(), + ), + "wqa!" | "wqal!" | "wqall!" => ( + "wqall!", + workspace::CloseAllItemsAndPanes { + save_behavior: Some(SaveBehavior::SilentlyOverwrite), + } + .boxed_clone(), + ), + + "j" | "jo" | "joi" | "join" => ("join", JoinLines.boxed_clone()), + + "sp" | "spl" | "spli" | "split" => ("split", workspace::SplitUp.boxed_clone()), + "vs" | "vsp" | "vspl" | "vspli" | "vsplit" => { + ("vsplit", workspace::SplitLeft.boxed_clone()) + } + "cn" | "cne" | "cnex" | "cnext" => ("cnext", editor::GoToDiagnostic.boxed_clone()), + "cp" | "cpr" | "cpre" | "cprev" => ("cprev", editor::GoToPrevDiagnostic.boxed_clone()), + + _ => { + if let Ok(line) = query.parse::() { + (query, GoToLine { line }.boxed_clone()) + } else { + return None; + } + } + }; + + let string = ":".to_owned() + name; + let positions = generate_positions(&string, query); + + Some(CommandInterceptResult { + action, + string, + positions, + }) +} + +fn generate_positions(string: &str, query: &str) -> Vec { + let mut positions = Vec::new(); + let mut chars = query.chars().into_iter(); + + let Some(mut current) = chars.next() else { + return positions; + }; + + for (i, c) in string.chars().enumerate() { + if c == current { + positions.push(i); + if let Some(c) = chars.next() { + current = c; + } else { + break; + } + } + } + + positions +} diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index e2fa6e989a..6ff997a161 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -1,6 +1,7 @@ #[cfg(test)] mod test; +mod command; mod editor_events; mod insert; mod mode_indicator; @@ -13,6 +14,7 @@ mod visual; use anyhow::Result; use collections::{CommandPaletteFilter, HashMap}; +use command_palette::CommandPaletteInterceptor; use editor::{movement, Editor, EditorMode, Event}; use gpui::{ actions, impl_actions, keymap_matcher::KeymapContext, keymap_matcher::MatchResult, Action, @@ -63,6 +65,7 @@ pub fn init(cx: &mut AppContext) { insert::init(cx); object::init(cx); motion::init(cx); + command::init(cx); // Vim Actions cx.add_action(|_: &mut Workspace, &SwitchMode(mode): &SwitchMode, cx| { @@ -469,6 +472,12 @@ impl Vim { } }); + if self.enabled { + cx.set_global::(Box::new(command::command_interceptor)); + } else if cx.has_global::() { + let _ = cx.remove_global::(); + } + cx.update_active_window(|cx| { if self.enabled { let active_editor = cx diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index a3e6a547dd..5275a2664a 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -78,10 +78,17 @@ pub struct CloseItemsToTheRightById { } #[derive(Clone, PartialEq, Debug, Deserialize, Default)] +#[serde(rename_all = "camelCase")] pub struct CloseActiveItem { pub save_behavior: Option, } +#[derive(Clone, PartialEq, Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CloseAllItems { + pub save_behavior: Option, +} + actions!( pane, [ @@ -92,7 +99,6 @@ actions!( CloseCleanItems, CloseItemsToTheLeft, CloseItemsToTheRight, - CloseAllItems, GoBack, GoForward, ReopenClosedItem, @@ -103,7 +109,7 @@ actions!( ] ); -impl_actions!(pane, [ActivateItem, CloseActiveItem]); +impl_actions!(pane, [ActivateItem, CloseActiveItem, CloseAllItems]); const MAX_NAVIGATION_HISTORY_LEN: usize = 1024; @@ -829,14 +835,18 @@ impl Pane { pub fn close_all_items( &mut self, - _: &CloseAllItems, + action: &CloseAllItems, cx: &mut ViewContext, ) -> Option>> { if self.items.is_empty() { return None; } - Some(self.close_items(cx, SaveBehavior::PromptOnWrite, |_| true)) + Some(self.close_items( + cx, + action.save_behavior.unwrap_or(SaveBehavior::PromptOnWrite), + |_| true, + )) } pub fn close_items( @@ -1175,7 +1185,12 @@ impl Pane { ContextMenuItem::action("Close Clean Items", CloseCleanItems), ContextMenuItem::action("Close Items To The Left", CloseItemsToTheLeft), ContextMenuItem::action("Close Items To The Right", CloseItemsToTheRight), - ContextMenuItem::action("Close All Items", CloseAllItems), + ContextMenuItem::action( + "Close All Items", + CloseAllItems { + save_behavior: None, + }, + ), ] } else { // In the case of the user right clicking on a non-active tab, for some item-closing commands, we need to provide the id of the tab, for the others, we can reuse the existing command. @@ -1219,7 +1234,12 @@ impl Pane { } } }), - ContextMenuItem::action("Close All Items", CloseAllItems), + ContextMenuItem::action( + "Close All Items", + CloseAllItems { + save_behavior: None, + }, + ), ] }, cx, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index feab53d094..c297962684 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -122,13 +122,11 @@ actions!( Open, NewFile, NewWindow, - CloseWindow, CloseInactiveTabsAndPanes, AddFolderToProject, Unfollow, - Save, SaveAs, - SaveAll, + ReloadActiveItem, ActivatePreviousPane, ActivateNextPane, FollowNextCollaborator, @@ -158,6 +156,30 @@ pub struct ActivatePane(pub usize); #[derive(Clone, Deserialize, PartialEq)] pub struct ActivatePaneInDirection(pub SplitDirection); +#[derive(Clone, PartialEq, Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SaveAll { + pub save_behavior: Option, +} + +#[derive(Clone, PartialEq, Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Save { + pub save_behavior: Option, +} + +#[derive(Clone, PartialEq, Debug, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct CloseWindow { + pub save_behavior: Option, +} + +#[derive(Clone, PartialEq, Debug, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct CloseAllItemsAndPanes { + pub save_behavior: Option, +} + #[derive(Deserialize)] pub struct Toast { id: usize, @@ -210,7 +232,16 @@ pub struct OpenTerminal { impl_actions!( workspace, - [ActivatePane, ActivatePaneInDirection, Toast, OpenTerminal] + [ + ActivatePane, + ActivatePaneInDirection, + Toast, + OpenTerminal, + SaveAll, + Save, + CloseWindow, + CloseAllItemsAndPanes, + ] ); pub type WorkspaceId = i64; @@ -251,6 +282,7 @@ pub fn init(app_state: Arc, cx: &mut AppContext) { cx.add_async_action(Workspace::follow_next_collaborator); cx.add_async_action(Workspace::close); cx.add_async_action(Workspace::close_inactive_items_and_panes); + cx.add_async_action(Workspace::close_all_items_and_panes); cx.add_global_action(Workspace::close_global); cx.add_global_action(restart); cx.add_async_action(Workspace::save_all); @@ -1262,11 +1294,15 @@ impl Workspace { pub fn close( &mut self, - _: &CloseWindow, + action: &CloseWindow, cx: &mut ViewContext, ) -> Option>> { let window = cx.window(); - let prepare = self.prepare_to_close(false, cx); + let prepare = self.prepare_to_close( + false, + action.save_behavior.unwrap_or(SaveBehavior::PromptOnWrite), + cx, + ); Some(cx.spawn(|_, mut cx| async move { if prepare.await? { window.remove(&mut cx); @@ -1323,8 +1359,17 @@ impl Workspace { }) } - fn save_all(&mut self, _: &SaveAll, cx: &mut ViewContext) -> Option>> { - let save_all = self.save_all_internal(SaveBehavior::PromptOnConflict, cx); + fn save_all( + &mut self, + action: &SaveAll, + cx: &mut ViewContext, + ) -> Option>> { + let save_all = self.save_all_internal( + action + .save_behavior + .unwrap_or(SaveBehavior::PromptOnConflict), + cx, + ); Some(cx.foreground().spawn(async move { save_all.await?; Ok(()) @@ -1691,24 +1736,52 @@ impl Workspace { &mut self, _: &CloseInactiveTabsAndPanes, cx: &mut ViewContext, + ) -> Option>> { + self.close_all_internal(true, SaveBehavior::PromptOnWrite, cx) + } + + pub fn close_all_items_and_panes( + &mut self, + action: &CloseAllItemsAndPanes, + cx: &mut ViewContext, + ) -> Option>> { + self.close_all_internal( + false, + action.save_behavior.unwrap_or(SaveBehavior::PromptOnWrite), + cx, + ) + } + + fn close_all_internal( + &mut self, + retain_active_pane: bool, + save_behavior: SaveBehavior, + cx: &mut ViewContext, ) -> Option>> { let current_pane = self.active_pane(); let mut tasks = Vec::new(); - if let Some(current_pane_close) = current_pane.update(cx, |pane, cx| { - pane.close_inactive_items(&CloseInactiveItems, cx) - }) { - tasks.push(current_pane_close); - }; + if retain_active_pane { + if let Some(current_pane_close) = current_pane.update(cx, |pane, cx| { + pane.close_inactive_items(&CloseInactiveItems, cx) + }) { + tasks.push(current_pane_close); + }; + } for pane in self.panes() { - if pane.id() == current_pane.id() { + if retain_active_pane && pane.id() == current_pane.id() { continue; } if let Some(close_pane_items) = pane.update(cx, |pane: &mut Pane, cx| { - pane.close_all_items(&CloseAllItems, cx) + pane.close_all_items( + &CloseAllItems { + save_behavior: Some(save_behavior), + }, + cx, + ) }) { tasks.push(close_pane_items) } diff --git a/crates/zed/src/menus.rs b/crates/zed/src/menus.rs index 6b5f7b3a35..da3f7e4c32 100644 --- a/crates/zed/src/menus.rs +++ b/crates/zed/src/menus.rs @@ -38,16 +38,31 @@ pub fn menus() -> Vec> { MenuItem::action("Open Recent...", recent_projects::OpenRecent), MenuItem::separator(), MenuItem::action("Add Folder to Project…", workspace::AddFolderToProject), - MenuItem::action("Save", workspace::Save), + MenuItem::action( + "Save", + workspace::Save { + save_behavior: None, + }, + ), MenuItem::action("Save As…", workspace::SaveAs), - MenuItem::action("Save All", workspace::SaveAll), + MenuItem::action( + "Save All", + workspace::SaveAll { + save_behavior: None, + }, + ), MenuItem::action( "Close Editor", workspace::CloseActiveItem { save_behavior: None, }, ), - MenuItem::action("Close Window", workspace::CloseWindow), + MenuItem::action( + "Close Window", + workspace::CloseWindow { + save_behavior: None, + }, + ), ], }, Menu {