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