repeat.rs

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