diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 7d0c2ef2cc6c8d77223c7424b46a1ed09267fb56..aed9b78c44f2a624d18e337173269c6fb5a54475 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -1,12 +1,6 @@ [ { - "context": "ProjectPanel || Editor", - "bindings": { - "ctrl-6": "pane::AlternateFile" - } - }, - { - "context": "Editor && VimControl && !VimWaiting && !menu", + "context": "VimControl && !menu", "bindings": { "i": ["vim::PushOperator", { "Object": { "around": false } }], "a": ["vim::PushOperator", { "Object": { "around": true } }], @@ -198,20 +192,21 @@ "ctrl-w g shift-d": "editor::GoToTypeDefinitionSplit", "ctrl-w space": "editor::OpenExcerptsSplit", "ctrl-w g space": "editor::OpenExcerptsSplit", - "-": "pane::RevealInProjectPanel" + "-": "pane::RevealInProjectPanel", + "ctrl-6": "pane::AlternateFile" } }, { - // escape is in its own section so that it cancels a pending count. - "context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting", + "context": "VimControl && VimCount", "bindings": { - "escape": "editor::Cancel", - "ctrl-[": "editor::Cancel" + "0": ["vim::Number", 0] } }, { - "context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting", + "context": "vim_mode == normal", "bindings": { + "escape": "editor::Cancel", + "ctrl-[": "editor::Cancel", ".": "vim::Repeat", "c": ["vim::PushOperator", "Change"], "shift-c": "vim::ChangeToEndOfLine", @@ -255,12 +250,48 @@ "] d": "editor::GoToDiagnostic", "[ d": "editor::GoToPrevDiagnostic", "] c": "editor::GoToHunk", - "[ c": "editor::GoToPrevHunk" + "[ c": "editor::GoToPrevHunk", + "g c c": "vim::ToggleComments" } }, { - "context": "Editor && vim_mode == visual && vim_operator == none && !VimWaiting", + "context": "vim_mode == visual", "bindings": { + "u": "vim::ConvertToLowerCase", + "U": "vim::ConvertToUpperCase", + "o": "vim::OtherEnd", + "shift-o": "vim::OtherEnd", + "d": "vim::VisualDelete", + "x": "vim::VisualDelete", + "shift-d": "vim::VisualDeleteLine", + "shift-x": "vim::VisualDeleteLine", + "y": "vim::VisualYank", + "shift-y": "vim::VisualYank", + "p": "vim::Paste", + "shift-p": ["vim::Paste", { "preserveClipboard": true }], + "s": "vim::Substitute", + "shift-s": "vim::SubstituteLine", + "shift-r": "vim::SubstituteLine", + "c": "vim::Substitute", + "~": "vim::ChangeCase", + "*": ["vim::MoveToNext", { "partialWord": true }], + "#": ["vim::MoveToPrev", { "partialWord": true }], + "ctrl-a": "vim::Increment", + "ctrl-x": "vim::Decrement", + "g ctrl-a": ["vim::Increment", { "step": true }], + "g ctrl-x": ["vim::Decrement", { "step": true }], + "shift-i": "vim::InsertBefore", + "shift-a": "vim::InsertAfter", + "shift-j": "vim::JoinLines", + "r": ["vim::PushOperator", "Replace"], + "ctrl-c": ["vim::SwitchMode", "Normal"], + "escape": ["vim::SwitchMode", "Normal"], + "ctrl-[": ["vim::SwitchMode", "Normal"], + ">": "vim::Indent", + "<": "vim::Outdent", + "i": ["vim::PushOperator", { "Object": { "around": false } }], + "a": ["vim::PushOperator", { "Object": { "around": true } }], + "g c": "vim::ToggleComments", "\"": ["vim::PushOperator", "Register"], // tree-sitter related commands "[ x": "editor::SelectLargerSyntaxNode", @@ -268,89 +299,52 @@ } }, { - "context": "Editor && VimCount && vim_mode != insert", + "context": "vim_mode == insert", "bindings": { - "0": ["vim::Number", 0] - } - }, - { - "context": "Editor && vim_operator == c", - "bindings": { - "c": "vim::CurrentLine", - "d": "editor::Rename" // zed specific - } - }, - { - "context": "Editor && vim_mode == normal && vim_operator == c", - "bindings": { - "s": ["vim::PushOperator", { "ChangeSurrounds": {} }] - } - }, - { - "context": "Editor && vim_operator == d", - "bindings": { - "d": "vim::CurrentLine" - } - }, - { - "context": "Editor && vim_operator == gu", - "bindings": { - "g u": "vim::CurrentLine", - "u": "vim::CurrentLine" - } - }, - { - "context": "Editor && vim_operator == gU", - "bindings": { - "g shift-u": "vim::CurrentLine", - "shift-u": "vim::CurrentLine" - } - }, - { - "context": "Editor && vim_operator == g~", - "bindings": { - "g ~": "vim::CurrentLine", - "~": "vim::CurrentLine" - } - }, - { - "context": "Editor && vim_mode == normal && vim_operator == d", - "bindings": { - "s": ["vim::PushOperator", "DeleteSurrounds"] - } - }, - { - "context": "Editor && vim_operator == y", - "bindings": { - "y": "vim::CurrentLine" - } - }, - { - "context": "Editor && vim_mode == normal && vim_operator == y", - "bindings": { - "s": ["vim::PushOperator", { "AddSurrounds": {} }] + "escape": "vim::NormalBefore", + "ctrl-c": "vim::NormalBefore", + "ctrl-[": "vim::NormalBefore", + "ctrl-x": null, + "ctrl-x ctrl-o": "editor::ShowCompletions", + "ctrl-x ctrl-a": "assistant::InlineAssist", // zed specific + "ctrl-x ctrl-c": "editor::ShowInlineCompletion", // zed specific + "ctrl-x ctrl-l": "editor::ToggleCodeActions", // zed specific + "ctrl-x ctrl-z": "editor::Cancel", + "ctrl-w": "editor::DeleteToPreviousWordStart", + "ctrl-u": "editor::DeleteToBeginningOfLine", + "ctrl-t": "vim::Indent", + "ctrl-d": "vim::Outdent", + "ctrl-r": ["vim::PushOperator", "Register"] } }, { - "context": "Editor && vim_operator == ys", + "context": "vim_mode == replace", "bindings": { - "s": "vim::CurrentLine" + "escape": "vim::NormalBefore", + "ctrl-c": "vim::NormalBefore", + "ctrl-[": "vim::NormalBefore", + "backspace": "vim::UndoReplace", + "tab": "vim::Tab", + "enter": "vim::Enter" } }, { - "context": "Editor && vim_operator == >", + "context": "vim_mode == waiting", "bindings": { - ">": "vim::CurrentLine" + "tab": "vim::Tab", + "enter": "vim::Enter" } }, { - "context": "Editor && vim_operator == <", + "context": "vim_mode == operator", "bindings": { - "<": "vim::CurrentLine" + "escape": "vim::ClearOperators", + "ctrl-c": "vim::ClearOperators", + "ctrl-[": "vim::ClearOperators" } }, { - "context": "Editor && VimObject", + "context": "vim_operator == a || vim_operator == i || vim_operator == cs", "bindings": { "w": "vim::Word", "shift-w": ["vim::Word", { "ignorePunctuation": true }], @@ -375,100 +369,64 @@ } }, { - "context": "Editor && vim_mode == visual && !VimWaiting && !VimObject", + "context": "vim_operator == c", "bindings": { - "u": "vim::ConvertToLowerCase", - "U": "vim::ConvertToUpperCase", - "o": "vim::OtherEnd", - "shift-o": "vim::OtherEnd", - "d": "vim::VisualDelete", - "x": "vim::VisualDelete", - "shift-d": "vim::VisualDeleteLine", - "shift-x": "vim::VisualDeleteLine", - "y": "vim::VisualYank", - "shift-y": "vim::VisualYank", - "p": "vim::Paste", - "shift-p": ["vim::Paste", { "preserveClipboard": true }], - "s": "vim::Substitute", - "shift-s": "vim::SubstituteLine", - "shift-r": "vim::SubstituteLine", - "c": "vim::Substitute", - "~": "vim::ChangeCase", - "*": ["vim::MoveToNext", { "partialWord": true }], - "#": ["vim::MoveToPrev", { "partialWord": true }], - "ctrl-a": "vim::Increment", - "ctrl-x": "vim::Decrement", - "g ctrl-a": ["vim::Increment", { "step": true }], - "g ctrl-x": ["vim::Decrement", { "step": true }], - "shift-i": "vim::InsertBefore", - "shift-a": "vim::InsertAfter", - "shift-j": "vim::JoinLines", - "r": ["vim::PushOperator", "Replace"], - "ctrl-c": ["vim::SwitchMode", "Normal"], - "escape": ["vim::SwitchMode", "Normal"], - "ctrl-[": ["vim::SwitchMode", "Normal"], - ">": "vim::Indent", - "<": "vim::Outdent", - "i": ["vim::PushOperator", { "Object": { "around": false } }], - "a": ["vim::PushOperator", { "Object": { "around": true } }] + "c": "vim::CurrentLine", + "d": "editor::Rename", // zed specific + "s": ["vim::PushOperator", { "ChangeSurrounds": {} }] } }, { - "context": "Editor && vim_mode == normal && !VimWaiting", + "context": "vim_operator == d", "bindings": { - "g c c": "vim::ToggleComments" + "d": "vim::CurrentLine", + "s": ["vim::PushOperator", "DeleteSurrounds"] } }, { - "context": "Editor && vim_mode == visual", + "context": "vim_operator == gu", "bindings": { - "g c": "vim::ToggleComments" + "g u": "vim::CurrentLine", + "u": "vim::CurrentLine" } }, { - "context": "Editor && vim_mode == insert", + "context": "vim_operator == gU", "bindings": { - "escape": "vim::NormalBefore", - "ctrl-c": "vim::NormalBefore", - "ctrl-[": "vim::NormalBefore", - "ctrl-x": null, - "ctrl-x ctrl-o": "editor::ShowCompletions", - "ctrl-x ctrl-a": "assistant::InlineAssist", // zed specific - "ctrl-x ctrl-c": "editor::ShowInlineCompletion", // zed specific - "ctrl-x ctrl-l": "editor::ToggleCodeActions", // zed specific - "ctrl-x ctrl-z": "editor::Cancel", - "ctrl-w": "editor::DeleteToPreviousWordStart", - "ctrl-u": "editor::DeleteToBeginningOfLine", - "ctrl-t": "vim::Indent", - "ctrl-d": "vim::Outdent", - "ctrl-r": ["vim::PushOperator", "Register"] + "g shift-u": "vim::CurrentLine", + "shift-u": "vim::CurrentLine" } }, { - "context": "Editor && vim_mode == replace", + "context": "vim_operator == g~", "bindings": { - "escape": "vim::NormalBefore", - "ctrl-c": "vim::NormalBefore", - "ctrl-[": "vim::NormalBefore", - "tab": "vim::Tab", - "enter": "vim::Enter", - "backspace": "vim::UndoReplace" + "g ~": "vim::CurrentLine", + "~": "vim::CurrentLine" } }, { - "context": "Editor && vim_mode != replace && VimWaiting", + "context": "vim_operator == y", "bindings": { - "tab": "vim::Tab", - "enter": "vim::Enter", - "escape": ["vim::SwitchMode", "Normal"], - "ctrl-[": ["vim::SwitchMode", "Normal"] + "y": "vim::CurrentLine", + "s": ["vim::PushOperator", { "AddSurrounds": {} }] } }, { - "context": "Editor && vim_mode == insert && VimWaiting", + "context": "vim_operator == ys", "bindings": { - "escape": "vim::NormalBefore", - "ctrl-[": "vim::NormalBefore" + "s": "vim::CurrentLine" + } + }, + { + "context": "vim_operator == >", + "bindings": { + ">": "vim::CurrentLine" + } + }, + { + "context": "vim_operator == <", + "bindings": { + "<": "vim::CurrentLine" } }, { @@ -508,7 +466,8 @@ "x": "project_panel::RevealInFileManager", "shift-g": "menu::SelectLast", "g g": "menu::SelectFirst", - "-": "project_panel::SelectParent" + "-": "project_panel::SelectParent", + "ctrl-6": "pane::AlternateFile" } }, { diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index 8c724228a91f71452401dc97bc57cd95138676cc..8adaa7271d6f4a7e984198b8ac2d037b143dcf79 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -228,21 +228,19 @@ impl EditorState { } } - pub fn vim_controlled(&self) -> bool { - let is_insert_mode = matches!(self.mode, Mode::Insert); - if !is_insert_mode { - return true; + pub fn editor_input_enabled(&self) -> bool { + match self.mode { + Mode::Insert => { + if let Some(operator) = self.operator_stack.last() { + !operator.is_waiting(self.mode) + } else { + true + } + } + Mode::Normal | Mode::Replace | Mode::Visual | Mode::VisualLine | Mode::VisualBlock => { + false + } } - matches!( - self.operator_stack.last(), - Some(Operator::FindForward { .. }) - | Some(Operator::FindBackward { .. }) - | Some(Operator::Mark) - | Some(Operator::Register) - | Some(Operator::RecordRegister) - | Some(Operator::ReplayRegister) - | Some(Operator::Jump { .. }) - ) } pub fn should_autoindent(&self) -> bool { @@ -264,48 +262,39 @@ impl EditorState { pub fn keymap_context_layer(&self) -> KeyContext { let mut context = KeyContext::new_with_defaults(); - context.set( - "vim_mode", - match self.mode { - Mode::Normal => "normal", - Mode::Visual | Mode::VisualLine | Mode::VisualBlock => "visual", - Mode::Insert => "insert", - Mode::Replace => "replace", - }, - ); - if self.vim_controlled() { - context.add("VimControl"); + let mut mode = match self.mode { + Mode::Normal => "normal", + Mode::Visual | Mode::VisualLine | Mode::VisualBlock => "visual", + Mode::Insert => "insert", + Mode::Replace => "replace", } + .to_string(); - if self.active_operator().is_none() && self.pre_count.is_some() - || self.active_operator().is_some() && self.post_count.is_some() + let mut operator_id = "none"; + + let active_operator = self.active_operator(); + if active_operator.is_none() && self.pre_count.is_some() + || active_operator.is_some() && self.post_count.is_some() { context.add("VimCount"); } - let active_operator = self.active_operator(); - - if let Some(active_operator) = active_operator.clone() { - for context_flag in active_operator.context_flags().into_iter() { - context.add(*context_flag); + if let Some(active_operator) = active_operator { + if active_operator.is_waiting(self.mode) { + mode = "waiting".to_string(); + } else { + mode = "operator".to_string(); + operator_id = active_operator.id(); } } - context.set( - "vim_operator", - active_operator - .clone() - .map(|op| op.id()) - .unwrap_or_else(|| "none"), - ); - - if self.mode == Mode::Replace - || (matches!(active_operator, Some(Operator::AddSurrounds { .. })) - && self.mode.is_visual()) - { - context.add("VimWaiting"); + if mode != "waiting" && mode != "insert" && mode != "replace" { + context.add("VimControl"); } + context.set("vim_mode", mode); + context.set("vim_operator", operator_id); + context } } @@ -340,9 +329,9 @@ impl Operator { } } - pub fn context_flags(&self) -> &'static [&'static str] { + pub fn is_waiting(&self, mode: Mode) -> bool { match self { - Operator::Object { .. } | Operator::ChangeSurrounds { target: None } => &["VimObject"], + Operator::AddSurrounds { target } => target.is_some() || mode.is_visual(), Operator::FindForward { .. } | Operator::Mark | Operator::Jump { .. } @@ -351,10 +340,18 @@ impl Operator { | Operator::RecordRegister | Operator::ReplayRegister | Operator::Replace - | Operator::AddSurrounds { target: Some(_) } - | Operator::ChangeSurrounds { .. } - | Operator::DeleteSurrounds => &["VimWaiting"], - _ => &[], + | Operator::ChangeSurrounds { target: Some(_) } + | Operator::DeleteSurrounds => true, + Operator::Change + | Operator::Delete + | Operator::Yank + | Operator::Indent + | Operator::Outdent + | Operator::Lowercase + | Operator::Uppercase + | Operator::Object { .. } + | Operator::ChangeSurrounds { target: None } + | Operator::OppositeCase => false, } } } diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index be7244f7f0fd069f2bae5abb5d37832f7331015f..cf800f854fd6bd781f0d3355a5015a0e02c53e75 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -76,6 +76,7 @@ struct SelectRegister(String); actions!( vim, [ + ClearOperators, Tab, Enter, Object, @@ -129,6 +130,9 @@ fn register(workspace: &mut Workspace, cx: &mut ViewContext) { Vim::update(cx, |vim, cx| vim.push_operator(operator.clone(), cx)) }, ); + workspace.register_action(|_: &mut Workspace, _: &ClearOperators, cx| { + Vim::update(cx, |vim, cx| vim.clear_operator(cx)) + }); workspace.register_action(|_: &mut Workspace, n: &Number, cx: _| { Vim::update(cx, |vim, cx| vim.push_count_digit(n.0, cx)); }); @@ -973,7 +977,7 @@ impl Vim { editor.set_cursor_shape(state.cursor_shape(), cx); editor.set_clip_at_line_ends(state.clip_at_line_ends(), cx); editor.set_collapse_matches(true); - editor.set_input_enabled(!state.vim_controlled()); + editor.set_input_enabled(state.editor_input_enabled()); editor.set_autoindent(state.should_autoindent()); editor.selections.line_mode = matches!(state.mode, Mode::VisualLine); if editor.is_focused(cx) || editor.mouse_menu_is_focused(cx) { diff --git a/docs/src/vim.md b/docs/src/vim.md index 370e09066921e71bd7c8c21ac8c872bc28c35187..cc40483853b758ed04afa113e790a89aa965bf8b 100644 --- a/docs/src/vim.md +++ b/docs/src/vim.md @@ -85,36 +85,22 @@ Finally, Vim mode's search and replace functionality is backed by Zed's. This me ## Custom key bindings You can edit your personal key bindings with `:keymap`. -For vim-specific shortcuts, you may find the following template a good place to start: +For vim-specific shortcuts, you may find the following template a good place to start. + +> **Note:** We made some breaking changes in Zed version `0.145.0`. For older versions, see [the previous version of this document](https://github.com/zed-industries/zed/blob/c67aeaa9c58619a58708722ac7d7a78c75c29336/docs/src/vim.md#L90). ```json [ { - "context": "Editor && (vim_mode == normal || vim_mode == visual) && !VimWaiting && !menu", + "context": "VimControl && !menu", "bindings": { // put key-bindings here if you want them to work in normal & visual mode } }, { - "context": "Editor && vim_mode == normal && !VimWaiting && !menu", - "bindings": { - // put key-bindings here if you want them to work only in normal mode - // "down": ["workspace::SendKeystrokes", "4 j"] - // "up": ["workspace::SendKeystrokes", "4 k"] - } - }, - { - "context": "Editor && vim_mode == visual && !VimWaiting && !menu", + "context": "vim_mode == insert", "bindings": { - // visual, visual line & visual block modes - } - }, - { - "context": "Editor && vim_mode == insert && !menu", - "bindings": { - // put key-bindings here if you want them to work in insert mode - // e.g. - // "j j": "vim::NormalBefore" // remap jj in insert mode to escape. + // "j k": "vim::NormalBefore" // remap jk in insert mode to escape. } }, { @@ -122,7 +108,6 @@ For vim-specific shortcuts, you may find the following template a good place to "bindings": { // put key-bindings here (in addition to above) if you want them to // work when no editor exists - // e.g. // "space f": "file_finder::Toggle" } } @@ -133,20 +118,15 @@ If you would like to emulate vim's `map` (`nmap` etc.) commands you can bind to You can see the bindings that are enabled by default in vim mode [here](https://github.com/zed-industries/zed/blob/main/assets/keymaps/vim.json). -The details of the context are a little out of scope for this doc, but suffice to say that `menu` is true when a menu is open (e.g. the completions menu), `VimWaiting` is true after you type `f` or `t` when we’re waiting for a new key (and you probably don’t want bindings to happen). Please reach out on [GitHub](https://github.com/zed-industries/zed) if you want help making a key bindings work. +#### Contexts -### Examples +Zed's keyboard bindings are evaluated only when the `"context"` matches the location you are in on the screen. Locations are nested, so when you're editing you're in the `"Workspace"` location is at the top, containing a `"Pane"` which contains an `"Editor"`. Contexts are matched only on one level at a time. So it is possible to combine `Editor && vim_mode == normal`, but `Workspace && vim_mode == normal` will never match because we set the vim context at the `Editor` level. -Binding `jk` to exit insert mode and go to normal mode: +Vim mode adds several contexts to the `Editor`: -``` -{ - "context": "Editor && vim_mode == insert && !menu", - "bindings": { - "j k": ["vim::SwitchMode", "Normal"] - } -} -``` +* `vim_mode` is similar to, but not identical to, the current mode. It starts as one of `normal`, `visual`, `insert` or `replace` (depending on your mode). If you are mid-way through typing a sequence, `vim_mode` will be either `waiting` if it's waiting for an arbitrary key (for example after typing `f` or `t`), or `operator` if it's waiting for another binding to trigger (for example after typing `c` or `d`). +* `vim_operator` is set to `none` unless `vim_mode == operator` in which case it is set to the current operator's default keybinding (for example after typing `d`, `vim_operator == d`). +* `"VimControl"` indicates that vim keybindings should work. It is currently an alias for `vim_mode == normal || vim_mode == visual || vim_mode == operator`, but the definition may change over time. ### Restoring some sense of normality @@ -155,7 +135,7 @@ that you can't live without. You can restore them to their defaults by copying t ``` { - "context": "Editor && !VimWaiting && !menu", + "context": "Editor && !menu", "bindings": { "ctrl-c": "editor::Copy", // vim default: return to normal mode "ctrl-x": "editor::Cut", // vim default: increment @@ -304,7 +284,7 @@ Subword motion is not enabled by default. To enable it, add these bindings to yo ```json { - "context": "Editor && VimControl && !VimWaiting && !menu", + "context": "VimControl && !menu", "bindings": { "w": "vim::NextSubwordStart", "b": "vim::PreviousSubwordStart", @@ -318,7 +298,7 @@ Surrounding the selection in visual mode is also not enabled by default (`shift- ```json { - "context": "Editor && vim_mode == visual && !VimWaiting && !VimObject", + "context": "vim_mode == visual", "bindings": { "shift-s": [ "vim::PushOperator",