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            // The `globals.dot_replaying = false` is a fail-safe to ensure that
149            // this value is always reset, in the case that the focus is moved
150            // away from the editor, effectively preventing the `EndRepeat`
151            // action from being handled.
152            let globals = Vim::globals(cx);
153            globals.replayer.take();
154            globals.dot_replaying = false;
155            return;
156        };
157        match action {
158            ReplayableAction::Action(action) => {
159                if should_replay(&*action) {
160                    window.dispatch_action(action.boxed_clone(), cx);
161                    cx.defer(move |cx| Vim::globals(cx).observe_action(action.boxed_clone()));
162                }
163            }
164            ReplayableAction::Insertion {
165                text,
166                utf16_range_to_replace,
167            } => {
168                let Some(Some(workspace)) = window.root::<Workspace>() else {
169                    return;
170                };
171                let Some(editor) = workspace
172                    .read(cx)
173                    .active_item(cx)
174                    .and_then(|item| item.act_as::<Editor>(cx))
175                else {
176                    return;
177                };
178                editor.update(cx, |editor, cx| {
179                    editor.replay_insert_event(&text, utf16_range_to_replace.clone(), window, cx)
180                })
181            }
182        }
183        window.defer(cx, move |window, cx| self.next(window, cx));
184    }
185}
186
187impl Vim {
188    pub(crate) fn record_register(
189        &mut self,
190        register: char,
191        window: &mut Window,
192        cx: &mut Context<Self>,
193    ) {
194        let globals = Vim::globals(cx);
195        globals.recording_register = Some(register);
196        globals.recordings.remove(&register);
197        globals.ignore_current_insertion = true;
198        self.clear_operator(window, cx)
199    }
200
201    pub(crate) fn replay_register(
202        &mut self,
203        mut register: char,
204        window: &mut Window,
205        cx: &mut Context<Self>,
206    ) {
207        let mut count = Vim::take_count(cx).unwrap_or(1);
208        Vim::take_forced_motion(cx);
209        self.clear_operator(window, cx);
210
211        let globals = Vim::globals(cx);
212        if register == '@' {
213            let Some(last) = globals.last_replayed_register else {
214                return;
215            };
216            register = last;
217        }
218        let Some(actions) = globals.recordings.get(&register) else {
219            return;
220        };
221
222        let mut repeated_actions = vec![];
223        while count > 0 {
224            repeated_actions.extend(actions.iter().cloned());
225            count -= 1
226        }
227
228        globals.last_replayed_register = Some(register);
229        let mut replayer = globals.replayer.get_or_insert_with(Replayer::new).clone();
230        replayer.replay(repeated_actions, window, cx);
231    }
232
233    pub(crate) fn repeat(
234        &mut self,
235        from_insert_mode: bool,
236        window: &mut Window,
237        cx: &mut Context<Self>,
238    ) {
239        if self.active_operator().is_some() {
240            Vim::update_globals(cx, |globals, _| {
241                globals.recording_actions.clear();
242                globals.recording_count = None;
243                globals.dot_recording = false;
244                globals.stop_recording_after_next_action = false;
245            });
246            self.clear_operator(window, cx);
247            return;
248        }
249
250        Vim::take_forced_motion(cx);
251        let count = Vim::take_count(cx);
252
253        let Some((mut actions, selection, mode)) = Vim::update_globals(cx, |globals, _| {
254            let actions = globals.recorded_actions.clone();
255            if actions.is_empty() {
256                return None;
257            }
258            if globals.replayer.is_none()
259                && let Some(recording_register) = globals.recording_register
260            {
261                globals
262                    .recordings
263                    .entry(recording_register)
264                    .or_default()
265                    .push(ReplayableAction::Action(Repeat.boxed_clone()));
266            }
267
268            let mut mode = None;
269            let selection = globals.recorded_selection.clone();
270            match selection {
271                RecordedSelection::SingleLine { .. } | RecordedSelection::Visual { .. } => {
272                    globals.recorded_count = None;
273                    mode = Some(Mode::Visual);
274                }
275                RecordedSelection::VisualLine { .. } => {
276                    globals.recorded_count = None;
277                    mode = Some(Mode::VisualLine)
278                }
279                RecordedSelection::VisualBlock { .. } => {
280                    globals.recorded_count = None;
281                    mode = Some(Mode::VisualBlock)
282                }
283                RecordedSelection::None => {
284                    if let Some(count) = count {
285                        globals.recorded_count = Some(count);
286                    }
287                }
288            }
289
290            Some((actions, selection, mode))
291        }) else {
292            return;
293        };
294        if mode != Some(self.mode) {
295            if let Some(mode) = mode {
296                self.switch_mode(mode, false, window, cx)
297            }
298
299            match selection {
300                RecordedSelection::SingleLine { cols } => {
301                    if cols > 1 {
302                        self.visual_motion(Motion::Right, Some(cols as usize - 1), window, cx)
303                    }
304                }
305                RecordedSelection::Visual { rows, cols } => {
306                    self.visual_motion(
307                        Motion::Down {
308                            display_lines: false,
309                        },
310                        Some(rows as usize),
311                        window,
312                        cx,
313                    );
314                    self.visual_motion(
315                        Motion::StartOfLine {
316                            display_lines: false,
317                        },
318                        None,
319                        window,
320                        cx,
321                    );
322                    if cols > 1 {
323                        self.visual_motion(Motion::Right, Some(cols as usize - 1), window, cx)
324                    }
325                }
326                RecordedSelection::VisualBlock { rows, cols } => {
327                    self.visual_motion(
328                        Motion::Down {
329                            display_lines: false,
330                        },
331                        Some(rows as usize),
332                        window,
333                        cx,
334                    );
335                    if cols > 1 {
336                        self.visual_motion(Motion::Right, Some(cols as usize - 1), window, cx);
337                    }
338                }
339                RecordedSelection::VisualLine { rows } => {
340                    self.visual_motion(
341                        Motion::Down {
342                            display_lines: false,
343                        },
344                        Some(rows as usize),
345                        window,
346                        cx,
347                    );
348                }
349                RecordedSelection::None => {}
350            }
351        }
352
353        // insert internally uses repeat to handle counts
354        // vim doesn't treat 3a1 as though you literally repeated a1
355        // 3 times, instead it inserts the content thrice at the insert position.
356        if let Some(to_repeat) = repeatable_insert(&actions[0]) {
357            if let Some(ReplayableAction::Action(action)) = actions.last()
358                && NormalBefore.partial_eq(&**action)
359            {
360                actions.pop();
361            }
362
363            let mut new_actions = actions.clone();
364            actions[0] = ReplayableAction::Action(to_repeat.boxed_clone());
365
366            let mut count = cx.global::<VimGlobals>().recorded_count.unwrap_or(1);
367
368            // if we came from insert mode we're just doing repetitions 2 onwards.
369            if from_insert_mode {
370                count -= 1;
371                new_actions[0] = actions[0].clone();
372            }
373
374            for _ in 1..count {
375                new_actions.append(actions.clone().as_mut());
376            }
377            new_actions.push(ReplayableAction::Action(NormalBefore.boxed_clone()));
378            actions = new_actions;
379        }
380
381        actions.push(ReplayableAction::Action(EndRepeat.boxed_clone()));
382
383        if self.temp_mode {
384            self.temp_mode = false;
385            actions.push(ReplayableAction::Action(InsertBefore.boxed_clone()));
386        }
387
388        let globals = Vim::globals(cx);
389        globals.dot_replaying = true;
390        let mut replayer = globals.replayer.get_or_insert_with(Replayer::new).clone();
391
392        replayer.replay(actions, window, cx);
393    }
394}
395
396#[cfg(test)]
397mod test {
398    use editor::test::editor_lsp_test_context::EditorLspTestContext;
399    use futures::StreamExt;
400    use indoc::indoc;
401
402    use gpui::EntityInputHandler;
403
404    use crate::{
405        VimGlobals,
406        state::Mode,
407        test::{NeovimBackedTestContext, VimTestContext},
408    };
409
410    #[gpui::test]
411    async fn test_dot_repeat(cx: &mut gpui::TestAppContext) {
412        let mut cx = NeovimBackedTestContext::new(cx).await;
413
414        // "o"
415        cx.set_shared_state("ˇhello").await;
416        cx.simulate_shared_keystrokes("o w o r l d escape").await;
417        cx.shared_state().await.assert_eq("hello\nworlˇd");
418        cx.simulate_shared_keystrokes(".").await;
419        cx.shared_state().await.assert_eq("hello\nworld\nworlˇd");
420
421        // "d"
422        cx.simulate_shared_keystrokes("^ d f o").await;
423        cx.simulate_shared_keystrokes("g g .").await;
424        cx.shared_state().await.assert_eq("ˇ\nworld\nrld");
425
426        // "p" (note that it pastes the current clipboard)
427        cx.simulate_shared_keystrokes("j y y p").await;
428        cx.simulate_shared_keystrokes("shift-g y y .").await;
429        cx.shared_state()
430            .await
431            .assert_eq("\nworld\nworld\nrld\nˇrld");
432
433        // "~" (note that counts apply to the action taken, not . itself)
434        cx.set_shared_state("ˇthe quick brown fox").await;
435        cx.simulate_shared_keystrokes("2 ~ .").await;
436        cx.set_shared_state("THE ˇquick brown fox").await;
437        cx.simulate_shared_keystrokes("3 .").await;
438        cx.set_shared_state("THE QUIˇck brown fox").await;
439        cx.run_until_parked();
440        cx.simulate_shared_keystrokes(".").await;
441        cx.shared_state().await.assert_eq("THE QUICK ˇbrown fox");
442    }
443
444    #[gpui::test]
445    async fn test_repeat_ime(cx: &mut gpui::TestAppContext) {
446        let mut cx = VimTestContext::new(cx, true).await;
447
448        cx.set_state("hˇllo", Mode::Normal);
449        cx.simulate_keystrokes("i");
450
451        // simulate brazilian input for ä.
452        cx.update_editor(|editor, window, cx| {
453            editor.replace_and_mark_text_in_range(None, "\"", Some(1..1), window, cx);
454            editor.replace_text_in_range(None, "ä", window, cx);
455        });
456        cx.simulate_keystrokes("escape");
457        cx.assert_state("hˇällo", Mode::Normal);
458        cx.simulate_keystrokes(".");
459        cx.assert_state("hˇäällo", Mode::Normal);
460    }
461
462    #[gpui::test]
463    async fn test_repeat_completion(cx: &mut gpui::TestAppContext) {
464        VimTestContext::init(cx);
465        let cx = EditorLspTestContext::new_rust(
466            lsp::ServerCapabilities {
467                completion_provider: Some(lsp::CompletionOptions {
468                    trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
469                    resolve_provider: Some(true),
470                    ..Default::default()
471                }),
472                ..Default::default()
473            },
474            cx,
475        )
476        .await;
477        let mut cx = VimTestContext::new_with_lsp(cx, true);
478
479        cx.set_state(
480            indoc! {"
481            onˇe
482            two
483            three
484        "},
485            Mode::Normal,
486        );
487
488        let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(
489            move |_, params, _| async move {
490                let position = params.text_document_position.position;
491                Ok(Some(lsp::CompletionResponse::Array(vec![
492                    lsp::CompletionItem {
493                        label: "first".to_string(),
494                        text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
495                            range: lsp::Range::new(position, position),
496                            new_text: "first".to_string(),
497                        })),
498                        ..Default::default()
499                    },
500                    lsp::CompletionItem {
501                        label: "second".to_string(),
502                        text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
503                            range: lsp::Range::new(position, position),
504                            new_text: "second".to_string(),
505                        })),
506                        ..Default::default()
507                    },
508                ])))
509            },
510        );
511        cx.simulate_keystrokes("a .");
512        request.next().await;
513        cx.condition(|editor, _| editor.context_menu_visible())
514            .await;
515        cx.simulate_keystrokes("down enter ! escape");
516
517        cx.assert_state(
518            indoc! {"
519                one.secondˇ!
520                two
521                three
522            "},
523            Mode::Normal,
524        );
525        cx.simulate_keystrokes("j .");
526        cx.assert_state(
527            indoc! {"
528                one.second!
529                two.secondˇ!
530                three
531            "},
532            Mode::Normal,
533        );
534    }
535
536    #[gpui::test]
537    async fn test_repeat_completion_unicode_bug(cx: &mut gpui::TestAppContext) {
538        VimTestContext::init(cx);
539        let cx = EditorLspTestContext::new_rust(
540            lsp::ServerCapabilities {
541                completion_provider: Some(lsp::CompletionOptions {
542                    trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
543                    resolve_provider: Some(true),
544                    ..Default::default()
545                }),
546                ..Default::default()
547            },
548            cx,
549        )
550        .await;
551        let mut cx = VimTestContext::new_with_lsp(cx, true);
552
553        cx.set_state(
554            indoc! {"
555                ĩлˇк
556                ĩлк
557            "},
558            Mode::Normal,
559        );
560
561        let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(
562            move |_, params, _| async move {
563                let position = params.text_document_position.position;
564                let mut to_the_left = position;
565                to_the_left.character -= 2;
566                Ok(Some(lsp::CompletionResponse::Array(vec![
567                    lsp::CompletionItem {
568                        label: "oops".to_string(),
569                        text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
570                            range: lsp::Range::new(to_the_left, position),
571                            new_text: "к!".to_string(),
572                        })),
573                        ..Default::default()
574                    },
575                ])))
576            },
577        );
578        cx.simulate_keystrokes("i .");
579        request.next().await;
580        cx.condition(|editor, _| editor.context_menu_visible())
581            .await;
582        cx.simulate_keystrokes("enter escape");
583        cx.assert_state(
584            indoc! {"
585                ĩкˇ!к
586                ĩлк
587            "},
588            Mode::Normal,
589        );
590    }
591
592    #[gpui::test]
593    async fn test_repeat_visual(cx: &mut gpui::TestAppContext) {
594        let mut cx = NeovimBackedTestContext::new(cx).await;
595
596        // single-line (3 columns)
597        cx.set_shared_state(indoc! {
598            "ˇthe quick brown
599            fox jumps over
600            the lazy dog"
601        })
602        .await;
603        cx.simulate_shared_keystrokes("v i w s o escape").await;
604        cx.shared_state().await.assert_eq(indoc! {
605            "ˇo quick brown
606            fox jumps over
607            the lazy dog"
608        });
609        cx.simulate_shared_keystrokes("j w .").await;
610        cx.shared_state().await.assert_eq(indoc! {
611            "o quick brown
612            fox ˇops over
613            the lazy dog"
614        });
615        cx.simulate_shared_keystrokes("f r .").await;
616        cx.shared_state().await.assert_eq(indoc! {
617            "o quick brown
618            fox ops oveˇothe lazy dog"
619        });
620
621        // visual
622        cx.set_shared_state(indoc! {
623            "the ˇquick brown
624            fox jumps over
625            fox jumps over
626            fox jumps over
627            the lazy dog"
628        })
629        .await;
630        cx.simulate_shared_keystrokes("v j x").await;
631        cx.shared_state().await.assert_eq(indoc! {
632            "the ˇumps over
633            fox jumps over
634            fox jumps over
635            the lazy dog"
636        });
637        cx.simulate_shared_keystrokes(".").await;
638        cx.shared_state().await.assert_eq(indoc! {
639            "the ˇumps over
640            fox jumps over
641            the lazy dog"
642        });
643        cx.simulate_shared_keystrokes("w .").await;
644        cx.shared_state().await.assert_eq(indoc! {
645            "the umps ˇumps over
646            the lazy dog"
647        });
648        cx.simulate_shared_keystrokes("j .").await;
649        cx.shared_state().await.assert_eq(indoc! {
650            "the umps umps over
651            the ˇog"
652        });
653
654        // block mode (3 rows)
655        cx.set_shared_state(indoc! {
656            "ˇthe quick brown
657            fox jumps over
658            the lazy dog"
659        })
660        .await;
661        cx.simulate_shared_keystrokes("ctrl-v j j shift-i o escape")
662            .await;
663        cx.shared_state().await.assert_eq(indoc! {
664            "ˇothe quick brown
665            ofox jumps over
666            othe lazy dog"
667        });
668        cx.simulate_shared_keystrokes("j 4 l .").await;
669        cx.shared_state().await.assert_eq(indoc! {
670            "othe quick brown
671            ofoxˇo jumps over
672            otheo lazy dog"
673        });
674
675        // line mode
676        cx.set_shared_state(indoc! {
677            "ˇthe quick brown
678            fox jumps over
679            the lazy dog"
680        })
681        .await;
682        cx.simulate_shared_keystrokes("shift-v shift-r o escape")
683            .await;
684        cx.shared_state().await.assert_eq(indoc! {
685            "ˇo
686            fox jumps over
687            the lazy dog"
688        });
689        cx.simulate_shared_keystrokes("j .").await;
690        cx.shared_state().await.assert_eq(indoc! {
691            "o
692            ˇo
693            the lazy dog"
694        });
695    }
696
697    #[gpui::test]
698    async fn test_repeat_motion_counts(cx: &mut gpui::TestAppContext) {
699        let mut cx = NeovimBackedTestContext::new(cx).await;
700
701        cx.set_shared_state(indoc! {
702            "ˇthe quick brown
703            fox jumps over
704            the lazy dog"
705        })
706        .await;
707        cx.simulate_shared_keystrokes("3 d 3 l").await;
708        cx.shared_state().await.assert_eq(indoc! {
709            "ˇ brown
710            fox jumps over
711            the lazy dog"
712        });
713        cx.simulate_shared_keystrokes("j .").await;
714        cx.shared_state().await.assert_eq(indoc! {
715            " brown
716            ˇ over
717            the lazy dog"
718        });
719        cx.simulate_shared_keystrokes("j 2 .").await;
720        cx.shared_state().await.assert_eq(indoc! {
721            " brown
722             over
723            ˇe lazy dog"
724        });
725    }
726
727    #[gpui::test]
728    async fn test_record_interrupted(cx: &mut gpui::TestAppContext) {
729        let mut cx = VimTestContext::new(cx, true).await;
730
731        cx.set_state("ˇhello\n", Mode::Normal);
732        cx.simulate_keystrokes("4 i j cmd-shift-p escape");
733        cx.simulate_keystrokes("escape");
734        cx.assert_state("ˇjhello\n", Mode::Normal);
735    }
736
737    #[gpui::test]
738    async fn test_repeat_over_blur(cx: &mut gpui::TestAppContext) {
739        let mut cx = NeovimBackedTestContext::new(cx).await;
740
741        cx.set_shared_state("ˇhello hello hello\n").await;
742        cx.simulate_shared_keystrokes("c f o x escape").await;
743        cx.shared_state().await.assert_eq("ˇx hello hello\n");
744        cx.simulate_shared_keystrokes(": escape").await;
745        cx.simulate_shared_keystrokes(".").await;
746        cx.shared_state().await.assert_eq("ˇx hello\n");
747    }
748
749    #[gpui::test]
750    async fn test_repeat_after_blur_resets_dot_replaying(cx: &mut gpui::TestAppContext) {
751        let mut cx = VimTestContext::new(cx, true).await;
752
753        // Bind `ctrl-f` to the `buffer_search::Deploy` action so that this can
754        // be triggered while in Insert mode, ensuring that an action which
755        // moves the focus away from the editor, gets recorded.
756        cx.update(|_, cx| {
757            cx.bind_keys([gpui::KeyBinding::new(
758                "ctrl-f",
759                search::buffer_search::Deploy::find(),
760                None,
761            )])
762        });
763
764        cx.set_state("ˇhello", Mode::Normal);
765
766        // We're going to enter insert mode, which will start recording, type a
767        // character and then immediately use `ctrl-f` to trigger the buffer
768        // search. Triggering the buffer search will move focus away from the
769        // editor, effectively stopping the recording immediately after
770        // `buffer_search::Deploy` is recorded. The first `escape` is used to
771        // dismiss the search bar, while the second is used to move from Insert
772        // to Normal mode.
773        cx.simulate_keystrokes("i x ctrl-f escape escape");
774        cx.run_until_parked();
775
776        // Using the `.` key will dispatch the `vim::Repeat` action, repeating
777        // the set of recorded actions. This will eventually focus on the search
778        // bar, preventing the `EndRepeat` action from being correctly handled.
779        cx.simulate_keystrokes(".");
780        cx.run_until_parked();
781
782        // After replay finishes, even though the `EndRepeat` action wasn't
783        // handled, seeing as the editor lost focus during replay, the
784        // `dot_replaying` value should be set back to `false`.
785        assert!(
786            !cx.update(|_, cx| cx.global::<VimGlobals>().dot_replaying),
787            "dot_replaying should be false after repeat completes"
788        );
789    }
790
791    #[gpui::test]
792    async fn test_undo_repeated_insert(cx: &mut gpui::TestAppContext) {
793        let mut cx = NeovimBackedTestContext::new(cx).await;
794
795        cx.set_shared_state("hellˇo").await;
796        cx.simulate_shared_keystrokes("3 a . escape").await;
797        cx.shared_state().await.assert_eq("hello..ˇ.");
798        cx.simulate_shared_keystrokes("u").await;
799        cx.shared_state().await.assert_eq("hellˇo");
800    }
801
802    #[gpui::test]
803    async fn test_record_replay(cx: &mut gpui::TestAppContext) {
804        let mut cx = NeovimBackedTestContext::new(cx).await;
805
806        cx.set_shared_state("ˇhello world").await;
807        cx.simulate_shared_keystrokes("q w c w j escape q").await;
808        cx.shared_state().await.assert_eq("ˇj world");
809        cx.simulate_shared_keystrokes("2 l @ w").await;
810        cx.shared_state().await.assert_eq("j ˇj");
811    }
812
813    #[gpui::test]
814    async fn test_record_replay_count(cx: &mut gpui::TestAppContext) {
815        let mut cx = NeovimBackedTestContext::new(cx).await;
816
817        cx.set_shared_state("ˇhello world!!").await;
818        cx.simulate_shared_keystrokes("q a v 3 l s 0 escape l q")
819            .await;
820        cx.shared_state().await.assert_eq("0ˇo world!!");
821        cx.simulate_shared_keystrokes("2 @ a").await;
822        cx.shared_state().await.assert_eq("000ˇ!");
823    }
824
825    #[gpui::test]
826    async fn test_record_replay_dot(cx: &mut gpui::TestAppContext) {
827        let mut cx = NeovimBackedTestContext::new(cx).await;
828
829        cx.set_shared_state("ˇhello world").await;
830        cx.simulate_shared_keystrokes("q a r a l r b l q").await;
831        cx.shared_state().await.assert_eq("abˇllo world");
832        cx.simulate_shared_keystrokes(".").await;
833        cx.shared_state().await.assert_eq("abˇblo world");
834        cx.simulate_shared_keystrokes("shift-q").await;
835        cx.shared_state().await.assert_eq("ababˇo world");
836        cx.simulate_shared_keystrokes(".").await;
837        cx.shared_state().await.assert_eq("ababˇb world");
838    }
839
840    #[gpui::test]
841    async fn test_record_replay_of_dot(cx: &mut gpui::TestAppContext) {
842        let mut cx = NeovimBackedTestContext::new(cx).await;
843
844        cx.set_shared_state("ˇhello world").await;
845        cx.simulate_shared_keystrokes("r o q w . q").await;
846        cx.shared_state().await.assert_eq("ˇoello world");
847        cx.simulate_shared_keystrokes("d l").await;
848        cx.shared_state().await.assert_eq("ˇello world");
849        cx.simulate_shared_keystrokes("@ w").await;
850        cx.shared_state().await.assert_eq("ˇllo world");
851    }
852
853    #[gpui::test]
854    async fn test_record_replay_interleaved(cx: &mut gpui::TestAppContext) {
855        let mut cx = NeovimBackedTestContext::new(cx).await;
856
857        cx.set_shared_state("ˇhello world").await;
858        cx.simulate_shared_keystrokes("q z r a l q").await;
859        cx.shared_state().await.assert_eq("aˇello world");
860        cx.simulate_shared_keystrokes("q b @ z @ z q").await;
861        cx.shared_state().await.assert_eq("aaaˇlo world");
862        cx.simulate_shared_keystrokes("@ @").await;
863        cx.shared_state().await.assert_eq("aaaaˇo world");
864        cx.simulate_shared_keystrokes("@ b").await;
865        cx.shared_state().await.assert_eq("aaaaaaˇworld");
866        cx.simulate_shared_keystrokes("@ @").await;
867        cx.shared_state().await.assert_eq("aaaaaaaˇorld");
868        cx.simulate_shared_keystrokes("q z r b l q").await;
869        cx.shared_state().await.assert_eq("aaaaaaabˇrld");
870        cx.simulate_shared_keystrokes("@ b").await;
871        cx.shared_state().await.assert_eq("aaaaaaabbbˇd");
872    }
873
874    #[gpui::test]
875    async fn test_repeat_clear(cx: &mut gpui::TestAppContext) {
876        let mut cx = VimTestContext::new(cx, true).await;
877
878        // Check that, when repeat is preceded by something other than a number,
879        // the current operator is cleared, in order to prevent infinite loops.
880        cx.set_state("ˇhello world", Mode::Normal);
881        cx.simulate_keystrokes("d .");
882        assert_eq!(cx.active_operator(), None);
883    }
884
885    #[gpui::test]
886    async fn test_repeat_clear_repeat(cx: &mut gpui::TestAppContext) {
887        let mut cx = NeovimBackedTestContext::new(cx).await;
888
889        cx.set_shared_state(indoc! {
890            "ˇthe quick brown
891            fox jumps over
892            the lazy dog"
893        })
894        .await;
895        cx.simulate_shared_keystrokes("d d").await;
896        cx.shared_state().await.assert_eq(indoc! {
897            "ˇfox jumps over
898            the lazy dog"
899        });
900        cx.simulate_shared_keystrokes("d . .").await;
901        cx.shared_state().await.assert_eq(indoc! {
902            "ˇthe lazy dog"
903        });
904    }
905
906    #[gpui::test]
907    async fn test_repeat_clear_count(cx: &mut gpui::TestAppContext) {
908        let mut cx = NeovimBackedTestContext::new(cx).await;
909
910        cx.set_shared_state(indoc! {
911            "ˇthe quick brown
912            fox jumps over
913            the lazy dog"
914        })
915        .await;
916        cx.simulate_shared_keystrokes("d d").await;
917        cx.shared_state().await.assert_eq(indoc! {
918            "ˇfox jumps over
919            the lazy dog"
920        });
921        cx.simulate_shared_keystrokes("2 d .").await;
922        cx.shared_state().await.assert_eq(indoc! {
923            "ˇfox jumps over
924            the lazy dog"
925        });
926        cx.simulate_shared_keystrokes(".").await;
927        cx.shared_state().await.assert_eq(indoc! {
928            "ˇthe lazy dog"
929        });
930
931        cx.set_shared_state(indoc! {
932            "ˇthe quick brown
933            fox jumps over
934            the lazy dog
935            the quick brown
936            fox jumps over
937            the lazy dog"
938        })
939        .await;
940        cx.simulate_shared_keystrokes("2 d d").await;
941        cx.shared_state().await.assert_eq(indoc! {
942            "ˇthe lazy dog
943            the quick brown
944            fox jumps over
945            the lazy dog"
946        });
947        cx.simulate_shared_keystrokes("5 d .").await;
948        cx.shared_state().await.assert_eq(indoc! {
949            "ˇthe lazy dog
950            the quick brown
951            fox jumps over
952            the lazy dog"
953        });
954        cx.simulate_shared_keystrokes(".").await;
955        cx.shared_state().await.assert_eq(indoc! {
956            "ˇfox jumps over
957            the lazy dog"
958        });
959    }
960}