vim: Add some forced motion support (#27991)

Peter Finn created

Closes https://github.com/zed-industries/zed/issues/20971

Added `v` input to yank and delete to override default motion. The
global vim state tracking if the forced motion flag was passed handled
the same way that the count is. [The main chunk of code maps the motion
kind from the default to the overridden
kind](https://github.com/zed-industries/zed/pull/27991/files#diff-2dca6b7d1673c912d14e4edc74e415abbe3a4e6d6b37e0e2006d30828bf4bb9cR1249-R1254).
To handle the case of deleting a single character (dv0) at the start of
a row I had to modify the control flow
[here](https://github.com/zed-industries/zed/pull/27991/files#diff-2dca6b7d1673c912d14e4edc74e415abbe3a4e6d6b37e0e2006d30828bf4bb9cR1240-R1244).
Then to handle an exclusive delete till the end of the row (dv$) I
[saturated the endpoint with a left
bias](https://github.com/zed-industries/zed/pull/27991/files#diff-2dca6b7d1673c912d14e4edc74e415abbe3a4e6d6b37e0e2006d30828bf4bb9cR1281-R1286).

Test case: dv0


https://github.com/user-attachments/assets/613cf9fb-9732-425c-9179-025f3e107584

Test case: yvjp


https://github.com/user-attachments/assets/550b7c77-1eb8-41c3-894b-117eb50b7a5d

Release Notes:

- Added some forced motion support for delete and yank

Change summary

assets/keymaps/vim.json                                              |   2 
crates/vim/src/change_list.rs                                        |   1 
crates/vim/src/command.rs                                            |  10 
crates/vim/src/indent.rs                                             |  12 
crates/vim/src/insert.rs                                             |   1 
crates/vim/src/motion.rs                                             | 190 
crates/vim/src/normal.rs                                             | 131 
crates/vim/src/normal/change.rs                                      |   4 
crates/vim/src/normal/convert.rs                                     |  10 
crates/vim/src/normal/delete.rs                                      |  11 
crates/vim/src/normal/increment.rs                                   |   2 
crates/vim/src/normal/paste.rs                                       |  10 
crates/vim/src/normal/repeat.rs                                      |   2 
crates/vim/src/normal/scroll.rs                                      |   1 
crates/vim/src/normal/search.rs                                      |   3 
crates/vim/src/normal/substitute.rs                                  |   4 
crates/vim/src/normal/toggle_comments.rs                             |   9 
crates/vim/src/normal/yank.rs                                        |  16 
crates/vim/src/replace.rs                                            |  10 
crates/vim/src/rewrap.rs                                             |  10 
crates/vim/src/state.rs                                              |   2 
crates/vim/src/surrounds.rs                                          |   9 
crates/vim/src/test/neovim_backed_test_context.rs                    |   2 
crates/vim/src/test/vim_test_context.rs                              |   4 
crates/vim/src/vim.rs                                                |  18 
crates/vim/src/visual.rs                                             |   5 
crates/vim/test_data/test_forced_motion_delete_to_end_of_line.json   |  10 
crates/vim/test_data/test_forced_motion_delete_to_start_of_line.json |  15 
crates/vim/test_data/test_forced_motion_yank.json                    |  24 
crates/vim/test_data/test_inclusive_to_exclusive_delete.json         |  15 
30 files changed, 485 insertions(+), 58 deletions(-)

Detailed changes

assets/keymaps/vim.json 🔗

@@ -539,6 +539,7 @@
     "bindings": {
       "d": "vim::CurrentLine",
       "s": "vim::PushDeleteSurrounds",
+      "v": "vim::PushForcedMotion", // "d v"
       "o": "editor::ToggleSelectedDiffHunks", // "d o"
       "shift-o": "git::ToggleStaged",
       "p": "git::Restore", // "d p"
@@ -587,6 +588,7 @@
     "context": "vim_operator == y",
     "bindings": {
       "y": "vim::CurrentLine",
+      "v": "vim::PushForcedMotion",
       "s": ["vim::PushAddSurrounds", {}]
     }
   },

crates/vim/src/change_list.rs 🔗

@@ -24,6 +24,7 @@ impl Vim {
         cx: &mut Context<Self>,
     ) {
         let count = Vim::take_count(cx).unwrap_or(1);
+        Vim::take_forced_motion(cx);
         if self.change_list.is_empty() {
             return;
         }

crates/vim/src/command.rs 🔗

@@ -234,6 +234,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
             return;
         };
         let count = Vim::take_count(cx).unwrap_or(1);
+        Vim::take_forced_motion(cx);
         let n = if count > 1 {
             format!(".,.+{}", count.saturating_sub(1))
         } else {
@@ -1323,6 +1324,7 @@ impl Vim {
         &mut self,
         motion: Motion,
         times: Option<usize>,
+        forced_motion: bool,
         window: &mut Window,
         cx: &mut Context<Vim>,
     ) {
@@ -1335,7 +1337,13 @@ impl Vim {
             let start = editor.selections.newest_display(cx);
             let text_layout_details = editor.text_layout_details(window);
             let (mut range, _) = motion
-                .range(&snapshot, start.clone(), times, &text_layout_details)
+                .range(
+                    &snapshot,
+                    start.clone(),
+                    times,
+                    &text_layout_details,
+                    forced_motion,
+                )
                 .unwrap_or((start.range(), MotionKind::Exclusive));
             if range.start != start.start {
                 editor.change_selections(None, window, cx, |s| {

crates/vim/src/indent.rs 🔗

@@ -18,6 +18,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
     Vim::action(editor, cx, |vim, _: &Indent, window, cx| {
         vim.record_current_action(cx);
         let count = Vim::take_count(cx).unwrap_or(1);
+        Vim::take_forced_motion(cx);
         vim.store_visual_marks(window, cx);
         vim.update_editor(window, cx, |vim, editor, window, cx| {
             editor.transact(window, cx, |editor, window, cx| {
@@ -36,6 +37,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
     Vim::action(editor, cx, |vim, _: &Outdent, window, cx| {
         vim.record_current_action(cx);
         let count = Vim::take_count(cx).unwrap_or(1);
+        Vim::take_forced_motion(cx);
         vim.store_visual_marks(window, cx);
         vim.update_editor(window, cx, |vim, editor, window, cx| {
             editor.transact(window, cx, |editor, window, cx| {
@@ -54,6 +56,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
     Vim::action(editor, cx, |vim, _: &AutoIndent, window, cx| {
         vim.record_current_action(cx);
         let count = Vim::take_count(cx).unwrap_or(1);
+        Vim::take_forced_motion(cx);
         vim.store_visual_marks(window, cx);
         vim.update_editor(window, cx, |vim, editor, window, cx| {
             editor.transact(window, cx, |editor, window, cx| {
@@ -75,6 +78,7 @@ impl Vim {
         &mut self,
         motion: Motion,
         times: Option<usize>,
+        forced_motion: bool,
         dir: IndentDirection,
         window: &mut Window,
         cx: &mut Context<Self>,
@@ -88,7 +92,13 @@ impl Vim {
                     s.move_with(|map, selection| {
                         let anchor = map.display_point_to_anchor(selection.head(), Bias::Right);
                         selection_starts.insert(selection.id, anchor);
-                        motion.expand_selection(map, selection, times, &text_layout_details);
+                        motion.expand_selection(
+                            map,
+                            selection,
+                            times,
+                            &text_layout_details,
+                            forced_motion,
+                        );
                     });
                 });
                 match dir {

crates/vim/src/insert.rs 🔗

@@ -23,6 +23,7 @@ impl Vim {
             return;
         }
         let count = Vim::take_count(cx).unwrap_or(1);
+        Vim::take_forced_motion(cx);
         self.stop_recording_immediately(action.boxed_clone(), cx);
         if count <= 1 || Vim::globals(cx).dot_replaying {
             self.create_mark("^".into(), window, cx);

crates/vim/src/motion.rs 🔗

@@ -650,6 +650,7 @@ impl Vim {
         }
 
         let count = Vim::take_count(cx);
+        let forced_motion = Vim::take_forced_motion(cx);
         let active_operator = self.active_operator();
         let mut waiting_operator: Option<Operator> = None;
         match self.mode {
@@ -659,7 +660,14 @@ impl Vim {
                         target: Some(SurroundsType::Motion(motion)),
                     });
                 } else {
-                    self.normal_motion(motion.clone(), active_operator.clone(), count, window, cx)
+                    self.normal_motion(
+                        motion.clone(),
+                        active_operator.clone(),
+                        count,
+                        forced_motion,
+                        window,
+                        cx,
+                    )
                 }
             }
             Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
@@ -1183,7 +1191,6 @@ impl Motion {
                 SelectionGoal::None,
             ),
         };
-
         (new_point != point || infallible).then_some((new_point, goal))
     }
 
@@ -1194,6 +1201,7 @@ impl Motion {
         selection: Selection<DisplayPoint>,
         times: Option<usize>,
         text_layout_details: &TextLayoutDetails,
+        forced_motion: bool,
     ) -> Option<(Range<DisplayPoint>, MotionKind)> {
         if let Motion::ZedSearchResult {
             prior_selections,
@@ -1221,18 +1229,29 @@ impl Motion {
                 return None;
             }
         }
-
-        let (new_head, goal) = self.move_point(
+        let maybe_new_point = self.move_point(
             map,
             selection.head(),
             selection.goal,
             times,
             text_layout_details,
-        )?;
+        );
+
+        let (new_head, goal) = match (maybe_new_point, forced_motion) {
+            (Some((p, g)), _) => Some((p, g)),
+            (None, false) => None,
+            (None, true) => Some((selection.head(), selection.goal)),
+        }?;
+
         let mut selection = selection.clone();
         selection.set_head(new_head, goal);
 
-        let mut kind = self.default_kind();
+        let mut kind = match (self.default_kind(), forced_motion) {
+            (MotionKind::Linewise, true) => MotionKind::Exclusive,
+            (MotionKind::Exclusive, true) => MotionKind::Inclusive,
+            (MotionKind::Inclusive, true) => MotionKind::Exclusive,
+            (kind, false) => kind,
+        };
 
         if let Motion::NextWordStart {
             ignore_punctuation: _,
@@ -1259,6 +1278,12 @@ impl Motion {
         } else if kind == MotionKind::Exclusive && !self.skip_exclusive_special_case() {
             let start_point = selection.start.to_point(map);
             let mut end_point = selection.end.to_point(map);
+            let mut next_point = selection.end;
+            *next_point.column_mut() += 1;
+            next_point = map.clip_point(next_point, Bias::Right);
+            if next_point.to_point(map) == end_point && forced_motion {
+                selection.end = movement::saturating_left(map, selection.end);
+            }
 
             if end_point.row > start_point.row {
                 let first_non_blank_of_start_row = map
@@ -1304,8 +1329,15 @@ impl Motion {
         selection: &mut Selection<DisplayPoint>,
         times: Option<usize>,
         text_layout_details: &TextLayoutDetails,
+        forced_motion: bool,
     ) -> Option<MotionKind> {
-        let (range, kind) = self.range(map, selection.clone(), times, text_layout_details)?;
+        let (range, kind) = self.range(
+            map,
+            selection.clone(),
+            times,
+            text_layout_details,
+            forced_motion,
+        )?;
         selection.start = range.start;
         selection.end = range.end;
         Some(kind)
@@ -3816,6 +3848,7 @@ mod test {
             Mode::Normal,
         );
     }
+
     #[gpui::test]
     async fn test_delete_key_can_remove_last_character(cx: &mut gpui::TestAppContext) {
         let mut cx = NeovimBackedTestContext::new(cx).await;
@@ -3823,4 +3856,147 @@ mod test {
         cx.simulate_shared_keystrokes("delete").await;
         cx.shared_state().await.assert_eq("aˇb");
     }
+
+    #[gpui::test]
+    async fn test_forced_motion_delete_to_start_of_line(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        cx.set_shared_state(indoc! {"
+             ˇthe quick brown fox
+             jumped over the lazy dog"})
+            .await;
+        cx.simulate_shared_keystrokes("d v 0").await;
+        cx.shared_state().await.assert_eq(indoc! {"
+             ˇhe quick brown fox
+             jumped over the lazy dog"});
+        assert_eq!(cx.cx.forced_motion(), false);
+
+        cx.set_shared_state(indoc! {"
+            the quick bˇrown fox
+            jumped over the lazy dog"})
+            .await;
+        cx.simulate_shared_keystrokes("d v 0").await;
+        cx.shared_state().await.assert_eq(indoc! {"
+            ˇown fox
+            jumped over the lazy dog"});
+        assert_eq!(cx.cx.forced_motion(), false);
+
+        cx.set_shared_state(indoc! {"
+            the quick brown foˇx
+            jumped over the lazy dog"})
+            .await;
+        cx.simulate_shared_keystrokes("d v 0").await;
+        cx.shared_state().await.assert_eq(indoc! {"
+            ˇ
+            jumped over the lazy dog"});
+        assert_eq!(cx.cx.forced_motion(), false);
+    }
+
+    #[gpui::test]
+    async fn test_forced_motion_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        cx.set_shared_state(indoc! {"
+             the quick brown foˇx
+             jumped over the lazy dog"})
+            .await;
+        cx.simulate_shared_keystrokes("d v $").await;
+        cx.shared_state().await.assert_eq(indoc! {"
+             the quick brown foˇx
+             jumped over the lazy dog"});
+        assert_eq!(cx.cx.forced_motion(), false);
+
+        cx.set_shared_state(indoc! {"
+             ˇthe quick brown fox
+             jumped over the lazy dog"})
+            .await;
+        cx.simulate_shared_keystrokes("d v $").await;
+        cx.shared_state().await.assert_eq(indoc! {"
+             ˇx
+             jumped over the lazy dog"});
+        assert_eq!(cx.cx.forced_motion(), false);
+    }
+
+    #[gpui::test]
+    async fn test_forced_motion_yank(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        cx.set_shared_state(indoc! {"
+               ˇthe quick brown fox
+               jumped over the lazy dog"})
+            .await;
+        cx.simulate_shared_keystrokes("y v j p").await;
+        cx.shared_state().await.assert_eq(indoc! {"
+               the quick brown fox
+               ˇthe quick brown fox
+               jumped over the lazy dog"});
+        assert_eq!(cx.cx.forced_motion(), false);
+
+        cx.set_shared_state(indoc! {"
+              the quick bˇrown fox
+              jumped over the lazy dog"})
+            .await;
+        cx.simulate_shared_keystrokes("y v j p").await;
+        cx.shared_state().await.assert_eq(indoc! {"
+              the quick brˇrown fox
+              jumped overown fox
+              jumped over the lazy dog"});
+        assert_eq!(cx.cx.forced_motion(), false);
+
+        cx.set_shared_state(indoc! {"
+             the quick brown foˇx
+             jumped over the lazy dog"})
+            .await;
+        cx.simulate_shared_keystrokes("y v j p").await;
+        cx.shared_state().await.assert_eq(indoc! {"
+             the quick brown foxˇx
+             jumped over the la
+             jumped over the lazy dog"});
+        assert_eq!(cx.cx.forced_motion(), false);
+
+        cx.set_shared_state(indoc! {"
+             the quick brown fox
+             jˇumped over the lazy dog"})
+            .await;
+        cx.simulate_shared_keystrokes("y v k p").await;
+        cx.shared_state().await.assert_eq(indoc! {"
+            thˇhe quick brown fox
+            je quick brown fox
+            jumped over the lazy dog"});
+        assert_eq!(cx.cx.forced_motion(), false);
+    }
+
+    #[gpui::test]
+    async fn test_inclusive_to_exclusive_delete(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        cx.set_shared_state(indoc! {"
+              ˇthe quick brown fox
+              jumped over the lazy dog"})
+            .await;
+        cx.simulate_shared_keystrokes("d v e").await;
+        cx.shared_state().await.assert_eq(indoc! {"
+              ˇe quick brown fox
+              jumped over the lazy dog"});
+        assert_eq!(cx.cx.forced_motion(), false);
+
+        cx.set_shared_state(indoc! {"
+              the quick bˇrown fox
+              jumped over the lazy dog"})
+            .await;
+        cx.simulate_shared_keystrokes("d v e").await;
+        cx.shared_state().await.assert_eq(indoc! {"
+              the quick bˇn fox
+              jumped over the lazy dog"});
+        assert_eq!(cx.cx.forced_motion(), false);
+
+        cx.set_shared_state(indoc! {"
+             the quick brown foˇx
+             jumped over the lazy dog"})
+            .await;
+        cx.simulate_shared_keystrokes("d v e").await;
+        cx.shared_state().await.assert_eq(indoc! {"
+        the quick brown foˇd over the lazy dog"});
+        assert_eq!(cx.cx.forced_motion(), false);
+    }
 }

crates/vim/src/normal.rs 🔗

@@ -86,12 +86,14 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
     Vim::action(editor, cx, |vim, _: &DeleteLeft, window, cx| {
         vim.record_current_action(cx);
         let times = Vim::take_count(cx);
-        vim.delete_motion(Motion::Left, times, window, cx);
+        let forced_motion = Vim::take_forced_motion(cx);
+        vim.delete_motion(Motion::Left, times, forced_motion, window, cx);
     });
     Vim::action(editor, cx, |vim, _: &DeleteRight, window, cx| {
         vim.record_current_action(cx);
         let times = Vim::take_count(cx);
-        vim.delete_motion(Motion::Right, times, window, cx);
+        let forced_motion = Vim::take_forced_motion(cx);
+        vim.delete_motion(Motion::Right, times, forced_motion, window, cx);
     });
 
     Vim::action(editor, cx, |vim, _: &HelixDelete, window, cx| {
@@ -111,11 +113,13 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
     Vim::action(editor, cx, |vim, _: &ChangeToEndOfLine, window, cx| {
         vim.start_recording(cx);
         let times = Vim::take_count(cx);
+        let forced_motion = Vim::take_forced_motion(cx);
         vim.change_motion(
             Motion::EndOfLine {
                 display_lines: false,
             },
             times,
+            forced_motion,
             window,
             cx,
         );
@@ -123,11 +127,13 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
     Vim::action(editor, cx, |vim, _: &DeleteToEndOfLine, window, cx| {
         vim.record_current_action(cx);
         let times = Vim::take_count(cx);
+        let forced_motion = Vim::take_forced_motion(cx);
         vim.delete_motion(
             Motion::EndOfLine {
                 display_lines: false,
             },
             times,
+            forced_motion,
             window,
             cx,
         );
@@ -142,6 +148,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
 
     Vim::action(editor, cx, |vim, _: &Undo, window, cx| {
         let times = Vim::take_count(cx);
+        Vim::take_forced_motion(cx);
         vim.update_editor(window, cx, |_, editor, window, cx| {
             for _ in 0..times.unwrap_or(1) {
                 editor.undo(&editor::actions::Undo, window, cx);
@@ -150,6 +157,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
     });
     Vim::action(editor, cx, |vim, _: &Redo, window, cx| {
         let times = Vim::take_count(cx);
+        Vim::take_forced_motion(cx);
         vim.update_editor(window, cx, |_, editor, window, cx| {
             for _ in 0..times.unwrap_or(1) {
                 editor.redo(&editor::actions::Redo, window, cx);
@@ -170,48 +178,93 @@ impl Vim {
         motion: Motion,
         operator: Option<Operator>,
         times: Option<usize>,
+        forced_motion: bool,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
         match operator {
             None => self.move_cursor(motion, times, window, cx),
-            Some(Operator::Change) => self.change_motion(motion, times, window, cx),
-            Some(Operator::Delete) => self.delete_motion(motion, times, window, cx),
-            Some(Operator::Yank) => self.yank_motion(motion, times, window, cx),
+            Some(Operator::Change) => self.change_motion(motion, times, forced_motion, window, cx),
+            Some(Operator::Delete) => self.delete_motion(motion, times, forced_motion, window, cx),
+            Some(Operator::Yank) => self.yank_motion(motion, times, forced_motion, window, cx),
             Some(Operator::AddSurrounds { target: None }) => {}
-            Some(Operator::Indent) => {
-                self.indent_motion(motion, times, IndentDirection::In, window, cx)
-            }
-            Some(Operator::Rewrap) => self.rewrap_motion(motion, times, window, cx),
-            Some(Operator::Outdent) => {
-                self.indent_motion(motion, times, IndentDirection::Out, window, cx)
-            }
-            Some(Operator::AutoIndent) => {
-                self.indent_motion(motion, times, IndentDirection::Auto, window, cx)
-            }
-            Some(Operator::ShellCommand) => self.shell_command_motion(motion, times, window, cx),
-            Some(Operator::Lowercase) => {
-                self.convert_motion(motion, times, ConvertTarget::LowerCase, window, cx)
-            }
-            Some(Operator::Uppercase) => {
-                self.convert_motion(motion, times, ConvertTarget::UpperCase, window, cx)
-            }
-            Some(Operator::OppositeCase) => {
-                self.convert_motion(motion, times, ConvertTarget::OppositeCase, window, cx)
-            }
-            Some(Operator::Rot13) => {
-                self.convert_motion(motion, times, ConvertTarget::Rot13, window, cx)
-            }
-            Some(Operator::Rot47) => {
-                self.convert_motion(motion, times, ConvertTarget::Rot47, window, cx)
+            Some(Operator::Indent) => self.indent_motion(
+                motion,
+                times,
+                forced_motion,
+                IndentDirection::In,
+                window,
+                cx,
+            ),
+            Some(Operator::Rewrap) => self.rewrap_motion(motion, times, forced_motion, window, cx),
+            Some(Operator::Outdent) => self.indent_motion(
+                motion,
+                times,
+                forced_motion,
+                IndentDirection::Out,
+                window,
+                cx,
+            ),
+            Some(Operator::AutoIndent) => self.indent_motion(
+                motion,
+                times,
+                forced_motion,
+                IndentDirection::Auto,
+                window,
+                cx,
+            ),
+            Some(Operator::ShellCommand) => {
+                self.shell_command_motion(motion, times, forced_motion, window, cx)
             }
+            Some(Operator::Lowercase) => self.convert_motion(
+                motion,
+                times,
+                forced_motion,
+                ConvertTarget::LowerCase,
+                window,
+                cx,
+            ),
+            Some(Operator::Uppercase) => self.convert_motion(
+                motion,
+                times,
+                forced_motion,
+                ConvertTarget::UpperCase,
+                window,
+                cx,
+            ),
+            Some(Operator::OppositeCase) => self.convert_motion(
+                motion,
+                times,
+                forced_motion,
+                ConvertTarget::OppositeCase,
+                window,
+                cx,
+            ),
+            Some(Operator::Rot13) => self.convert_motion(
+                motion,
+                times,
+                forced_motion,
+                ConvertTarget::Rot13,
+                window,
+                cx,
+            ),
+            Some(Operator::Rot47) => self.convert_motion(
+                motion,
+                times,
+                forced_motion,
+                ConvertTarget::Rot47,
+                window,
+                cx,
+            ),
             Some(Operator::ToggleComments) => {
-                self.toggle_comments_motion(motion, times, window, cx)
+                self.toggle_comments_motion(motion, times, forced_motion, window, cx)
             }
             Some(Operator::ReplaceWithRegister) => {
-                self.replace_with_register_motion(motion, times, window, cx)
+                self.replace_with_register_motion(motion, times, forced_motion, window, cx)
+            }
+            Some(Operator::Exchange) => {
+                self.exchange_motion(motion, times, forced_motion, window, cx)
             }
-            Some(Operator::Exchange) => self.exchange_motion(motion, times, window, cx),
             Some(operator) => {
                 // Can't do anything for text objects, Ignoring
                 error!("Unexpected normal mode motion operator: {:?}", operator)
@@ -492,6 +545,7 @@ impl Vim {
     ) {
         self.record_current_action(cx);
         let mut times = Vim::take_count(cx).unwrap_or(1);
+        Vim::take_forced_motion(cx);
         if self.mode.is_visual() {
             times = 1;
         } else if times > 1 {
@@ -513,11 +567,19 @@ impl Vim {
 
     fn yank_line(&mut self, _: &YankLine, window: &mut Window, cx: &mut Context<Self>) {
         let count = Vim::take_count(cx);
-        self.yank_motion(motion::Motion::CurrentLine, count, window, cx)
+        let forced_motion = Vim::take_forced_motion(cx);
+        self.yank_motion(
+            motion::Motion::CurrentLine,
+            count,
+            forced_motion,
+            window,
+            cx,
+        )
     }
 
     fn show_location(&mut self, _: &ShowLocation, window: &mut Window, cx: &mut Context<Self>) {
         let count = Vim::take_count(cx);
+        Vim::take_forced_motion(cx);
         self.update_editor(window, cx, |vim, editor, _window, cx| {
             let selection = editor.selections.newest_anchor();
             if let Some((_, buffer, _)) = editor.active_excerpt(cx) {
@@ -577,6 +639,7 @@ impl Vim {
         cx: &mut Context<Self>,
     ) {
         let count = Vim::take_count(cx).unwrap_or(1);
+        Vim::take_forced_motion(cx);
         self.stop_recording(cx);
         self.update_editor(window, cx, |_, editor, window, cx| {
             editor.transact(window, cx, |editor, window, cx| {

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

@@ -18,6 +18,7 @@ impl Vim {
         &mut self,
         motion: Motion,
         times: Option<usize>,
+        forced_motion: bool,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
@@ -59,6 +60,7 @@ impl Vim {
                                     selection,
                                     times,
                                     &text_layout_details,
+                                    forced_motion,
                                 );
                                 if let Motion::CurrentLine = motion {
                                     let mut start_offset =
@@ -181,7 +183,7 @@ fn expand_changed_word_selection(
         } else {
             Motion::NextWordStart { ignore_punctuation }
         };
-        motion.expand_selection(map, selection, times, text_layout_details)
+        motion.expand_selection(map, selection, times, text_layout_details, false)
     }
 }
 

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

@@ -25,6 +25,7 @@ impl Vim {
         &mut self,
         motion: Motion,
         times: Option<usize>,
+        forced_motion: bool,
         mode: ConvertTarget,
         window: &mut Window,
         cx: &mut Context<Self>,
@@ -39,7 +40,13 @@ impl Vim {
                     s.move_with(|map, selection| {
                         let anchor = map.display_point_to_anchor(selection.head(), Bias::Left);
                         selection_starts.insert(selection.id, anchor);
-                        motion.expand_selection(map, selection, times, &text_layout_details);
+                        motion.expand_selection(
+                            map,
+                            selection,
+                            times,
+                            &text_layout_details,
+                            forced_motion,
+                        );
                     });
                 });
                 match mode {
@@ -185,6 +192,7 @@ impl Vim {
         self.record_current_action(cx);
         self.store_visual_marks(window, cx);
         let count = Vim::take_count(cx).unwrap_or(1) as u32;
+        Vim::take_forced_motion(cx);
 
         self.update_editor(window, cx, |vim, editor, window, cx| {
             let mut ranges = Vec::new();

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

@@ -18,6 +18,7 @@ impl Vim {
         &mut self,
         motion: Motion,
         times: Option<usize>,
+        forced_motion: bool,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
@@ -33,9 +34,13 @@ impl Vim {
                     s.move_with(|map, selection| {
                         let original_head = selection.head();
                         original_columns.insert(selection.id, original_head.column());
-                        let kind =
-                            motion.expand_selection(map, selection, times, &text_layout_details);
-
+                        let kind = motion.expand_selection(
+                            map,
+                            selection,
+                            times,
+                            &text_layout_details,
+                            forced_motion,
+                        );
                         ranges_to_copy
                             .push(selection.start.to_point(map)..selection.end.to_point(map));
 

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

@@ -29,12 +29,14 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
     Vim::action(editor, cx, |vim, action: &Increment, window, cx| {
         vim.record_current_action(cx);
         let count = Vim::take_count(cx).unwrap_or(1);
+        Vim::take_forced_motion(cx);
         let step = if action.step { count as i32 } else { 0 };
         vim.increment(count as i64, step, window, cx)
     });
     Vim::action(editor, cx, |vim, action: &Decrement, window, cx| {
         vim.record_current_action(cx);
         let count = Vim::take_count(cx).unwrap_or(1);
+        Vim::take_forced_motion(cx);
         let step = if action.step { -1 * (count as i32) } else { 0 };
         vim.increment(-(count as i64), step, window, cx)
     });

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

@@ -28,6 +28,7 @@ impl Vim {
         self.record_current_action(cx);
         self.store_visual_marks(window, cx);
         let count = Vim::take_count(cx).unwrap_or(1);
+        Vim::take_forced_motion(cx);
 
         self.update_editor(window, cx, |vim, editor, window, cx| {
             let text_layout_details = editor.text_layout_details(window);
@@ -247,6 +248,7 @@ impl Vim {
         &mut self,
         motion: Motion,
         times: Option<usize>,
+        forced_motion: bool,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
@@ -258,7 +260,13 @@ impl Vim {
                 editor.set_clip_at_line_ends(false, cx);
                 editor.change_selections(None, window, cx, |s| {
                     s.move_with(|map, selection| {
-                        motion.expand_selection(map, selection, times, &text_layout_details);
+                        motion.expand_selection(
+                            map,
+                            selection,
+                            times,
+                            &text_layout_details,
+                            forced_motion,
+                        );
                     });
                 });
 

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

@@ -170,6 +170,7 @@ impl Vim {
         cx: &mut Context<Self>,
     ) {
         let mut count = Vim::take_count(cx).unwrap_or(1);
+        Vim::take_forced_motion(cx);
         self.clear_operator(window, cx);
 
         let globals = Vim::globals(cx);
@@ -201,6 +202,7 @@ impl Vim {
         cx: &mut Context<Self>,
     ) {
         let count = Vim::take_count(cx);
+        Vim::take_forced_motion(cx);
 
         let Some((mut actions, selection, mode)) = Vim::update_globals(cx, |globals, _| {
             let actions = globals.recorded_actions.clone();

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

@@ -55,6 +55,7 @@ impl Vim {
         by: fn(c: Option<f32>) -> ScrollAmount,
     ) {
         let amount = by(Vim::take_count(cx).map(|c| c as f32));
+        Vim::take_forced_motion(cx);
         self.update_editor(window, cx, |_, editor, window, cx| {
             scroll_editor(editor, move_cursor, &amount, window, cx)
         });

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

@@ -138,6 +138,7 @@ impl Vim {
             Direction::Next
         };
         let count = Vim::take_count(cx).unwrap_or(1);
+        Vim::take_forced_motion(cx);
         let prior_selections = self.editor_selections(window, cx);
         pane.update(cx, |pane, cx| {
             if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
@@ -261,6 +262,7 @@ impl Vim {
             return;
         };
         let count = Vim::take_count(cx).unwrap_or(1);
+        Vim::take_forced_motion(cx);
         let prior_selections = self.editor_selections(window, cx);
 
         let success = pane.update(cx, |pane, cx| {
@@ -303,6 +305,7 @@ impl Vim {
             return;
         };
         let count = Vim::take_count(cx).unwrap_or(1);
+        Vim::take_forced_motion(cx);
         let prior_selections = self.editor_selections(window, cx);
         let cursor_word = self.editor_cursor_word(window, cx);
         let vim = cx.entity().clone();

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

@@ -13,6 +13,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
     Vim::action(editor, cx, |vim, _: &Substitute, window, cx| {
         vim.start_recording(cx);
         let count = Vim::take_count(cx);
+        Vim::take_forced_motion(cx);
         vim.substitute(count, vim.mode == Mode::VisualLine, window, cx);
     });
 
@@ -22,6 +23,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
             vim.switch_mode(Mode::VisualLine, false, window, cx)
         }
         let count = Vim::take_count(cx);
+        Vim::take_forced_motion(cx);
         vim.substitute(count, true, window, cx)
     });
 }
@@ -47,6 +49,7 @@ impl Vim {
                                 selection,
                                 count,
                                 &text_layout_details,
+                                false,
                             );
                         }
                         if line_mode {
@@ -60,6 +63,7 @@ impl Vim {
                                 selection,
                                 None,
                                 &text_layout_details,
+                                false,
                             );
                             if let Some((point, _)) = (Motion::FirstNonWhitespace {
                                 display_lines: false,

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

@@ -9,6 +9,7 @@ impl Vim {
         &mut self,
         motion: Motion,
         times: Option<usize>,
+        forced_motion: bool,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
@@ -21,7 +22,13 @@ impl Vim {
                     s.move_with(|map, selection| {
                         let anchor = map.display_point_to_anchor(selection.head(), Bias::Right);
                         selection_starts.insert(selection.id, anchor);
-                        motion.expand_selection(map, selection, times, &text_layout_details);
+                        motion.expand_selection(
+                            map,
+                            selection,
+                            times,
+                            &text_layout_details,
+                            forced_motion,
+                        );
                     });
                 });
                 editor.toggle_comments(&Default::default(), window, cx);

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

@@ -21,6 +21,7 @@ impl Vim {
         &mut self,
         motion: Motion,
         times: Option<usize>,
+        forced_motion: bool,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
@@ -33,8 +34,19 @@ impl Vim {
                 editor.change_selections(None, window, cx, |s| {
                     s.move_with(|map, selection| {
                         let original_position = (selection.head(), selection.goal);
-                        original_positions.insert(selection.id, original_position);
-                        kind = motion.expand_selection(map, selection, times, &text_layout_details);
+                        kind = motion.expand_selection(
+                            map,
+                            selection,
+                            times,
+                            &text_layout_details,
+                            forced_motion,
+                        );
+                        if kind == Some(MotionKind::Exclusive) {
+                            original_positions
+                                .insert(selection.id, (selection.start, selection.goal));
+                        } else {
+                            original_positions.insert(selection.id, original_position);
+                        }
                     })
                 });
                 let Some(kind) = kind else { return };

crates/vim/src/replace.rs 🔗

@@ -27,6 +27,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
             return;
         }
         let count = Vim::take_count(cx);
+        Vim::take_forced_motion(cx);
         vim.undo_replace(count, window, cx)
     });
 }
@@ -179,6 +180,7 @@ impl Vim {
         &mut self,
         motion: Motion,
         times: Option<usize>,
+        forced_motion: bool,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
@@ -188,7 +190,13 @@ impl Vim {
             let text_layout_details = editor.text_layout_details(window);
             let mut selection = editor.selections.newest_display(cx);
             let snapshot = editor.snapshot(window, cx);
-            motion.expand_selection(&snapshot, &mut selection, times, &text_layout_details);
+            motion.expand_selection(
+                &snapshot,
+                &mut selection,
+                times,
+                &text_layout_details,
+                forced_motion,
+            );
             let start = snapshot
                 .buffer_snapshot
                 .anchor_before(selection.start.to_point(&snapshot));

crates/vim/src/rewrap.rs 🔗

@@ -10,6 +10,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
     Vim::action(editor, cx, |vim, _: &Rewrap, window, cx| {
         vim.record_current_action(cx);
         Vim::take_count(cx);
+        Vim::take_forced_motion(cx);
         vim.store_visual_marks(window, cx);
         vim.update_editor(window, cx, |vim, editor, window, cx| {
             editor.transact(window, cx, |editor, window, cx| {
@@ -43,6 +44,7 @@ impl Vim {
         &mut self,
         motion: Motion,
         times: Option<usize>,
+        forced_motion: bool,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
@@ -55,7 +57,13 @@ impl Vim {
                     s.move_with(|map, selection| {
                         let anchor = map.display_point_to_anchor(selection.head(), Bias::Right);
                         selection_starts.insert(selection.id, anchor);
-                        motion.expand_selection(map, selection, times, &text_layout_details);
+                        motion.expand_selection(
+                            map,
+                            selection,
+                            times,
+                            &text_layout_details,
+                            forced_motion,
+                        );
                     });
                 });
                 editor.rewrap_impl(

crates/vim/src/state.rs 🔗

@@ -202,7 +202,7 @@ pub struct VimGlobals {
     pub pre_count: Option<usize>,
     /// post_count is the number after an operator is specified (2 in 3d2d)
     pub post_count: Option<usize>,
-
+    pub forced_motion: bool,
     pub stop_recording_after_next_action: bool,
     pub ignore_current_insertion: bool,
     pub recorded_count: Option<usize>,

crates/vim/src/surrounds.rs 🔗

@@ -27,6 +27,7 @@ impl Vim {
     ) {
         self.stop_recording(cx);
         let count = Vim::take_count(cx);
+        let forced_motion = Vim::take_forced_motion(cx);
         let mode = self.mode;
         self.update_editor(window, cx, |_, editor, window, cx| {
             let text_layout_details = editor.text_layout_details(window);
@@ -55,7 +56,13 @@ impl Vim {
                         }
                         SurroundsType::Motion(motion) => {
                             motion
-                                .range(&display_map, selection.clone(), count, &text_layout_details)
+                                .range(
+                                    &display_map,
+                                    selection.clone(),
+                                    count,
+                                    &text_layout_details,
+                                    forced_motion,
+                                )
                                 .map(|(mut range, _)| {
                                     // The Motion::CurrentLine operation will contain the newline of the current line and leading/trailing whitespace
                                     if let Motion::CurrentLine = motion {

crates/vim/src/test/neovim_backed_test_context.rs 🔗

@@ -13,7 +13,7 @@ use super::{VimTestContext, neovim_connection::NeovimConnection};
 use crate::state::{Mode, VimGlobals};
 
 pub struct NeovimBackedTestContext {
-    cx: VimTestContext,
+    pub(crate) cx: VimTestContext,
     pub(crate) neovim: NeovimConnection,
 
     last_set_state: Option<String>,

crates/vim/src/test/vim_test_context.rs 🔗

@@ -142,6 +142,10 @@ impl VimTestContext {
         self.update_editor(|editor, _, cx| editor.addon::<VimAddon>().unwrap().entity.read(cx).mode)
     }
 
+    pub fn forced_motion(&mut self) -> bool {
+        self.update_editor(|_, _, cx| cx.global::<VimGlobals>().forced_motion)
+    }
+
     pub fn active_operator(&mut self) -> Option<Operator> {
         self.update_editor(|editor, _, cx| {
             editor

crates/vim/src/vim.rs 🔗

@@ -145,6 +145,7 @@ actions!(
         PushDeleteSurrounds,
         PushMark,
         ToggleMarksView,
+        PushForcedMotion,
         PushIndent,
         PushOutdent,
         PushAutoIndent,
@@ -233,6 +234,7 @@ pub fn init(cx: &mut App) {
 
         workspace.register_action(|workspace, _: &ResizePaneRight, window, cx| {
             let count = Vim::take_count(cx).unwrap_or(1) as f32;
+            Vim::take_forced_motion(cx);
             let theme = ThemeSettings::get_global(cx);
             let Ok(font_id) = window.text_system().font_id(&theme.buffer_font) else {
                 return;
@@ -248,6 +250,7 @@ pub fn init(cx: &mut App) {
 
         workspace.register_action(|workspace, _: &ResizePaneLeft, window, cx| {
             let count = Vim::take_count(cx).unwrap_or(1) as f32;
+            Vim::take_forced_motion(cx);
             let theme = ThemeSettings::get_global(cx);
             let Ok(font_id) = window.text_system().font_id(&theme.buffer_font) else {
                 return;
@@ -263,6 +266,7 @@ pub fn init(cx: &mut App) {
 
         workspace.register_action(|workspace, _: &ResizePaneUp, window, cx| {
             let count = Vim::take_count(cx).unwrap_or(1) as f32;
+            Vim::take_forced_motion(cx);
             let theme = ThemeSettings::get_global(cx);
             let height = theme.buffer_font_size(cx) * theme.buffer_line_height.value();
             workspace.resize_pane(Axis::Vertical, height * count, window, cx);
@@ -270,6 +274,7 @@ pub fn init(cx: &mut App) {
 
         workspace.register_action(|workspace, _: &ResizePaneDown, window, cx| {
             let count = Vim::take_count(cx).unwrap_or(1) as f32;
+            Vim::take_forced_motion(cx);
             let theme = ThemeSettings::get_global(cx);
             let height = theme.buffer_font_size(cx) * theme.buffer_line_height.value();
             workspace.resize_pane(Axis::Vertical, -height * count, window, cx);
@@ -472,7 +477,9 @@ impl Vim {
                     vim.switch_mode(Mode::HelixNormal, false, window, cx)
                 },
             );
-
+            Vim::action(editor, cx, |_, _: &PushForcedMotion, _, cx| {
+                Vim::globals(cx).forced_motion = true;
+            });
             Vim::action(editor, cx, |vim, action: &PushObject, window, cx| {
                 vim.push_operator(
                     Operator::Object {
@@ -907,6 +914,7 @@ impl Vim {
             self.current_tx.take();
             self.current_anchor.take();
         }
+        Vim::take_forced_motion(cx);
         if mode != Mode::Insert && mode != Mode::Replace {
             Vim::take_count(cx);
         }
@@ -1011,6 +1019,13 @@ impl Vim {
         count
     }
 
+    pub fn take_forced_motion(cx: &mut App) -> bool {
+        let global_state = cx.global_mut::<VimGlobals>();
+        let forced_motion = global_state.forced_motion;
+        global_state.forced_motion = false;
+        forced_motion
+    }
+
     pub fn cursor_shape(&self, cx: &mut App) -> CursorShape {
         match self.mode {
             Mode::Normal => {
@@ -1372,6 +1387,7 @@ impl Vim {
 
     fn clear_operator(&mut self, window: &mut Window, cx: &mut Context<Self>) {
         Vim::take_count(cx);
+        Vim::take_forced_motion(cx);
         self.selected_register.take();
         self.operator_stack.clear();
         self.sync_vim_settings(window, cx);

crates/vim/src/visual.rs 🔗

@@ -85,6 +85,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
 
     Vim::action(editor, cx, |vim, _: &SelectLargerSyntaxNode, window, cx| {
         let count = Vim::take_count(cx).unwrap_or(1);
+        Vim::take_forced_motion(cx);
         for _ in 0..count {
             vim.update_editor(window, cx, |_, editor, window, cx| {
                 editor.select_larger_syntax_node(&Default::default(), window, cx);
@@ -97,6 +98,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
         cx,
         |vim, _: &SelectSmallerSyntaxNode, window, cx| {
             let count = Vim::take_count(cx).unwrap_or(1);
+            Vim::take_forced_motion(cx);
             for _ in 0..count {
                 vim.update_editor(window, cx, |_, editor, window, cx| {
                     editor.select_smaller_syntax_node(&Default::default(), window, cx);
@@ -682,6 +684,7 @@ impl Vim {
     }
 
     pub fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
+        Vim::take_forced_motion(cx);
         let count =
             Vim::take_count(cx).unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 });
         self.update_editor(window, cx, |_, editor, window, cx| {
@@ -704,6 +707,7 @@ impl Vim {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
+        Vim::take_forced_motion(cx);
         let count =
             Vim::take_count(cx).unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 });
         self.update_editor(window, cx, |_, editor, window, cx| {
@@ -725,6 +729,7 @@ impl Vim {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
+        Vim::take_forced_motion(cx);
         let count = Vim::take_count(cx).unwrap_or(1);
         let Some(pane) = self.pane(window, cx) else {
             return;

crates/vim/test_data/test_forced_motion_delete_to_end_of_line.json 🔗

@@ -0,0 +1,10 @@
+{"Put":{"state":"the quick brown foˇx\njumped over the lazy dog"}}
+{"Key":"d"}
+{"Key":"v"}
+{"Key":"$"}
+{"Get":{"state":"the quick brown foˇx\njumped over the lazy dog","mode":"Normal"}}
+{"Put":{"state":"ˇthe quick brown fox\njumped over the lazy dog"}}
+{"Key":"d"}
+{"Key":"v"}
+{"Key":"$"}
+{"Get":{"state":"ˇx\njumped over the lazy dog","mode":"Normal"}}

crates/vim/test_data/test_forced_motion_delete_to_start_of_line.json 🔗

@@ -0,0 +1,15 @@
+{"Put":{"state":"ˇthe quick brown fox\njumped over the lazy dog"}}
+{"Key":"d"}
+{"Key":"v"}
+{"Key":"0"}
+{"Get":{"state":"ˇhe quick brown fox\njumped over the lazy dog","mode":"Normal"}}
+{"Put":{"state":"the quick bˇrown fox\njumped over the lazy dog"}}
+{"Key":"d"}
+{"Key":"v"}
+{"Key":"0"}
+{"Get":{"state":"ˇown fox\njumped over the lazy dog","mode":"Normal"}}
+{"Put":{"state":"the quick brown foˇx\njumped over the lazy dog"}}
+{"Key":"d"}
+{"Key":"v"}
+{"Key":"0"}
+{"Get":{"state":"ˇ\njumped over the lazy dog","mode":"Normal"}}

crates/vim/test_data/test_forced_motion_yank.json 🔗

@@ -0,0 +1,24 @@
+{"Put":{"state":"ˇthe quick brown fox\njumped over the lazy dog"}}
+{"Key":"y"}
+{"Key":"v"}
+{"Key":"j"}
+{"Key":"p"}
+{"Get":{"state":"the quick brown fox\nˇthe quick brown fox\njumped over the lazy dog","mode":"Normal"}}
+{"Put":{"state":"the quick bˇrown fox\njumped over the lazy dog"}}
+{"Key":"y"}
+{"Key":"v"}
+{"Key":"j"}
+{"Key":"p"}
+{"Get":{"state":"the quick brˇrown fox\njumped overown fox\njumped over the lazy dog","mode":"Normal"}}
+{"Put":{"state":"the quick brown foˇx\njumped over the lazy dog"}}
+{"Key":"y"}
+{"Key":"v"}
+{"Key":"j"}
+{"Key":"p"}
+{"Get":{"state":"the quick brown foxˇx\njumped over the la\njumped over the lazy dog","mode":"Normal"}}
+{"Put":{"state":"the quick brown fox\njˇumped over the lazy dog"}}
+{"Key":"y"}
+{"Key":"v"}
+{"Key":"k"}
+{"Key":"p"}
+{"Get":{"state":"thˇhe quick brown fox\nje quick brown fox\njumped over the lazy dog","mode":"Normal"}}

crates/vim/test_data/test_inclusive_to_exclusive_delete.json 🔗

@@ -0,0 +1,15 @@
+{"Put":{"state":"ˇthe quick brown fox\njumped over the lazy dog"}}
+{"Key":"d"}
+{"Key":"v"}
+{"Key":"e"}
+{"Get":{"state":"ˇe quick brown fox\njumped over the lazy dog","mode":"Normal"}}
+{"Put":{"state":"the quick bˇrown fox\njumped over the lazy dog"}}
+{"Key":"d"}
+{"Key":"v"}
+{"Key":"e"}
+{"Get":{"state":"the quick bˇn fox\njumped over the lazy dog","mode":"Normal"}}
+{"Put":{"state":"the quick brown foˇx\njumped over the lazy dog"}}
+{"Key":"d"}
+{"Key":"v"}
+{"Key":"e"}
+{"Get":{"state":"the quick brown foˇd over the lazy dog","mode":"Normal"}}