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