repeat.rs

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