repeat.rs

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