vim: Fix gv after indent/toggle comments (#17986)

Conrad Irwin created

Release Notes:

- vim: Fixed `gv` after > and < in visual mode

Change summary

crates/vim/src/indent.rs                 |  62 +++++++++++++++
crates/vim/src/normal.rs                 | 104 +++++++++-----------------
crates/vim/src/normal/mark.rs            |   2 
crates/vim/src/vim.rs                    |   2 
crates/vim/test_data/test_indent_gv.json |   8 ++
5 files changed, 107 insertions(+), 71 deletions(-)

Detailed changes

crates/vim/src/normal/indent.rs → crates/vim/src/indent.rs 🔗

@@ -1,6 +1,7 @@
-use crate::{motion::Motion, object::Object, Vim};
+use crate::{motion::Motion, object::Object, state::Mode, Vim};
 use collections::HashMap;
-use editor::{display_map::ToDisplayPoint, Bias};
+use editor::{display_map::ToDisplayPoint, Bias, Editor};
+use gpui::actions;
 use language::SelectionGoal;
 use ui::ViewContext;
 
@@ -10,6 +11,46 @@ pub(crate) enum IndentDirection {
     Out,
 }
 
+actions!(vim, [Indent, Outdent,]);
+
+pub(crate) fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
+    Vim::action(editor, cx, |vim, _: &Indent, cx| {
+        vim.record_current_action(cx);
+        let count = vim.take_count(cx).unwrap_or(1);
+        vim.store_visual_marks(cx);
+        vim.update_editor(cx, |vim, editor, cx| {
+            editor.transact(cx, |editor, cx| {
+                let mut original_positions = vim.save_selection_starts(editor, cx);
+                for _ in 0..count {
+                    editor.indent(&Default::default(), cx);
+                }
+                vim.restore_selection_cursors(editor, cx, &mut original_positions);
+            });
+        });
+        if vim.mode.is_visual() {
+            vim.switch_mode(Mode::Normal, true, cx)
+        }
+    });
+
+    Vim::action(editor, cx, |vim, _: &Outdent, cx| {
+        vim.record_current_action(cx);
+        let count = vim.take_count(cx).unwrap_or(1);
+        vim.store_visual_marks(cx);
+        vim.update_editor(cx, |vim, editor, cx| {
+            editor.transact(cx, |editor, cx| {
+                let mut original_positions = vim.save_selection_starts(editor, cx);
+                for _ in 0..count {
+                    editor.outdent(&Default::default(), cx);
+                }
+                vim.restore_selection_cursors(editor, cx, &mut original_positions);
+            });
+        });
+        if vim.mode.is_visual() {
+            vim.switch_mode(Mode::Normal, true, cx)
+        }
+    });
+}
+
 impl Vim {
     pub(crate) fn indent_motion(
         &mut self,
@@ -78,3 +119,20 @@ impl Vim {
         });
     }
 }
+
+#[cfg(test)]
+mod test {
+    use crate::test::NeovimBackedTestContext;
+
+    #[gpui::test]
+    async fn test_indent_gv(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+        cx.set_neovim_option("shiftwidth=4").await;
+
+        cx.set_shared_state("ˇhello\nworld\n").await;
+        cx.simulate_shared_keystrokes("v j > g v").await;
+        cx.shared_state()
+            .await
+            .assert_eq("«    hello\n ˇ»   world\n");
+    }
+}

crates/vim/src/normal.rs 🔗

@@ -2,7 +2,6 @@ mod case;
 mod change;
 mod delete;
 mod increment;
-mod indent;
 pub(crate) mod mark;
 mod paste;
 pub(crate) mod repeat;
@@ -16,6 +15,7 @@ use std::collections::HashMap;
 use std::sync::Arc;
 
 use crate::{
+    indent::IndentDirection,
     motion::{self, first_non_whitespace, next_line_end, right, Motion},
     object::Object,
     state::{Mode, Operator},
@@ -34,8 +34,6 @@ use language::{Point, SelectionGoal};
 use log::error;
 use multi_buffer::MultiBufferRow;
 
-use self::indent::IndentDirection;
-
 actions!(
     vim,
     [
@@ -56,8 +54,6 @@ actions!(
         ConvertToUpperCase,
         ConvertToLowerCase,
         JoinLines,
-        Indent,
-        Outdent,
         ToggleComments,
         Undo,
         Redo,
@@ -129,41 +125,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
             })
         });
         if vim.mode.is_visual() {
-            vim.switch_mode(Mode::Normal, false, cx)
-        }
-    });
-
-    Vim::action(editor, cx, |vim, _: &Indent, cx| {
-        vim.record_current_action(cx);
-        let count = vim.take_count(cx).unwrap_or(1);
-        vim.update_editor(cx, |_, editor, cx| {
-            editor.transact(cx, |editor, cx| {
-                let mut original_positions = save_selection_starts(editor, cx);
-                for _ in 0..count {
-                    editor.indent(&Default::default(), cx);
-                }
-                restore_selection_cursors(editor, cx, &mut original_positions);
-            });
-        });
-        if vim.mode.is_visual() {
-            vim.switch_mode(Mode::Normal, false, cx)
-        }
-    });
-
-    Vim::action(editor, cx, |vim, _: &Outdent, cx| {
-        vim.record_current_action(cx);
-        let count = vim.take_count(cx).unwrap_or(1);
-        vim.update_editor(cx, |_, editor, cx| {
-            editor.transact(cx, |editor, cx| {
-                let mut original_positions = save_selection_starts(editor, cx);
-                for _ in 0..count {
-                    editor.outdent(&Default::default(), cx);
-                }
-                restore_selection_cursors(editor, cx, &mut original_positions);
-            });
-        });
-        if vim.mode.is_visual() {
-            vim.switch_mode(Mode::Normal, false, cx)
+            vim.switch_mode(Mode::Normal, true, cx)
         }
     });
 
