repeat.rs

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