vim: Make each vim repeat its own transaction (#41735)

AidanV and Conrad Irwin created

Release Notes:

- Pressing `u` after multiple `.` in rapid succession will now only undo
the latest repeat instead of all repeats.

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>

Change summary

crates/vim/src/normal/repeat.rs                      | 19 +++++++++++++
crates/vim/src/test.rs                               | 16 +++++++++++
crates/vim/test_data/test_repeat_grouping_41735.json | 10 +++++++
3 files changed, 44 insertions(+), 1 deletion(-)

Detailed changes

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

@@ -110,7 +110,24 @@ impl Replayer {
         }
         lock.running = true;
         let this = self.clone();
-        window.defer(cx, move |window, cx| this.next(window, cx))
+        window.defer(cx, move |window, cx| {
+            this.next(window, cx);
+            let Some(Some(workspace)) = window.root::<Workspace>() else {
+                return;
+            };
+            let Some(editor) = workspace
+                .read(cx)
+                .active_item(cx)
+                .and_then(|item| item.act_as::<Editor>(cx))
+            else {
+                return;
+            };
+            editor.update(cx, |editor, cx| {
+                editor
+                    .buffer()
+                    .update(cx, |multi, cx| multi.finalize_last_transaction(cx))
+            });
+        })
     }
 
     pub fn stop(self) {

crates/vim/src/test.rs 🔗

@@ -2365,3 +2365,19 @@ async fn test_wrap_selections_in_tag_line_mode(cx: &mut gpui::TestAppContext) {
         Mode::VisualLine,
     );
 }
+
+#[gpui::test]
+async fn test_repeat_grouping_41735(cx: &mut gpui::TestAppContext) {
+    let mut cx = NeovimBackedTestContext::new(cx).await;
+
+    // typically transaction gropuing is disabled in tests, but here we need to test it.
+    cx.update_buffer(|buffer, _cx| buffer.set_group_interval(Duration::from_millis(300)));
+
+    cx.set_shared_state("ˇ").await;
+
+    cx.simulate_shared_keystrokes("i a escape").await;
+    cx.simulate_shared_keystrokes(". . .").await;
+    cx.shared_state().await.assert_eq("ˇaaaa");
+    cx.simulate_shared_keystrokes("u").await;
+    cx.shared_state().await.assert_eq("ˇaaa");
+}

crates/vim/test_data/test_repeat_grouping_41735.json 🔗

@@ -0,0 +1,10 @@
+{"Put":{"state":"ˇ"}}
+{"Key":"i"}
+{"Key":"a"}
+{"Key":"escape"}
+{"Key":"."}
+{"Key":"."}
+{"Key":"."}
+{"Get":{"state":"ˇaaaa","mode":"Normal"}}
+{"Key":"u"}
+{"Get":{"state":"ˇaaa","mode":"Normal"}}