repeat.rs

  1use std::{cell::RefCell, rc::Rc};
  2
  3use crate::{
  4    Vim,
  5    insert::NormalBefore,
  6    motion::Motion,
  7    normal::InsertBefore,
  8    state::{Mode, Operator, RecordedSelection, ReplayableAction, VimGlobals},
  9};
 10use editor::Editor;
 11use gpui::{Action, App, Context, Window, actions};
 12use workspace::Workspace;
 13
 14actions!(
 15    vim,
 16    [
 17        /// Repeats the last change.
 18        Repeat,
 19        /// Ends the repeat recording.
 20        EndRepeat,
 21        /// Toggles macro recording.
 22        ToggleRecord,
 23        /// Replays the last recorded macro.
 24        ReplayLastRecording
 25    ]
 26);
 27
 28fn should_replay(action: &dyn Action) -> bool {
 29    // skip so that we don't leave the character palette open
 30    if editor::actions::ShowCharacterPalette.partial_eq(action) {
 31        return false;
 32    }
 33    true
 34}
 35
 36fn repeatable_insert(action: &ReplayableAction) -> Option<Box<dyn Action>> {
 37    match action {
 38        ReplayableAction::Action(action) => {
 39            if super::InsertBefore.partial_eq(&**action)
 40                || super::InsertAfter.partial_eq(&**action)
 41                || super::InsertFirstNonWhitespace.partial_eq(&**action)
 42                || super::InsertEndOfLine.partial_eq(&**action)
 43            {
 44                Some(super::InsertBefore.boxed_clone())
 45            } else if super::InsertLineAbove.partial_eq(&**action)
 46                || super::InsertLineBelow.partial_eq(&**action)
 47            {
 48                Some(super::InsertLineBelow.boxed_clone())
 49            } else if crate::replace::ToggleReplace.partial_eq(&**action) {
 50                Some(crate::replace::ToggleReplace.boxed_clone())
 51            } else {
 52                None
 53            }
 54        }
 55        ReplayableAction::Insertion { .. } => None,
 56    }
 57}
 58
 59pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
 60    Vim::action(editor, cx, |vim, _: &EndRepeat, window, cx| {
 61        Vim::globals(cx).dot_replaying = false;
 62        vim.switch_mode(Mode::Normal, false, window, cx)
 63    });
 64
 65    Vim::action(editor, cx, |vim, _: &Repeat, window, cx| {
 66        vim.repeat(false, window, cx)
 67    });
 68
 69    Vim::action(editor, cx, |vim, _: &ToggleRecord, window, cx| {
 70        let globals = Vim::globals(cx);
 71        if let Some(char) = globals.recording_register.take() {
 72            globals.last_recorded_register = Some(char)
 73        } else {
 74            vim.push_operator(Operator::RecordRegister, window, cx);
 75        }
 76    });
 77
 78    Vim::action(editor, cx, |vim, _: &ReplayLastRecording, window, cx| {
 79        let Some(register) = Vim::globals(cx).last_recorded_register else {
 80            return;
 81        };
 82        vim.replay_register(register, window, cx)
 83    });
 84}
 85
 86pub struct ReplayerState {
 87    actions: Vec<ReplayableAction>,
 88    running: bool,
 89    ix: usize,
 90}
 91
 92#[derive(Clone)]
 93pub struct Replayer(Rc<RefCell<ReplayerState>>);
 94
 95impl Replayer {
 96    pub fn new() -> Self {
 97        Self(Rc::new(RefCell::new(ReplayerState {
 98            actions: vec![],
 99            running: false,
100            ix: 0,
101        })))
102    }
103
104    pub fn replay(&mut self, actions: Vec<ReplayableAction>, window: &mut Window, cx: &mut App) {
105        let mut lock = self.0.borrow_mut();
106        let range = lock.ix..lock.ix;
107        lock.actions.splice(range, actions);
108        if lock.running {
109            return;
110        }
111        lock.running = true;
112        let this = self.clone();
113        window.defer(cx, move |window, cx| {
114            this.next(window, cx);
115            let Some(Some(workspace)) = window.root::<Workspace>() else {
116                return;
117            };
118            let Some(editor) = workspace
119                .read(cx)
120                .active_item(cx)
121                .and_then(|item| item.act_as::<Editor>(cx))
122            else {
123                return;
124            };
125            editor.update(cx, |editor, cx| {
126                editor
127                    .buffer()
128                    .update(cx, |multi, cx| multi.finalize_last_transaction(cx))
129            });
130        })
131    }
132
133    pub fn stop(self) {
134        self.0.borrow_mut().actions.clear()
135    }
136
137    pub fn next(self, window: &mut Window, cx: &mut App) {
138        let mut lock = self.0.borrow_mut();
139        let action = if lock.ix < 10000 {
140            lock.actions.get(lock.ix).cloned()
141        } else {
142            log::error!("Aborting replay after 10000 actions");
143            None
144        };
145        lock.ix += 1;
146        drop(lock);
147        let Some(action) = action else {
148            Vim::globals(cx).replayer.take();
149            return;
150        };
151        match action {
152            ReplayableAction::Action(action) => {
153                if should_replay(&*action) {
154                    window.dispatch_action(action.boxed_clone(), cx);
155                    cx.defer(move |cx| Vim::globals(cx).observe_action(action.boxed_clone()));
156                }
157            }
158            ReplayableAction::Insertion {
159                text,
160                utf16_range_to_replace,
161            } => {
162                let Some(Some(workspace)) = window.root::<Workspace>() else {
163                    return;
164                };
165                let Some(editor) = workspace
166                    .read(cx)
167                    .active_item(cx)
168                    .and_then(|item| item.act_as::<Editor>(cx))
169                else {
170                    return;
171                };
172                editor.update(cx, |editor, cx| {
173                    editor.replay_insert_event(&text, utf16_range_to_replace.clone(), window, cx)
174                })
175            }
176        }
177        window.defer(cx, move |window, cx| self.next(window, cx));
178    }
179}
180
181impl Vim {
182    pub(crate) fn record_register(
183        &mut self,
184        register: char,
185        window: &mut Window,
186        cx: &mut Context<Self>,
187    ) {
188        let globals = Vim::globals(cx);
189        globals.recording_register = Some(register);
190        globals.recordings.remove(&register);
191        globals.ignore_current_insertion = true;
192        self.clear_operator(window, cx)
193    }
194
195    pub(crate) fn replay_register(
196        &mut self,
197        mut register: char,
198        window: &mut Window,
199        cx: &mut Context<Self>,
200    ) {
201        let mut count = Vim::take_count(cx).unwrap_or(1);
202        Vim::take_forced_motion(cx);
203        self.clear_operator(window, cx);
204
205        let globals = Vim::globals(cx);
206        if register == '@' {
207            let Some(last) = globals.last_replayed_register else {
208                return;
209            };
210            register = last;
211        }
212        let Some(actions) = globals.recordings.get(&register) else {
213            return;
214        };
215
216        let mut repeated_actions = vec![];
217        while count > 0 {
218            repeated_actions.extend(actions.iter().cloned());
219            count -= 1
220        }
221
222        globals.last_replayed_register = Some(register);
223        let mut replayer = globals.replayer.get_or_insert_with(Replayer::new).clone();
224        replayer.replay(repeated_actions, window, cx);
225    }
226
227    pub(crate) fn repeat(
228        &mut self,
229        from_insert_mode: bool,
230        window: &mut Window,
231        cx: &mut Context<Self>,
232    ) {
233        let count = Vim::take_count(cx);
234        Vim::take_forced_motion(cx);
235
236        let Some((mut actions, selection, mode)) = Vim::update_globals(cx, |globals, _| {
237            let actions = globals.recorded_actions.clone();
238            if actions.is_empty() {
239                return None;
240            }
241            if globals.replayer.is_none()
242                && let Some(recording_register) = globals.recording_register
243            {
244                globals
245                    .recordings
246                    .entry(recording_register)
247                    .or_default()
248                    .push(ReplayableAction::Action(Repeat.boxed_clone()));
249            }
250
251            let mut mode = None;
252            let selection = globals.recorded_selection.clone();
253            match selection {
254                RecordedSelection::SingleLine { .. } | RecordedSelection::Visual { .. } => {
255                    globals.recorded_count = None;
256                    mode = Some(Mode::Visual);
257                }
258                RecordedSelection::VisualLine { .. } => {
259                    globals.recorded_count = None;
260                    mode = Some(Mode::VisualLine)
261                }
262                RecordedSelection::VisualBlock { .. } => {
263                    globals.recorded_count = None;
264                    mode = Some(Mode::VisualBlock)
265                }
266                RecordedSelection::None => {
267                    if let Some(count) = count {
268                        globals.recorded_count = Some(count);
269                    }
270                }
271            }
272
273            Some((actions, selection, mode))
274        }) else {
275            return;
276        };
277        if mode != Some(self.mode) {
278            if let Some(mode) = mode {
279                self.switch_mode(mode, false, window, cx)
280            }
281
282            match selection {
283                RecordedSelection::SingleLine { cols } => {
284                    if cols > 1 {
285                        self.visual_motion(Motion::Right, Some(cols as usize - 1), window, cx)
286                    }
287                }
288                RecordedSelection::Visual { rows, cols } => {
289                    self.visual_motion(
290                        Motion::Down {
291                            display_lines: false,
292                        },
293                        Some(rows as usize),
294                        window,
295                        cx,
296                    );
297                    self.visual_motion(
298                        Motion::StartOfLine {
299                            display_lines: false,
300                        },
301                        None,
302                        window,
303                        cx,
304                    );
305                    if cols > 1 {
306                        self.visual_motion(Motion::Right, Some(cols as usize - 1), window, cx)
307                    }
308                }
309                RecordedSelection::VisualBlock { rows, cols } => {
310                    self.visual_motion(
311                        Motion::Down {
312                            display_lines: false,
313                        },
314                        Some(rows as usize),
315                        window,
316                        cx,
317                    );
318                    if cols > 1 {
319                        self.visual_motion(Motion::Right, Some(cols as usize - 1), window, cx);
320                    }
321                }
322                RecordedSelection::VisualLine { rows } => {
323                    self.visual_motion(
324                        Motion::Down {
325                            display_lines: false,
326                        },
327                        Some(rows as usize),
328                        window,
329                        cx,
330                    );
331                }
332                RecordedSelection::None => {}
333            }
334        }
335
336        // insert internally uses repeat to handle counts
337        // vim doesn't treat 3a1 as though you literally repeated a1
338        // 3 times, instead it inserts the content thrice at the insert position.
339        if let Some(to_repeat) = repeatable_insert(&actions[0]) {
340            if let Some(ReplayableAction::Action(action)) = actions.last()
341                && NormalBefore.partial_eq(&**action)
342            {
343                actions.pop();
344            }
345
346            let mut new_actions = actions.clone();
347            actions[0] = ReplayableAction::Action(to_repeat.boxed_clone());
348
349            let mut count = cx.global::<VimGlobals>().recorded_count.unwrap_or(1);
350
351            // if we came from insert mode we're just doing repetitions 2 onwards.
352            if from_insert_mode {
353                count -= 1;
354                new_actions[0] = actions[0].clone();
355            }
356
357            for _ in 1..count {
358                new_actions.append(actions.clone().as_mut());
359            }
360            new_actions.push(ReplayableAction::Action(NormalBefore.boxed_clone()));
361            actions = new_actions;
362        }
363
364        actions.push(ReplayableAction::Action(EndRepeat.boxed_clone()));
365
366        if self.temp_mode {
367            self.temp_mode = false;
368            actions.push(ReplayableAction::Action(InsertBefore.boxed_clone()));
369        }
370
371        let globals = Vim::globals(cx);
372        globals.dot_replaying = true;
373        let mut replayer = globals.replayer.get_or_insert_with(Replayer::new).clone();
374
375        replayer.replay(actions, window, cx);
376    }
377}
378
379#[cfg(test)]
380mod test {
381    use editor::test::editor_lsp_test_context::EditorLspTestContext;
382    use futures::StreamExt;
383    use indoc::indoc;
384
385    use gpui::EntityInputHandler;
386
387    use crate::{
388        state::Mode,
389        test::{NeovimBackedTestContext, VimTestContext},
390    };
391
392    #[gpui::test]
393    async fn test_dot_repeat(cx: &mut gpui::TestAppContext) {
394        let mut cx = NeovimBackedTestContext::new(cx).await;
395
396        // "o"
397        cx.set_shared_state("ˇhello").await;
398        cx.simulate_shared_keystrokes("o w o r l d escape").await;
399        cx.shared_state().await.assert_eq("hello\nworlˇd");
400        cx.simulate_shared_keystrokes(".").await;
401        cx.shared_state().await.assert_eq("hello\nworld\nworlˇd");
402
403        // "d"
404        cx.simulate_shared_keystrokes("^ d f o").await;
405        cx.simulate_shared_keystrokes("g g .").await;
406        cx.shared_state().await.assert_eq("ˇ\nworld\nrld");
407
408        // "p" (note that it pastes the current clipboard)
409        cx.simulate_shared_keystrokes("j y y p").await;
410        cx.simulate_shared_keystrokes("shift-g y y .").await;
411        cx.shared_state()
412            .await
413            .assert_eq("\nworld\nworld\nrld\nˇrld");
414
415        // "~" (note that counts apply to the action taken, not . itself)
416        cx.set_shared_state("ˇthe quick brown fox").await;
417        cx.simulate_shared_keystrokes("2 ~ .").await;
418        cx.set_shared_state("THE ˇquick brown fox").await;
419        cx.simulate_shared_keystrokes("3 .").await;
420        cx.set_shared_state("THE QUIˇck brown fox").await;
421        cx.run_until_parked();
422        cx.simulate_shared_keystrokes(".").await;
423        cx.shared_state().await.assert_eq("THE QUICK ˇbrown fox");
424    }
425
426    #[gpui::test]
427    async fn test_repeat_ime(cx: &mut gpui::TestAppContext) {
428        let mut cx = VimTestContext::new(cx, true).await;
429
430        cx.set_state("hˇllo", Mode::Normal);
431        cx.simulate_keystrokes("i");
432
433        // simulate brazilian input for ä.
434        cx.update_editor(|editor, window, cx| {
435            editor.replace_and_mark_text_in_range(None, "\"", Some(1..1), window, cx);
436            editor.replace_text_in_range(None, "ä", window, cx);
437        });
438        cx.simulate_keystrokes("escape");
439        cx.assert_state("hˇällo", Mode::Normal);
440        cx.simulate_keystrokes(".");
441        cx.assert_state("hˇäällo", Mode::Normal);
442    }
443
444    #[gpui::test]
445    async fn test_repeat_completion(cx: &mut gpui::TestAppContext) {
446        VimTestContext::init(cx);
447        let cx = EditorLspTestContext::new_rust(
448            lsp::ServerCapabilities {
449                completion_provider: Some(lsp::CompletionOptions {
450                    trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
451                    resolve_provider: Some(true),
452                    ..Default::default()
453                }),
454                ..Default::default()
455            },
456            cx,
457        )
458        .await;
459        let mut cx = VimTestContext::new_with_lsp(cx, true);
460
461        cx.set_state(
462            indoc! {"
463            onˇe
464            two
465            three
466        "},
467            Mode::Normal,
468        );
469
470        let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(
471            move |_, params, _| async move {
472                let position = params.text_document_position.position;
473                Ok(Some(lsp::CompletionResponse::Array(vec![
474                    lsp::CompletionItem {
475                        label: "first".to_string(),
476                        text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
477                            range: lsp::Range::new(position, position),
478                            new_text: "first".to_string(),
479                        })),
480                        ..Default::default()
481                    },
482                    lsp::CompletionItem {
483                        label: "second".to_string(),
484                        text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
485                            range: lsp::Range::new(position, position),
486                            new_text: "second".to_string(),
487                        })),
488                        ..Default::default()
489                    },
490                ])))
491            },
492        );
493        cx.simulate_keystrokes("a .");
494        request.next().await;
495        cx.condition(|editor, _| editor.context_menu_visible())
496            .await;
497        cx.simulate_keystrokes("down enter ! escape");
498
499        cx.assert_state(
500            indoc! {"
501                one.secondˇ!
502                two
503                three
504            "},
505            Mode::Normal,
506        );
507        cx.simulate_keystrokes("j .");
508        cx.assert_state(
509            indoc! {"
510                one.second!
511                two.secondˇ!
512                three
513            "},
514            Mode::Normal,
515        );
516    }
517
518    #[gpui::test]
519    async fn test_repeat_completion_unicode_bug(cx: &mut gpui::TestAppContext) {
520        VimTestContext::init(cx);
521        let cx = EditorLspTestContext::new_rust(
522            lsp::ServerCapabilities {
523                completion_provider: Some(lsp::CompletionOptions {
524                    trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
525                    resolve_provider: Some(true),
526                    ..Default::default()
527                }),
528                ..Default::default()
529            },
530            cx,
531        )
532        .await;
533        let mut cx = VimTestContext::new_with_lsp(cx, true);
534
535        cx.set_state(
536            indoc! {"
537                ĩлˇк
538                ĩлк
539            "},
540            Mode::Normal,
541        );
542
543        let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(
544            move |_, params, _| async move {
545                let position = params.text_document_position.position;
546                let mut to_the_left = position;
547                to_the_left.character -= 2;
548                Ok(Some(lsp::CompletionResponse::Array(vec![
549                    lsp::CompletionItem {
550                        label: "oops".to_string(),
551                        text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
552                            range: lsp::Range::new(to_the_left, position),
553                            new_text: "к!".to_string(),
554                        })),
555                        ..Default::default()
556                    },
557                ])))
558            },
559        );
560        cx.simulate_keystrokes("i .");
561        request.next().await;
562        cx.condition(|editor, _| editor.context_menu_visible())
563            .await;
564        cx.simulate_keystrokes("enter escape");
565        cx.assert_state(
566            indoc! {"
567                ĩкˇ!к
568                ĩлк
569            "},
570            Mode::Normal,
571        );
572    }
573
574    #[gpui::test]
575    async fn test_repeat_visual(cx: &mut gpui::TestAppContext) {
576        let mut cx = NeovimBackedTestContext::new(cx).await;
577
578        // single-line (3 columns)
579        cx.set_shared_state(indoc! {
580            "ˇthe quick brown
581            fox jumps over
582            the lazy dog"
583        })
584        .await;
585        cx.simulate_shared_keystrokes("v i w s o escape").await;
586        cx.shared_state().await.assert_eq(indoc! {
587            "ˇo quick brown
588            fox jumps over
589            the lazy dog"
590        });
591        cx.simulate_shared_keystrokes("j w .").await;
592        cx.shared_state().await.assert_eq(indoc! {
593            "o quick brown
594            fox ˇops over
595            the lazy dog"
596        });
597        cx.simulate_shared_keystrokes("f r .").await;
598        cx.shared_state().await.assert_eq(indoc! {
599            "o quick brown
600            fox ops oveˇothe lazy dog"
601        });
602
603        // visual
604        cx.set_shared_state(indoc! {
605            "the ˇquick brown
606            fox jumps over
607            fox jumps over
608            fox jumps over
609            the lazy dog"
610        })
611        .await;
612        cx.simulate_shared_keystrokes("v j x").await;
613        cx.shared_state().await.assert_eq(indoc! {
614            "the ˇumps over
615            fox jumps over
616            fox jumps over
617            the lazy dog"
618        });
619        cx.simulate_shared_keystrokes(".").await;
620        cx.shared_state().await.assert_eq(indoc! {
621            "the ˇumps over
622            fox jumps over
623            the lazy dog"
624        });
625        cx.simulate_shared_keystrokes("w .").await;
626        cx.shared_state().await.assert_eq(indoc! {
627            "the umps ˇumps over
628            the lazy dog"
629        });
630        cx.simulate_shared_keystrokes("j .").await;
631        cx.shared_state().await.assert_eq(indoc! {
632            "the umps umps over
633            the ˇog"
634        });
635
636        // block mode (3 rows)
637        cx.set_shared_state(indoc! {
638            "ˇthe quick brown
639            fox jumps over
640            the lazy dog"
641        })
642        .await;
643        cx.simulate_shared_keystrokes("ctrl-v j j shift-i o escape")
644            .await;
645        cx.shared_state().await.assert_eq(indoc! {
646            "ˇothe quick brown
647            ofox jumps over
648            othe lazy dog"
649        });
650        cx.simulate_shared_keystrokes("j 4 l .").await;
651        cx.shared_state().await.assert_eq(indoc! {
652            "othe quick brown
653            ofoxˇo jumps over
654            otheo lazy dog"
655        });
656
657        // line mode
658        cx.set_shared_state(indoc! {
659            "ˇthe quick brown
660            fox jumps over
661            the lazy dog"
662        })
663        .await;
664        cx.simulate_shared_keystrokes("shift-v shift-r o escape")
665            .await;
666        cx.shared_state().await.assert_eq(indoc! {
667            "ˇo
668            fox jumps over
669            the lazy dog"
670        });
671        cx.simulate_shared_keystrokes("j .").await;
672        cx.shared_state().await.assert_eq(indoc! {
673            "o
674            ˇo
675            the lazy dog"
676        });
677    }
678
679    #[gpui::test]
680    async fn test_repeat_motion_counts(cx: &mut gpui::TestAppContext) {
681        let mut cx = NeovimBackedTestContext::new(cx).await;
682
683        cx.set_shared_state(indoc! {
684            "ˇthe quick brown
685            fox jumps over
686            the lazy dog"
687        })
688        .await;
689        cx.simulate_shared_keystrokes("3 d 3 l").await;
690        cx.shared_state().await.assert_eq(indoc! {
691            "ˇ brown
692            fox jumps over
693            the lazy dog"
694        });
695        cx.simulate_shared_keystrokes("j .").await;
696        cx.shared_state().await.assert_eq(indoc! {
697            " brown
698            ˇ over
699            the lazy dog"
700        });
701        cx.simulate_shared_keystrokes("j 2 .").await;
702        cx.shared_state().await.assert_eq(indoc! {
703            " brown
704             over
705            ˇe lazy dog"
706        });
707    }
708
709    #[gpui::test]
710    async fn test_record_interrupted(cx: &mut gpui::TestAppContext) {
711        let mut cx = VimTestContext::new(cx, true).await;
712
713        cx.set_state("ˇhello\n", Mode::Normal);
714        cx.simulate_keystrokes("4 i j cmd-shift-p escape");
715        cx.simulate_keystrokes("escape");
716        cx.assert_state("ˇjhello\n", Mode::Normal);
717    }
718
719    #[gpui::test]
720    async fn test_repeat_over_blur(cx: &mut gpui::TestAppContext) {
721        let mut cx = NeovimBackedTestContext::new(cx).await;
722
723        cx.set_shared_state("ˇhello hello hello\n").await;
724        cx.simulate_shared_keystrokes("c f o x escape").await;
725        cx.shared_state().await.assert_eq("ˇx hello hello\n");
726        cx.simulate_shared_keystrokes(": escape").await;
727        cx.simulate_shared_keystrokes(".").await;
728        cx.shared_state().await.assert_eq("ˇx hello\n");
729    }
730
731    #[gpui::test]
732    async fn test_undo_repeated_insert(cx: &mut gpui::TestAppContext) {
733        let mut cx = NeovimBackedTestContext::new(cx).await;
734
735        cx.set_shared_state("hellˇo").await;
736        cx.simulate_shared_keystrokes("3 a . escape").await;
737        cx.shared_state().await.assert_eq("hello..ˇ.");
738        cx.simulate_shared_keystrokes("u").await;
739        cx.shared_state().await.assert_eq("hellˇo");
740    }
741
742    #[gpui::test]
743    async fn test_record_replay(cx: &mut gpui::TestAppContext) {
744        let mut cx = NeovimBackedTestContext::new(cx).await;
745
746        cx.set_shared_state("ˇhello world").await;
747        cx.simulate_shared_keystrokes("q w c w j escape q").await;
748        cx.shared_state().await.assert_eq("ˇj world");
749        cx.simulate_shared_keystrokes("2 l @ w").await;
750        cx.shared_state().await.assert_eq("j ˇj");
751    }
752
753    #[gpui::test]
754    async fn test_record_replay_count(cx: &mut gpui::TestAppContext) {
755        let mut cx = NeovimBackedTestContext::new(cx).await;
756
757        cx.set_shared_state("ˇhello world!!").await;
758        cx.simulate_shared_keystrokes("q a v 3 l s 0 escape l q")
759            .await;
760        cx.shared_state().await.assert_eq("0ˇo world!!");
761        cx.simulate_shared_keystrokes("2 @ a").await;
762        cx.shared_state().await.assert_eq("000ˇ!");
763    }
764
765    #[gpui::test]
766    async fn test_record_replay_dot(cx: &mut gpui::TestAppContext) {
767        let mut cx = NeovimBackedTestContext::new(cx).await;
768
769        cx.set_shared_state("ˇhello world").await;
770        cx.simulate_shared_keystrokes("q a r a l r b l q").await;
771        cx.shared_state().await.assert_eq("abˇllo world");
772        cx.simulate_shared_keystrokes(".").await;
773        cx.shared_state().await.assert_eq("abˇblo world");
774        cx.simulate_shared_keystrokes("shift-q").await;
775        cx.shared_state().await.assert_eq("ababˇo world");
776        cx.simulate_shared_keystrokes(".").await;
777        cx.shared_state().await.assert_eq("ababˇb world");
778    }
779
780    #[gpui::test]
781    async fn test_record_replay_of_dot(cx: &mut gpui::TestAppContext) {
782        let mut cx = NeovimBackedTestContext::new(cx).await;
783
784        cx.set_shared_state("ˇhello world").await;
785        cx.simulate_shared_keystrokes("r o q w . q").await;
786        cx.shared_state().await.assert_eq("ˇoello world");
787        cx.simulate_shared_keystrokes("d l").await;
788        cx.shared_state().await.assert_eq("ˇello world");
789        cx.simulate_shared_keystrokes("@ w").await;
790        cx.shared_state().await.assert_eq("ˇllo world");
791    }
792
793    #[gpui::test]
794    async fn test_record_replay_interleaved(cx: &mut gpui::TestAppContext) {
795        let mut cx = NeovimBackedTestContext::new(cx).await;
796
797        cx.set_shared_state("ˇhello world").await;
798        cx.simulate_shared_keystrokes("q z r a l q").await;
799        cx.shared_state().await.assert_eq("aˇello world");
800        cx.simulate_shared_keystrokes("q b @ z @ z q").await;
801        cx.shared_state().await.assert_eq("aaaˇlo world");
802        cx.simulate_shared_keystrokes("@ @").await;
803        cx.shared_state().await.assert_eq("aaaaˇo world");
804        cx.simulate_shared_keystrokes("@ b").await;
805        cx.shared_state().await.assert_eq("aaaaaaˇworld");
806        cx.simulate_shared_keystrokes("@ @").await;
807        cx.shared_state().await.assert_eq("aaaaaaaˇorld");
808        cx.simulate_shared_keystrokes("q z r b l q").await;
809        cx.shared_state().await.assert_eq("aaaaaaabˇrld");
810        cx.simulate_shared_keystrokes("@ b").await;
811        cx.shared_state().await.assert_eq("aaaaaaabbbˇd");
812    }
813}