1use std::{cell::RefCell, rc::Rc};
2
3use crate::{
4 Vim,
5 insert::NormalBefore,
6 motion::Motion,
7 normal::InsertBefore,
8 state::{Mode, Operator, RecordedSelection, ReplayableAction, VimGlobals},
9};
10use editor::Editor;
11use gpui::{Action, App, Context, Window, actions};
12use workspace::Workspace;
13
14actions!(
15 vim,
16 [
17 /// Repeats the last change.
18 Repeat,
19 /// Ends the repeat recording.
20 EndRepeat,
21 /// Toggles macro recording.
22 ToggleRecord,
23 /// Replays the last recorded macro.
24 ReplayLastRecording
25 ]
26);
27
28fn should_replay(action: &dyn Action) -> bool {
29 // skip so that we don't leave the character palette open
30 if editor::actions::ShowCharacterPalette.partial_eq(action) {
31 return false;
32 }
33 true
34}
35
36fn repeatable_insert(action: &ReplayableAction) -> Option<Box<dyn Action>> {
37 match action {
38 ReplayableAction::Action(action) => {
39 if super::InsertBefore.partial_eq(&**action)
40 || super::InsertAfter.partial_eq(&**action)
41 || super::InsertFirstNonWhitespace.partial_eq(&**action)
42 || super::InsertEndOfLine.partial_eq(&**action)
43 {
44 Some(super::InsertBefore.boxed_clone())
45 } else if super::InsertLineAbove.partial_eq(&**action)
46 || super::InsertLineBelow.partial_eq(&**action)
47 {
48 Some(super::InsertLineBelow.boxed_clone())
49 } else if crate::replace::ToggleReplace.partial_eq(&**action) {
50 Some(crate::replace::ToggleReplace.boxed_clone())
51 } else {
52 None
53 }
54 }
55 ReplayableAction::Insertion { .. } => None,
56 }
57}
58
59pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
60 Vim::action(editor, cx, |vim, _: &EndRepeat, window, cx| {
61 Vim::globals(cx).dot_replaying = false;
62 vim.switch_mode(Mode::Normal, false, window, cx)
63 });
64
65 Vim::action(editor, cx, |vim, _: &Repeat, window, cx| {
66 vim.repeat(false, window, cx)
67 });
68
69 Vim::action(editor, cx, |vim, _: &ToggleRecord, window, cx| {
70 let globals = Vim::globals(cx);
71 if let Some(char) = globals.recording_register.take() {
72 globals.last_recorded_register = Some(char)
73 } else {
74 vim.push_operator(Operator::RecordRegister, window, cx);
75 }
76 });
77
78 Vim::action(editor, cx, |vim, _: &ReplayLastRecording, window, cx| {
79 let Some(register) = Vim::globals(cx).last_recorded_register else {
80 return;
81 };
82 vim.replay_register(register, window, cx)
83 });
84}
85
86pub struct ReplayerState {
87 actions: Vec<ReplayableAction>,
88 running: bool,
89 ix: usize,
90}
91
92#[derive(Clone)]
93pub struct Replayer(Rc<RefCell<ReplayerState>>);
94
95impl Replayer {
96 pub fn new() -> Self {
97 Self(Rc::new(RefCell::new(ReplayerState {
98 actions: vec![],
99 running: false,
100 ix: 0,
101 })))
102 }
103
104 pub fn replay(&mut self, actions: Vec<ReplayableAction>, window: &mut Window, cx: &mut App) {
105 let mut lock = self.0.borrow_mut();
106 let range = lock.ix..lock.ix;
107 lock.actions.splice(range, actions);
108 if lock.running {
109 return;
110 }
111 lock.running = true;
112 let this = self.clone();
113 window.defer(cx, move |window, cx| {
114 this.next(window, cx);
115 let Some(workspace) = Workspace::for_window(window, cx) else {
116 return;
117 };
118 let Some(editor) = workspace
119 .read(cx)
120 .active_item(cx)
121 .and_then(|item| item.act_as::<Editor>(cx))
122 else {
123 return;
124 };
125 editor.update(cx, |editor, cx| {
126 editor
127 .buffer()
128 .update(cx, |multi, cx| multi.finalize_last_transaction(cx))
129 });
130 })
131 }
132
133 pub fn stop(self) {
134 self.0.borrow_mut().actions.clear()
135 }
136
137 pub fn next(self, window: &mut Window, cx: &mut App) {
138 let mut lock = self.0.borrow_mut();
139 let action = if lock.ix < 10000 {
140 lock.actions.get(lock.ix).cloned()
141 } else {
142 log::error!("Aborting replay after 10000 actions");
143 None
144 };
145 lock.ix += 1;
146 drop(lock);
147 let Some(action) = action else {
148 // The `globals.dot_replaying = false` is a fail-safe to ensure that
149 // this value is always reset, in the case that the focus is moved
150 // away from the editor, effectively preventing the `EndRepeat`
151 // action from being handled.
152 let globals = Vim::globals(cx);
153 globals.replayer.take();
154 globals.dot_replaying = false;
155 return;
156 };
157 match action {
158 ReplayableAction::Action(action) => {
159 if should_replay(&*action) {
160 window.dispatch_action(action.boxed_clone(), cx);
161 cx.defer(move |cx| Vim::globals(cx).observe_action(action.boxed_clone()));
162 }
163 }
164 ReplayableAction::Insertion {
165 text,
166 utf16_range_to_replace,
167 } => {
168 let Some(workspace) = Workspace::for_window(window, cx) else {
169 return;
170 };
171 let Some(editor) = workspace
172 .read(cx)
173 .active_item(cx)
174 .and_then(|item| item.act_as::<Editor>(cx))
175 else {
176 return;
177 };
178 editor.update(cx, |editor, cx| {
179 editor.replay_insert_event(&text, utf16_range_to_replace.clone(), window, cx)
180 })
181 }
182 }
183 window.defer(cx, move |window, cx| self.next(window, cx));
184 }
185}
186
187impl Vim {
188 pub(crate) fn record_register(
189 &mut self,
190 register: char,
191 window: &mut Window,
192 cx: &mut Context<Self>,
193 ) {
194 let globals = Vim::globals(cx);
195 globals.recording_register = Some(register);
196 globals.recordings.remove(®ister);
197 globals.ignore_current_insertion = true;
198 self.clear_operator(window, cx)
199 }
200
201 pub(crate) fn replay_register(
202 &mut self,
203 mut register: char,
204 window: &mut Window,
205 cx: &mut Context<Self>,
206 ) {
207 let mut count = Vim::take_count(cx).unwrap_or(1);
208 Vim::take_forced_motion(cx);
209 self.clear_operator(window, cx);
210
211 let globals = Vim::globals(cx);
212 if register == '@' {
213 let Some(last) = globals.last_replayed_register else {
214 return;
215 };
216 register = last;
217 }
218 let Some(actions) = globals.recordings.get(®ister) else {
219 return;
220 };
221
222 let mut repeated_actions = vec![];
223 while count > 0 {
224 repeated_actions.extend(actions.iter().cloned());
225 count -= 1
226 }
227
228 globals.last_replayed_register = Some(register);
229 let mut replayer = globals.replayer.get_or_insert_with(Replayer::new).clone();
230 replayer.replay(repeated_actions, window, cx);
231 }
232
233 pub(crate) fn repeat(
234 &mut self,
235 from_insert_mode: bool,
236 window: &mut Window,
237 cx: &mut Context<Self>,
238 ) {
239 if self.active_operator().is_some() {
240 Vim::update_globals(cx, |globals, _| {
241 globals.recording_actions.clear();
242 globals.recording_count = None;
243 globals.dot_recording = false;
244 globals.stop_recording_after_next_action = false;
245 });
246 self.clear_operator(window, cx);
247 return;
248 }
249
250 Vim::take_forced_motion(cx);
251 let count = Vim::take_count(cx);
252
253 let Some((mut actions, selection, mode)) = Vim::update_globals(cx, |globals, _| {
254 let actions = globals.recorded_actions.clone();
255 if actions.is_empty() {
256 return None;
257 }
258 if globals.replayer.is_none()
259 && let Some(recording_register) = globals.recording_register
260 {
261 globals
262 .recordings
263 .entry(recording_register)
264 .or_default()
265 .push(ReplayableAction::Action(Repeat.boxed_clone()));
266 }
267
268 let mut mode = None;
269 let selection = globals.recorded_selection.clone();
270 match selection {
271 RecordedSelection::SingleLine { .. } | RecordedSelection::Visual { .. } => {
272 globals.recorded_count = None;
273 mode = Some(Mode::Visual);
274 }
275 RecordedSelection::VisualLine { .. } => {
276 globals.recorded_count = None;
277 mode = Some(Mode::VisualLine)
278 }
279 RecordedSelection::VisualBlock { .. } => {
280 globals.recorded_count = None;
281 mode = Some(Mode::VisualBlock)
282 }
283 RecordedSelection::None => {
284 if let Some(count) = count {
285 globals.recorded_count = Some(count);
286 }
287 }
288 }
289
290 Some((actions, selection, mode))
291 }) else {
292 return;
293 };
294
295 // Dot repeat always uses the recorded register, ignoring any "X
296 // override, as the register is an inherent part of the recorded action.
297 // For numbered registers, Neovim increments on each dot repeat so after
298 // using `"1p`, using `.` will equate to `"2p", the next `.` to `"3p`,
299 // etc..
300 let recorded_register = cx.global::<VimGlobals>().recorded_register_for_dot;
301 let next_register = recorded_register
302 .filter(|c| matches!(c, '1'..='9'))
303 .map(|c| ((c as u8 + 1).min(b'9')) as char);
304
305 self.selected_register = next_register.or(recorded_register);
306 if let Some(next_register) = next_register {
307 Vim::update_globals(cx, |globals, _| {
308 globals.recorded_register_for_dot = Some(next_register)
309 })
310 };
311
312 if mode != Some(self.mode) {
313 if let Some(mode) = mode {
314 self.switch_mode(mode, false, window, cx)
315 }
316
317 match selection {
318 RecordedSelection::SingleLine { cols } => {
319 if cols > 1 {
320 self.visual_motion(Motion::Right, Some(cols as usize - 1), window, cx)
321 }
322 }
323 RecordedSelection::Visual { rows, cols } => {
324 self.visual_motion(
325 Motion::Down {
326 display_lines: false,
327 },
328 Some(rows as usize),
329 window,
330 cx,
331 );
332 self.visual_motion(
333 Motion::StartOfLine {
334 display_lines: false,
335 },
336 None,
337 window,
338 cx,
339 );
340 if cols > 1 {
341 self.visual_motion(Motion::Right, Some(cols as usize - 1), window, cx)
342 }
343 }
344 RecordedSelection::VisualBlock { rows, cols } => {
345 self.visual_motion(
346 Motion::Down {
347 display_lines: false,
348 },
349 Some(rows as usize),
350 window,
351 cx,
352 );
353 if cols > 1 {
354 self.visual_motion(Motion::Right, Some(cols as usize - 1), window, cx);
355 }
356 }
357 RecordedSelection::VisualLine { rows } => {
358 self.visual_motion(
359 Motion::Down {
360 display_lines: false,
361 },
362 Some(rows as usize),
363 window,
364 cx,
365 );
366 }
367 RecordedSelection::None => {}
368 }
369 }
370
371 // insert internally uses repeat to handle counts
372 // vim doesn't treat 3a1 as though you literally repeated a1
373 // 3 times, instead it inserts the content thrice at the insert position.
374 if let Some(to_repeat) = repeatable_insert(&actions[0]) {
375 if let Some(ReplayableAction::Action(action)) = actions.last()
376 && NormalBefore.partial_eq(&**action)
377 {
378 actions.pop();
379 }
380
381 let mut new_actions = actions.clone();
382 actions[0] = ReplayableAction::Action(to_repeat.boxed_clone());
383
384 let mut count = cx.global::<VimGlobals>().recorded_count.unwrap_or(1);
385
386 // if we came from insert mode we're just doing repetitions 2 onwards.
387 if from_insert_mode {
388 count -= 1;
389 new_actions[0] = actions[0].clone();
390 }
391
392 for _ in 1..count {
393 new_actions.append(actions.clone().as_mut());
394 }
395 new_actions.push(ReplayableAction::Action(NormalBefore.boxed_clone()));
396 actions = new_actions;
397 }
398
399 actions.push(ReplayableAction::Action(EndRepeat.boxed_clone()));
400
401 if self.temp_mode {
402 self.temp_mode = false;
403 actions.push(ReplayableAction::Action(InsertBefore.boxed_clone()));
404 }
405
406 let globals = Vim::globals(cx);
407 globals.dot_replaying = true;
408 let mut replayer = globals.replayer.get_or_insert_with(Replayer::new).clone();
409
410 replayer.replay(actions, window, cx);
411 }
412}
413
414#[cfg(test)]
415mod test {
416 use editor::test::editor_lsp_test_context::EditorLspTestContext;
417 use futures::StreamExt;
418 use indoc::indoc;
419
420 use gpui::EntityInputHandler;
421
422 use crate::{
423 VimGlobals,
424 state::Mode,
425 test::{NeovimBackedTestContext, VimTestContext},
426 };
427
428 #[gpui::test]
429 async fn test_dot_repeat(cx: &mut gpui::TestAppContext) {
430 let mut cx = NeovimBackedTestContext::new(cx).await;
431
432 // "o"
433 cx.set_shared_state("ˇhello").await;
434 cx.simulate_shared_keystrokes("o w o r l d escape").await;
435 cx.shared_state().await.assert_eq("hello\nworlˇd");
436 cx.simulate_shared_keystrokes(".").await;
437 cx.shared_state().await.assert_eq("hello\nworld\nworlˇd");
438
439 // "d"
440 cx.simulate_shared_keystrokes("^ d f o").await;
441 cx.simulate_shared_keystrokes("g g .").await;
442 cx.shared_state().await.assert_eq("ˇ\nworld\nrld");
443
444 // "p" (note that it pastes the current clipboard)
445 cx.simulate_shared_keystrokes("j y y p").await;
446 cx.simulate_shared_keystrokes("shift-g y y .").await;
447 cx.shared_state()
448 .await
449 .assert_eq("\nworld\nworld\nrld\nˇrld");
450
451 // "~" (note that counts apply to the action taken, not . itself)
452 cx.set_shared_state("ˇthe quick brown fox").await;
453 cx.simulate_shared_keystrokes("2 ~ .").await;
454 cx.set_shared_state("THE ˇquick brown fox").await;
455 cx.simulate_shared_keystrokes("3 .").await;
456 cx.set_shared_state("THE QUIˇck brown fox").await;
457 cx.run_until_parked();
458 cx.simulate_shared_keystrokes(".").await;
459 cx.shared_state().await.assert_eq("THE QUICK ˇbrown fox");
460 }
461
462 #[gpui::test]
463 async fn test_dot_repeat_registers_paste(cx: &mut gpui::TestAppContext) {
464 let mut cx = NeovimBackedTestContext::new(cx).await;
465
466 // basic paste repeat uses the unnamed register
467 cx.set_shared_state("ˇhello\n").await;
468 cx.simulate_shared_keystrokes("y y p").await;
469 cx.shared_state().await.assert_eq("hello\nˇhello\n");
470 cx.simulate_shared_keystrokes(".").await;
471 cx.shared_state().await.assert_eq("hello\nhello\nˇhello\n");
472
473 // "_ (blackhole) is recorded and replayed, so the pasted text is still
474 // the original yanked line.
475 cx.set_shared_state(indoc! {"
476 ˇone
477 two
478 three
479 four
480 "})
481 .await;
482 cx.simulate_shared_keystrokes("y y j \" _ d d . p").await;
483 cx.shared_state().await.assert_eq(indoc! {"
484 one
485 four
486 ˇone
487 "});
488
489 // the recorded register is replayed, not whatever is in the unnamed register
490 cx.set_shared_state(indoc! {"
491 ˇone
492 two
493 "})
494 .await;
495 cx.simulate_shared_keystrokes("y y j \" a y y \" a p .")
496 .await;
497 cx.shared_state().await.assert_eq(indoc! {"
498 one
499 two
500 two
501 ˇtwo
502 "});
503
504 // `"X.` ignores the override and always uses the recorded register.
505 // Both `dd` calls go into register `a`, so register `b` is empty and
506 // `"bp` pastes nothing.
507 cx.set_shared_state(indoc! {"
508 ˇone
509 two
510 three
511 "})
512 .await;
513 cx.simulate_shared_keystrokes("\" a d d \" b .").await;
514 cx.shared_state().await.assert_eq(indoc! {"
515 ˇthree
516 "});
517 cx.simulate_shared_keystrokes("\" a p \" b p").await;
518 cx.shared_state().await.assert_eq(indoc! {"
519 three
520 ˇtwo
521 "});
522
523 // numbered registers cycle on each dot repeat: "1p . . uses registers 2, 3, …
524 // Since the cycling behavior caps at register 9, the first line to be
525 // deleted `1`, is no longer in any of the registers.
526 cx.set_shared_state(indoc! {"
527 ˇone
528 two
529 three
530 four
531 five
532 six
533 seven
534 eight
535 nine
536 ten
537 "})
538 .await;
539 cx.simulate_shared_keystrokes("d d . . . . . . . . .").await;
540 cx.shared_state().await.assert_eq(indoc! {"ˇ"});
541 cx.simulate_shared_keystrokes("\" 1 p . . . . . . . . .")
542 .await;
543 cx.shared_state().await.assert_eq(indoc! {"
544
545 ten
546 nine
547 eight
548 seven
549 six
550 five
551 four
552 three
553 two
554 ˇtwo"});
555
556 // unnamed register repeat: dd records None, so . pastes the same
557 // deleted text
558 cx.set_shared_state(indoc! {"
559 ˇone
560 two
561 three
562 "})
563 .await;
564 cx.simulate_shared_keystrokes("d d p .").await;
565 cx.shared_state().await.assert_eq(indoc! {"
566 two
567 one
568 ˇone
569 three
570 "});
571
572 // After `"1p` cycles to `2`, using `"ap` resets recorded_register to `a`,
573 // so the next `.` uses `a` and not 3.
574 cx.set_shared_state(indoc! {"
575 one
576 two
577 ˇthree
578 "})
579 .await;
580 cx.simulate_shared_keystrokes("\" 2 y y k k \" a y y j \" 1 y y k \" 1 p . \" a p .")
581 .await;
582 cx.shared_state().await.assert_eq(indoc! {"
583 one
584 two
585 three
586 one
587 ˇone
588 two
589 three
590 "});
591 }
592
593 // This needs to be a separate test from `test_dot_repeat_registers_paste`
594 // as Neovim doesn't have support for using registers in replace operations
595 // by default.
596 #[gpui::test]
597 async fn test_dot_repeat_registers_replace(cx: &mut gpui::TestAppContext) {
598 let mut cx = VimTestContext::new(cx, true).await;
599
600 cx.set_state(
601 indoc! {"
602 line ˇone
603 line two
604 line three
605 "},
606 Mode::Normal,
607 );
608
609 // 1. Yank `one` into register `a`
610 // 2. Move down and yank `two` into the default register
611 // 3. Replace `two` with the contents of register `a`
612 cx.simulate_keystrokes("\" a y w j y w \" a g R w");
613 cx.assert_state(
614 indoc! {"
615 line one
616 line onˇe
617 line three
618 "},
619 Mode::Normal,
620 );
621
622 // 1. Move down to `three`
623 // 2. Repeat the replace operation
624 cx.simulate_keystrokes("j .");
625 cx.assert_state(
626 indoc! {"
627 line one
628 line one
629 line onˇe
630 "},
631 Mode::Normal,
632 );
633
634 // Similar test, but this time using numbered registers, as those should
635 // automatically increase on successive uses of `.` .
636 cx.set_state(
637 indoc! {"
638 line ˇone
639 line two
640 line three
641 line four
642 "},
643 Mode::Normal,
644 );
645
646 // 1. Yank `one` into register `1`
647 // 2. Yank `two` into register `2`
648 // 3. Move down and yank `three` into the default register
649 // 4. Replace `three` with the contents of register `1`
650 // 5. Move down and repeat
651 cx.simulate_keystrokes("\" 1 y w j \" 2 y w j y w \" 1 g R w j .");
652 cx.assert_state(
653 indoc! {"
654 line one
655 line two
656 line one
657 line twˇo
658 "},
659 Mode::Normal,
660 );
661 }
662
663 #[gpui::test]
664 async fn test_repeat_ime(cx: &mut gpui::TestAppContext) {
665 let mut cx = VimTestContext::new(cx, true).await;
666
667 cx.set_state("hˇllo", Mode::Normal);
668 cx.simulate_keystrokes("i");
669
670 // simulate brazilian input for ä.
671 cx.update_editor(|editor, window, cx| {
672 editor.replace_and_mark_text_in_range(None, "\"", Some(1..1), window, cx);
673 editor.replace_text_in_range(None, "ä", window, cx);
674 });
675 cx.simulate_keystrokes("escape");
676 cx.assert_state("hˇällo", Mode::Normal);
677 cx.simulate_keystrokes(".");
678 cx.assert_state("hˇäällo", Mode::Normal);
679 }
680
681 #[gpui::test]
682 async fn test_repeat_completion(cx: &mut gpui::TestAppContext) {
683 VimTestContext::init(cx);
684 let cx = EditorLspTestContext::new_rust(
685 lsp::ServerCapabilities {
686 completion_provider: Some(lsp::CompletionOptions {
687 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
688 resolve_provider: Some(true),
689 ..Default::default()
690 }),
691 ..Default::default()
692 },
693 cx,
694 )
695 .await;
696 let mut cx = VimTestContext::new_with_lsp(cx, true);
697
698 cx.set_state(
699 indoc! {"
700 onˇe
701 two
702 three
703 "},
704 Mode::Normal,
705 );
706
707 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(
708 move |_, params, _| async move {
709 let position = params.text_document_position.position;
710 Ok(Some(lsp::CompletionResponse::Array(vec![
711 lsp::CompletionItem {
712 label: "first".to_string(),
713 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
714 range: lsp::Range::new(position, position),
715 new_text: "first".to_string(),
716 })),
717 ..Default::default()
718 },
719 lsp::CompletionItem {
720 label: "second".to_string(),
721 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
722 range: lsp::Range::new(position, position),
723 new_text: "second".to_string(),
724 })),
725 ..Default::default()
726 },
727 ])))
728 },
729 );
730 cx.simulate_keystrokes("a .");
731 request.next().await;
732 cx.condition(|editor, _| editor.context_menu_visible())
733 .await;
734 cx.simulate_keystrokes("down enter ! escape");
735
736 cx.assert_state(
737 indoc! {"
738 one.secondˇ!
739 two
740 three
741 "},
742 Mode::Normal,
743 );
744 cx.simulate_keystrokes("j .");
745 cx.assert_state(
746 indoc! {"
747 one.second!
748 two.secondˇ!
749 three
750 "},
751 Mode::Normal,
752 );
753 }
754
755 #[gpui::test]
756 async fn test_repeat_completion_unicode_bug(cx: &mut gpui::TestAppContext) {
757 VimTestContext::init(cx);
758 let cx = EditorLspTestContext::new_rust(
759 lsp::ServerCapabilities {
760 completion_provider: Some(lsp::CompletionOptions {
761 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
762 resolve_provider: Some(true),
763 ..Default::default()
764 }),
765 ..Default::default()
766 },
767 cx,
768 )
769 .await;
770 let mut cx = VimTestContext::new_with_lsp(cx, true);
771
772 cx.set_state(
773 indoc! {"
774 ĩлˇк
775 ĩлк
776 "},
777 Mode::Normal,
778 );
779
780 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(
781 move |_, params, _| async move {
782 let position = params.text_document_position.position;
783 let mut to_the_left = position;
784 to_the_left.character -= 2;
785 Ok(Some(lsp::CompletionResponse::Array(vec![
786 lsp::CompletionItem {
787 label: "oops".to_string(),
788 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
789 range: lsp::Range::new(to_the_left, position),
790 new_text: "к!".to_string(),
791 })),
792 ..Default::default()
793 },
794 ])))
795 },
796 );
797 cx.simulate_keystrokes("i .");
798 request.next().await;
799 cx.condition(|editor, _| editor.context_menu_visible())
800 .await;
801 cx.simulate_keystrokes("enter escape");
802 cx.assert_state(
803 indoc! {"
804 ĩкˇ!к
805 ĩлк
806 "},
807 Mode::Normal,
808 );
809 }
810
811 #[gpui::test]
812 async fn test_repeat_visual(cx: &mut gpui::TestAppContext) {
813 let mut cx = NeovimBackedTestContext::new(cx).await;
814
815 // single-line (3 columns)
816 cx.set_shared_state(indoc! {
817 "ˇthe quick brown
818 fox jumps over
819 the lazy dog"
820 })
821 .await;
822 cx.simulate_shared_keystrokes("v i w s o escape").await;
823 cx.shared_state().await.assert_eq(indoc! {
824 "ˇo quick brown
825 fox jumps over
826 the lazy dog"
827 });
828 cx.simulate_shared_keystrokes("j w .").await;
829 cx.shared_state().await.assert_eq(indoc! {
830 "o quick brown
831 fox ˇops over
832 the lazy dog"
833 });
834 cx.simulate_shared_keystrokes("f r .").await;
835 cx.shared_state().await.assert_eq(indoc! {
836 "o quick brown
837 fox ops oveˇothe lazy dog"
838 });
839
840 // visual
841 cx.set_shared_state(indoc! {
842 "the ˇquick brown
843 fox jumps over
844 fox jumps over
845 fox jumps over
846 the lazy dog"
847 })
848 .await;
849 cx.simulate_shared_keystrokes("v j x").await;
850 cx.shared_state().await.assert_eq(indoc! {
851 "the ˇumps over
852 fox jumps over
853 fox jumps over
854 the lazy dog"
855 });
856 cx.simulate_shared_keystrokes(".").await;
857 cx.shared_state().await.assert_eq(indoc! {
858 "the ˇumps over
859 fox jumps over
860 the lazy dog"
861 });
862 cx.simulate_shared_keystrokes("w .").await;
863 cx.shared_state().await.assert_eq(indoc! {
864 "the umps ˇumps over
865 the lazy dog"
866 });
867 cx.simulate_shared_keystrokes("j .").await;
868 cx.shared_state().await.assert_eq(indoc! {
869 "the umps umps over
870 the ˇog"
871 });
872
873 // block mode (3 rows)
874 cx.set_shared_state(indoc! {
875 "ˇthe quick brown
876 fox jumps over
877 the lazy dog"
878 })
879 .await;
880 cx.simulate_shared_keystrokes("ctrl-v j j shift-i o escape")
881 .await;
882 cx.shared_state().await.assert_eq(indoc! {
883 "ˇothe quick brown
884 ofox jumps over
885 othe lazy dog"
886 });
887 cx.simulate_shared_keystrokes("j 4 l .").await;
888 cx.shared_state().await.assert_eq(indoc! {
889 "othe quick brown
890 ofoxˇo jumps over
891 otheo lazy dog"
892 });
893
894 // line mode
895 cx.set_shared_state(indoc! {
896 "ˇthe quick brown
897 fox jumps over
898 the lazy dog"
899 })
900 .await;
901 cx.simulate_shared_keystrokes("shift-v shift-r o escape")
902 .await;
903 cx.shared_state().await.assert_eq(indoc! {
904 "ˇo
905 fox jumps over
906 the lazy dog"
907 });
908 cx.simulate_shared_keystrokes("j .").await;
909 cx.shared_state().await.assert_eq(indoc! {
910 "o
911 ˇo
912 the lazy dog"
913 });
914 }
915
916 #[gpui::test]
917 async fn test_repeat_motion_counts(cx: &mut gpui::TestAppContext) {
918 let mut cx = NeovimBackedTestContext::new(cx).await;
919
920 cx.set_shared_state(indoc! {
921 "ˇthe quick brown
922 fox jumps over
923 the lazy dog"
924 })
925 .await;
926 cx.simulate_shared_keystrokes("3 d 3 l").await;
927 cx.shared_state().await.assert_eq(indoc! {
928 "ˇ brown
929 fox jumps over
930 the lazy dog"
931 });
932 cx.simulate_shared_keystrokes("j .").await;
933 cx.shared_state().await.assert_eq(indoc! {
934 " brown
935 ˇ over
936 the lazy dog"
937 });
938 cx.simulate_shared_keystrokes("j 2 .").await;
939 cx.shared_state().await.assert_eq(indoc! {
940 " brown
941 over
942 ˇe lazy dog"
943 });
944 }
945
946 #[gpui::test]
947 async fn test_record_interrupted(cx: &mut gpui::TestAppContext) {
948 let mut cx = VimTestContext::new(cx, true).await;
949
950 cx.set_state("ˇhello\n", Mode::Normal);
951 cx.simulate_keystrokes("4 i j cmd-shift-p escape");
952 cx.simulate_keystrokes("escape");
953 cx.assert_state("ˇjhello\n", Mode::Normal);
954 }
955
956 #[gpui::test]
957 async fn test_repeat_over_blur(cx: &mut gpui::TestAppContext) {
958 let mut cx = NeovimBackedTestContext::new(cx).await;
959
960 cx.set_shared_state("ˇhello hello hello\n").await;
961 cx.simulate_shared_keystrokes("c f o x escape").await;
962 cx.shared_state().await.assert_eq("ˇx hello hello\n");
963 cx.simulate_shared_keystrokes(": escape").await;
964 cx.simulate_shared_keystrokes(".").await;
965 cx.shared_state().await.assert_eq("ˇx hello\n");
966 }
967
968 #[gpui::test]
969 async fn test_repeat_after_blur_resets_dot_replaying(cx: &mut gpui::TestAppContext) {
970 let mut cx = VimTestContext::new(cx, true).await;
971
972 // Bind `ctrl-f` to the `buffer_search::Deploy` action so that this can
973 // be triggered while in Insert mode, ensuring that an action which
974 // moves the focus away from the editor, gets recorded.
975 cx.update(|_, cx| {
976 cx.bind_keys([gpui::KeyBinding::new(
977 "ctrl-f",
978 search::buffer_search::Deploy::find(),
979 None,
980 )])
981 });
982
983 cx.set_state("ˇhello", Mode::Normal);
984
985 // We're going to enter insert mode, which will start recording, type a
986 // character and then immediately use `ctrl-f` to trigger the buffer
987 // search. Triggering the buffer search will move focus away from the
988 // editor, effectively stopping the recording immediately after
989 // `buffer_search::Deploy` is recorded. The first `escape` is used to
990 // dismiss the search bar, while the second is used to move from Insert
991 // to Normal mode.
992 cx.simulate_keystrokes("i x ctrl-f escape escape");
993 cx.run_until_parked();
994
995 // Using the `.` key will dispatch the `vim::Repeat` action, repeating
996 // the set of recorded actions. This will eventually focus on the search
997 // bar, preventing the `EndRepeat` action from being correctly handled.
998 cx.simulate_keystrokes(".");
999 cx.run_until_parked();
1000
1001 // After replay finishes, even though the `EndRepeat` action wasn't
1002 // handled, seeing as the editor lost focus during replay, the
1003 // `dot_replaying` value should be set back to `false`.
1004 assert!(
1005 !cx.update(|_, cx| cx.global::<VimGlobals>().dot_replaying),
1006 "dot_replaying should be false after repeat completes"
1007 );
1008 }
1009
1010 #[gpui::test]
1011 async fn test_undo_repeated_insert(cx: &mut gpui::TestAppContext) {
1012 let mut cx = NeovimBackedTestContext::new(cx).await;
1013
1014 cx.set_shared_state("hellˇo").await;
1015 cx.simulate_shared_keystrokes("3 a . escape").await;
1016 cx.shared_state().await.assert_eq("hello..ˇ.");
1017 cx.simulate_shared_keystrokes("u").await;
1018 cx.shared_state().await.assert_eq("hellˇo");
1019 }
1020
1021 #[gpui::test]
1022 async fn test_record_replay(cx: &mut gpui::TestAppContext) {
1023 let mut cx = NeovimBackedTestContext::new(cx).await;
1024
1025 cx.set_shared_state("ˇhello world").await;
1026 cx.simulate_shared_keystrokes("q w c w j escape q").await;
1027 cx.shared_state().await.assert_eq("ˇj world");
1028 cx.simulate_shared_keystrokes("2 l @ w").await;
1029 cx.shared_state().await.assert_eq("j ˇj");
1030 }
1031
1032 #[gpui::test]
1033 async fn test_record_replay_count(cx: &mut gpui::TestAppContext) {
1034 let mut cx = NeovimBackedTestContext::new(cx).await;
1035
1036 cx.set_shared_state("ˇhello world!!").await;
1037 cx.simulate_shared_keystrokes("q a v 3 l s 0 escape l q")
1038 .await;
1039 cx.shared_state().await.assert_eq("0ˇo world!!");
1040 cx.simulate_shared_keystrokes("2 @ a").await;
1041 cx.shared_state().await.assert_eq("000ˇ!");
1042 }
1043
1044 #[gpui::test]
1045 async fn test_record_replay_dot(cx: &mut gpui::TestAppContext) {
1046 let mut cx = NeovimBackedTestContext::new(cx).await;
1047
1048 cx.set_shared_state("ˇhello world").await;
1049 cx.simulate_shared_keystrokes("q a r a l r b l q").await;
1050 cx.shared_state().await.assert_eq("abˇllo world");
1051 cx.simulate_shared_keystrokes(".").await;
1052 cx.shared_state().await.assert_eq("abˇblo world");
1053 cx.simulate_shared_keystrokes("shift-q").await;
1054 cx.shared_state().await.assert_eq("ababˇo world");
1055 cx.simulate_shared_keystrokes(".").await;
1056 cx.shared_state().await.assert_eq("ababˇb world");
1057 }
1058
1059 #[gpui::test]
1060 async fn test_record_replay_of_dot(cx: &mut gpui::TestAppContext) {
1061 let mut cx = NeovimBackedTestContext::new(cx).await;
1062
1063 cx.set_shared_state("ˇhello world").await;
1064 cx.simulate_shared_keystrokes("r o q w . q").await;
1065 cx.shared_state().await.assert_eq("ˇoello world");
1066 cx.simulate_shared_keystrokes("d l").await;
1067 cx.shared_state().await.assert_eq("ˇello world");
1068 cx.simulate_shared_keystrokes("@ w").await;
1069 cx.shared_state().await.assert_eq("ˇllo world");
1070 }
1071
1072 #[gpui::test]
1073 async fn test_record_replay_interleaved(cx: &mut gpui::TestAppContext) {
1074 let mut cx = NeovimBackedTestContext::new(cx).await;
1075
1076 cx.set_shared_state("ˇhello world").await;
1077 cx.simulate_shared_keystrokes("q z r a l q").await;
1078 cx.shared_state().await.assert_eq("aˇello world");
1079 cx.simulate_shared_keystrokes("q b @ z @ z q").await;
1080 cx.shared_state().await.assert_eq("aaaˇlo world");
1081 cx.simulate_shared_keystrokes("@ @").await;
1082 cx.shared_state().await.assert_eq("aaaaˇo world");
1083 cx.simulate_shared_keystrokes("@ b").await;
1084 cx.shared_state().await.assert_eq("aaaaaaˇworld");
1085 cx.simulate_shared_keystrokes("@ @").await;
1086 cx.shared_state().await.assert_eq("aaaaaaaˇorld");
1087 cx.simulate_shared_keystrokes("q z r b l q").await;
1088 cx.shared_state().await.assert_eq("aaaaaaabˇrld");
1089 cx.simulate_shared_keystrokes("@ b").await;
1090 cx.shared_state().await.assert_eq("aaaaaaabbbˇd");
1091 }
1092
1093 #[gpui::test]
1094 async fn test_repeat_clear(cx: &mut gpui::TestAppContext) {
1095 let mut cx = VimTestContext::new(cx, true).await;
1096
1097 // Check that, when repeat is preceded by something other than a number,
1098 // the current operator is cleared, in order to prevent infinite loops.
1099 cx.set_state("ˇhello world", Mode::Normal);
1100 cx.simulate_keystrokes("d .");
1101 assert_eq!(cx.active_operator(), None);
1102 }
1103
1104 #[gpui::test]
1105 async fn test_repeat_clear_repeat(cx: &mut gpui::TestAppContext) {
1106 let mut cx = NeovimBackedTestContext::new(cx).await;
1107
1108 cx.set_shared_state(indoc! {
1109 "ˇthe quick brown
1110 fox jumps over
1111 the lazy dog"
1112 })
1113 .await;
1114 cx.simulate_shared_keystrokes("d d").await;
1115 cx.shared_state().await.assert_eq(indoc! {
1116 "ˇfox jumps over
1117 the lazy dog"
1118 });
1119 cx.simulate_shared_keystrokes("d . .").await;
1120 cx.shared_state().await.assert_eq(indoc! {
1121 "ˇthe lazy dog"
1122 });
1123 }
1124
1125 #[gpui::test]
1126 async fn test_repeat_clear_count(cx: &mut gpui::TestAppContext) {
1127 let mut cx = NeovimBackedTestContext::new(cx).await;
1128
1129 cx.set_shared_state(indoc! {
1130 "ˇthe quick brown
1131 fox jumps over
1132 the lazy dog"
1133 })
1134 .await;
1135 cx.simulate_shared_keystrokes("d d").await;
1136 cx.shared_state().await.assert_eq(indoc! {
1137 "ˇfox jumps over
1138 the lazy dog"
1139 });
1140 cx.simulate_shared_keystrokes("2 d .").await;
1141 cx.shared_state().await.assert_eq(indoc! {
1142 "ˇfox jumps over
1143 the lazy dog"
1144 });
1145 cx.simulate_shared_keystrokes(".").await;
1146 cx.shared_state().await.assert_eq(indoc! {
1147 "ˇthe lazy dog"
1148 });
1149
1150 cx.set_shared_state(indoc! {
1151 "ˇthe quick brown
1152 fox jumps over
1153 the lazy dog
1154 the quick brown
1155 fox jumps over
1156 the lazy dog"
1157 })
1158 .await;
1159 cx.simulate_shared_keystrokes("2 d d").await;
1160 cx.shared_state().await.assert_eq(indoc! {
1161 "ˇthe lazy dog
1162 the quick brown
1163 fox jumps over
1164 the lazy dog"
1165 });
1166 cx.simulate_shared_keystrokes("5 d .").await;
1167 cx.shared_state().await.assert_eq(indoc! {
1168 "ˇthe lazy dog
1169 the quick brown
1170 fox jumps over
1171 the lazy dog"
1172 });
1173 cx.simulate_shared_keystrokes(".").await;
1174 cx.shared_state().await.assert_eq(indoc! {
1175 "ˇfox jumps over
1176 the lazy dog"
1177 });
1178 }
1179}