vim: Add multicursor shortcuts (#3032)

Conrad Irwin created

Adding a few bindings to bring first class feeling multiselect to zed's
vim emulation.

gn and gN are similar to similar vim bindings, ga is similar to gA (and
I doubt we need vim's real ga), g> and g< are just made up.

Release Notes:

- vim: `g n` / `g N` to select next/previous
- vim: `g >` / `g <` to skip current selection and select next/previous
- vim: `g a` to select all

Change summary

assets/keymaps/vim.json  | 15 ++++++++++++
crates/vim/src/visual.rs | 50 ++++++++++++++++++++++++++++++++++++++++++
2 files changed, 65 insertions(+)

Detailed changes

assets/keymaps/vim.json 🔗

@@ -126,6 +126,21 @@
       "g shift-t": "pane::ActivatePrevItem",
       "g d": "editor::GoToDefinition",
       "g shift-d": "editor::GoToTypeDefinition",
+      "g n": "vim::SelectNext",
+      "g shift-n": "vim::SelectPrevious",
+      "g >": [
+        "editor::SelectNext",
+        {
+          "replace_newest": true
+        }
+      ],
+      "g <": [
+        "editor::SelectPrevious",
+        {
+          "replace_newest": true
+        }
+      ],
+      "g a": "editor::SelectAllMatches",
       "g s": "outline::Toggle",
       "g shift-s": "project_symbols::Toggle",
       "g .": "editor::ToggleCodeActions", // zed specific

crates/vim/src/visual.rs 🔗

@@ -1,3 +1,4 @@
+use anyhow::Result;
 use std::{cmp, sync::Arc};
 
 use collections::HashMap;
@@ -28,6 +29,8 @@ actions!(
         VisualDelete,
         VisualYank,
         OtherEnd,
+        SelectNext,
+        SelectPrevious,
     ]
 );
 
@@ -46,6 +49,9 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(other_end);
     cx.add_action(delete);
     cx.add_action(yank);
+
+    cx.add_action(select_next);
+    cx.add_action(select_previous);
 }
 
 pub fn visual_motion(motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
@@ -384,6 +390,50 @@ pub(crate) fn visual_replace(text: Arc<str>, cx: &mut WindowContext) {
     });
 }
 
+pub fn select_next(
+    _: &mut Workspace,
+    _: &SelectNext,
+    cx: &mut ViewContext<Workspace>,
+) -> Result<()> {
+    Vim::update(cx, |vim, cx| {
+        let count =
+            vim.take_count(cx)
+                .unwrap_or_else(|| if vim.state().mode.is_visual() { 1 } else { 2 });
+        vim.update_active_editor(cx, |editor, cx| {
+            for _ in 0..count {
+                match editor.select_next(&Default::default(), cx) {
+                    Err(a) => return Err(a),
+                    _ => {}
+                }
+            }
+            Ok(())
+        })
+    })
+    .unwrap_or(Ok(()))
+}
+
+pub fn select_previous(
+    _: &mut Workspace,
+    _: &SelectPrevious,
+    cx: &mut ViewContext<Workspace>,
+) -> Result<()> {
+    Vim::update(cx, |vim, cx| {
+        let count =
+            vim.take_count(cx)
+                .unwrap_or_else(|| if vim.state().mode.is_visual() { 1 } else { 2 });
+        vim.update_active_editor(cx, |editor, cx| {
+            for _ in 0..count {
+                match editor.select_previous(&Default::default(), cx) {
+                    Err(a) => return Err(a),
+                    _ => {}
+                }
+            }
+            Ok(())
+        })
+    })
+    .unwrap_or(Ok(()))
+}
+
 #[cfg(test)]
 mod test {
     use indoc::indoc;