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(®ister);
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 = Vim::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(®ister) 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 = Vim::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}