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