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