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