@@ -428,15 +390,16 @@ impl Vim {
 
     fn toggle_comments(&mut self, _: &ToggleComments, cx: &mut ViewContext<Self>) {
         self.record_current_action(cx);
-        self.update_editor(cx, |_, editor, cx| {
+        self.store_visual_marks(cx);
+        self.update_editor(cx, |vim, editor, cx| {
             editor.transact(cx, |editor, cx| {
-                let mut original_positions = save_selection_starts(editor, cx);
+                let mut original_positions = vim.save_selection_starts(editor, cx);
                 editor.toggle_comments(&Default::default(), cx);
-                restore_selection_cursors(editor, cx, &mut original_positions);
+                vim.restore_selection_cursors(editor, cx, &mut original_positions);
             });
         });
         if self.mode.is_visual() {
-            self.switch_mode(Mode::Normal, false, cx)
+            self.switch_mode(Mode::Normal, true, cx)
         }
     }
 
@@ -480,33 +443,38 @@ impl Vim {
         });
         self.pop_operator(cx);
     }
-}
 
-fn save_selection_starts(editor: &Editor, cx: &mut ViewContext<Editor>) -> HashMap<usize, Anchor> {
-    let (map, selections) = editor.selections.all_display(cx);
-    selections
-        .iter()
-        .map(|selection| {
-            (
-                selection.id,
-                map.display_point_to_anchor(selection.start, Bias::Right),
-            )
-        })
-        .collect::<HashMap<_, _>>()
-}
+    pub fn save_selection_starts(
+        &self,
+        editor: &Editor,
+        cx: &mut ViewContext<Editor>,
+    ) -> HashMap<usize, Anchor> {
+        let (map, selections) = editor.selections.all_display(cx);
+        selections
+            .iter()
+            .map(|selection| {
+                (
+                    selection.id,
+                    map.display_point_to_anchor(selection.start, Bias::Right),
+                )
+            })
+            .collect::<HashMap<_, _>>()
+    }
 
-fn restore_selection_cursors(
-    editor: &mut Editor,
-    cx: &mut ViewContext<Editor>,
-    positions: &mut HashMap<usize, Anchor>,
-) {
-    editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
-        s.move_with(|map, selection| {
-            if let Some(anchor) = positions.remove(&selection.id) {
-                selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None);
-            }
+    pub fn restore_selection_cursors(
+        &self,
+        editor: &mut Editor,
+        cx: &mut ViewContext<Editor>,
+        positions: &mut HashMap<usize, Anchor>,
+    ) {
+        editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+            s.move_with(|map, selection| {
+                if let Some(anchor) = positions.remove(&selection.id) {
+                    selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None);
+                }
+            });
         });
-    });
+    }
 }
 #[cfg(test)]
 mod test {

crates/vim/src/normal/mark.rs 🔗

@@ -54,7 +54,7 @@ impl Vim {
                 );
                 starts.push(
                     map.buffer_snapshot
-                        .anchor_after(selection.start.to_offset(&map, Bias::Right)),
+                        .anchor_before(selection.start.to_offset(&map, Bias::Left)),
                 );
                 reversed.push(selection.reversed)
             }

crates/vim/src/vim.rs 🔗

@@ -6,6 +6,7 @@ mod test;
 mod change_list;
 mod command;
 mod digraph;
+mod indent;
 mod insert;
 mod mode_indicator;
 mod motion;
@@ -289,6 +290,7 @@ impl Vim {
             motion::register(editor, cx);
             command::register(editor, cx);
             replace::register(editor, cx);
+            indent::register(editor, cx);
             object::register(editor, cx);
             visual::register(editor, cx);
             change_list::register(editor, cx);

crates/vim/test_data/test_indent_gv.json 🔗

@@ -0,0 +1,8 @@
+{"SetOption":{"value":"shiftwidth=4"}}
+{"Put":{"state":"ˇhello\nworld\n"}}
+{"Key":"v"}
+{"Key":"j"}
+{"Key":">"}
+{"Key":"g"}
+{"Key":"v"}
+{"Get":{"state":"«    hello\n ˇ»   world\n","mode":"Visual"}}