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(Some(workspace)) = window.root::<Workspace>() 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(Some(workspace)) = window.root::<Workspace>() 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 if mode != Some(self.mode) {
295 if let Some(mode) = mode {
296 self.switch_mode(mode, false, window, cx)
297 }
298
299 match selection {
300 RecordedSelection::SingleLine { cols } => {
301 if cols > 1 {
302 self.visual_motion(Motion::Right, Some(cols as usize - 1), window, cx)
303 }
304 }
305 RecordedSelection::Visual { rows, cols } => {
306 self.visual_motion(
307 Motion::Down {
308 display_lines: false,
309 },
310 Some(rows as usize),
311 window,
312 cx,
313 );
314 self.visual_motion(
315 Motion::StartOfLine {
316 display_lines: false,
317 },
318 None,
319 window,
320 cx,
321 );
322 if cols > 1 {
323 self.visual_motion(Motion::Right, Some(cols as usize - 1), window, cx)
324 }
325 }
326 RecordedSelection::VisualBlock { rows, cols } => {
327 self.visual_motion(
328 Motion::Down {
329 display_lines: false,
330 },
331 Some(rows as usize),
332 window,
333 cx,
334 );
335 if cols > 1 {
336 self.visual_motion(Motion::Right, Some(cols as usize - 1), window, cx);
337 }
338 }
339 RecordedSelection::VisualLine { rows } => {
340 self.visual_motion(
341 Motion::Down {
342 display_lines: false,
343 },
344 Some(rows as usize),
345 window,
346 cx,
347 );
348 }
349 RecordedSelection::None => {}
350 }
351 }
352
353 // insert internally uses repeat to handle counts
354 // vim doesn't treat 3a1 as though you literally repeated a1
355 // 3 times, instead it inserts the content thrice at the insert position.
356 if let Some(to_repeat) = repeatable_insert(&actions[0]) {
357 if let Some(ReplayableAction::Action(action)) = actions.last()
358 && NormalBefore.partial_eq(&**action)
359 {
360 actions.pop();
361 }
362
363 let mut new_actions = actions.clone();
364 actions[0] = ReplayableAction::Action(to_repeat.boxed_clone());
365
366 let mut count = cx.global::<VimGlobals>().recorded_count.unwrap_or(1);
367
368 // if we came from insert mode we're just doing repetitions 2 onwards.
369 if from_insert_mode {
370 count -= 1;
371 new_actions[0] = actions[0].clone();
372 }
373
374 for _ in 1..count {
375 new_actions.append(actions.clone().as_mut());
376 }
377 new_actions.push(ReplayableAction::Action(NormalBefore.boxed_clone()));
378 actions = new_actions;
379 }
380
381 actions.push(ReplayableAction::Action(EndRepeat.boxed_clone()));
382
383 if self.temp_mode {
384 self.temp_mode = false;
385 actions.push(ReplayableAction::Action(InsertBefore.boxed_clone()));
386 }
387
388 let globals = Vim::globals(cx);
389 globals.dot_replaying = true;
390 let mut replayer = globals.replayer.get_or_insert_with(Replayer::new).clone();
391
392 replayer.replay(actions, window, cx);
393 }
394}
395
396#[cfg(test)]
397mod test {
398 use editor::test::editor_lsp_test_context::EditorLspTestContext;
399 use futures::StreamExt;
400 use indoc::indoc;
401
402 use gpui::EntityInputHandler;
403
404 use crate::{
405 VimGlobals,
406 state::Mode,
407 test::{NeovimBackedTestContext, VimTestContext},
408 };
409
410 #[gpui::test]
411 async fn test_dot_repeat(cx: &mut gpui::TestAppContext) {
412 let mut cx = NeovimBackedTestContext::new(cx).await;
413
414 // "o"
415 cx.set_shared_state("ˇhello").await;
416 cx.simulate_shared_keystrokes("o w o r l d escape").await;
417 cx.shared_state().await.assert_eq("hello\nworlˇd");
418 cx.simulate_shared_keystrokes(".").await;
419 cx.shared_state().await.assert_eq("hello\nworld\nworlˇd");
420
421 // "d"
422 cx.simulate_shared_keystrokes("^ d f o").await;
423 cx.simulate_shared_keystrokes("g g .").await;
424 cx.shared_state().await.assert_eq("ˇ\nworld\nrld");
425
426 // "p" (note that it pastes the current clipboard)
427 cx.simulate_shared_keystrokes("j y y p").await;
428 cx.simulate_shared_keystrokes("shift-g y y .").await;
429 cx.shared_state()
430 .await
431 .assert_eq("\nworld\nworld\nrld\nˇrld");
432
433 // "~" (note that counts apply to the action taken, not . itself)
434 cx.set_shared_state("ˇthe quick brown fox").await;
435 cx.simulate_shared_keystrokes("2 ~ .").await;
436 cx.set_shared_state("THE ˇquick brown fox").await;
437 cx.simulate_shared_keystrokes("3 .").await;
438 cx.set_shared_state("THE QUIˇck brown fox").await;
439 cx.run_until_parked();
440 cx.simulate_shared_keystrokes(".").await;
441 cx.shared_state().await.assert_eq("THE QUICK ˇbrown fox");
442 }
443
444 #[gpui::test]
445 async fn test_repeat_ime(cx: &mut gpui::TestAppContext) {
446 let mut cx = VimTestContext::new(cx, true).await;
447
448 cx.set_state("hˇllo", Mode::Normal);
449 cx.simulate_keystrokes("i");
450
451 // simulate brazilian input for ä.
452 cx.update_editor(|editor, window, cx| {
453 editor.replace_and_mark_text_in_range(None, "\"", Some(1..1), window, cx);
454 editor.replace_text_in_range(None, "ä", window, cx);
455 });
456 cx.simulate_keystrokes("escape");
457 cx.assert_state("hˇällo", Mode::Normal);
458 cx.simulate_keystrokes(".");
459 cx.assert_state("hˇäällo", Mode::Normal);
460 }
461
462 #[gpui::test]
463 async fn test_repeat_completion(cx: &mut gpui::TestAppContext) {
464 VimTestContext::init(cx);
465 let cx = EditorLspTestContext::new_rust(
466 lsp::ServerCapabilities {
467 completion_provider: Some(lsp::CompletionOptions {
468 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
469 resolve_provider: Some(true),
470 ..Default::default()
471 }),
472 ..Default::default()
473 },
474 cx,
475 )
476 .await;
477 let mut cx = VimTestContext::new_with_lsp(cx, true);
478
479 cx.set_state(
480 indoc! {"
481 onˇe
482 two
483 three
484 "},
485 Mode::Normal,
486 );
487
488 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(
489 move |_, params, _| async move {
490 let position = params.text_document_position.position;
491 Ok(Some(lsp::CompletionResponse::Array(vec![
492 lsp::CompletionItem {
493 label: "first".to_string(),
494 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
495 range: lsp::Range::new(position, position),
496 new_text: "first".to_string(),
497 })),
498 ..Default::default()
499 },
500 lsp::CompletionItem {
501 label: "second".to_string(),
502 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
503 range: lsp::Range::new(position, position),
504 new_text: "second".to_string(),
505 })),
506 ..Default::default()
507 },
508 ])))
509 },
510 );
511 cx.simulate_keystrokes("a .");
512 request.next().await;
513 cx.condition(|editor, _| editor.context_menu_visible())
514 .await;
515 cx.simulate_keystrokes("down enter ! escape");
516
517 cx.assert_state(
518 indoc! {"
519 one.secondˇ!
520 two
521 three
522 "},
523 Mode::Normal,
524 );
525 cx.simulate_keystrokes("j .");
526 cx.assert_state(
527 indoc! {"
528 one.second!
529 two.secondˇ!
530 three
531 "},
532 Mode::Normal,
533 );
534 }
535
536 #[gpui::test]
537 async fn test_repeat_completion_unicode_bug(cx: &mut gpui::TestAppContext) {
538 VimTestContext::init(cx);
539 let cx = EditorLspTestContext::new_rust(
540 lsp::ServerCapabilities {
541 completion_provider: Some(lsp::CompletionOptions {
542 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
543 resolve_provider: Some(true),
544 ..Default::default()
545 }),
546 ..Default::default()
547 },
548 cx,
549 )
550 .await;
551 let mut cx = VimTestContext::new_with_lsp(cx, true);
552
553 cx.set_state(
554 indoc! {"
555 ĩлˇк
556 ĩлк
557 "},
558 Mode::Normal,
559 );
560
561 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(
562 move |_, params, _| async move {
563 let position = params.text_document_position.position;
564 let mut to_the_left = position;
565 to_the_left.character -= 2;
566 Ok(Some(lsp::CompletionResponse::Array(vec![
567 lsp::CompletionItem {
568 label: "oops".to_string(),
569 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
570 range: lsp::Range::new(to_the_left, position),
571 new_text: "к!".to_string(),
572 })),
573 ..Default::default()
574 },
575 ])))
576 },
577 );
578 cx.simulate_keystrokes("i .");
579 request.next().await;
580 cx.condition(|editor, _| editor.context_menu_visible())
581 .await;
582 cx.simulate_keystrokes("enter escape");
583 cx.assert_state(
584 indoc! {"
585 ĩкˇ!к
586 ĩлк
587 "},
588 Mode::Normal,
589 );
590 }
591
592 #[gpui::test]
593 async fn test_repeat_visual(cx: &mut gpui::TestAppContext) {
594 let mut cx = NeovimBackedTestContext::new(cx).await;
595
596 // single-line (3 columns)
597 cx.set_shared_state(indoc! {
598 "ˇthe quick brown
599 fox jumps over
600 the lazy dog"
601 })
602 .await;
603 cx.simulate_shared_keystrokes("v i w s o escape").await;
604 cx.shared_state().await.assert_eq(indoc! {
605 "ˇo quick brown
606 fox jumps over
607 the lazy dog"
608 });
609 cx.simulate_shared_keystrokes("j w .").await;
610 cx.shared_state().await.assert_eq(indoc! {
611 "o quick brown
612 fox ˇops over
613 the lazy dog"
614 });
615 cx.simulate_shared_keystrokes("f r .").await;
616 cx.shared_state().await.assert_eq(indoc! {
617 "o quick brown
618 fox ops oveˇothe lazy dog"
619 });
620
621 // visual
622 cx.set_shared_state(indoc! {
623 "the ˇquick brown
624 fox jumps over
625 fox jumps over
626 fox jumps over
627 the lazy dog"
628 })
629 .await;
630 cx.simulate_shared_keystrokes("v j x").await;
631 cx.shared_state().await.assert_eq(indoc! {
632 "the ˇumps over
633 fox jumps over
634 fox jumps over
635 the lazy dog"
636 });
637 cx.simulate_shared_keystrokes(".").await;
638 cx.shared_state().await.assert_eq(indoc! {
639 "the ˇumps over
640 fox jumps over
641 the lazy dog"
642 });
643 cx.simulate_shared_keystrokes("w .").await;
644 cx.shared_state().await.assert_eq(indoc! {
645 "the umps ˇumps over
646 the lazy dog"
647 });
648 cx.simulate_shared_keystrokes("j .").await;
649 cx.shared_state().await.assert_eq(indoc! {
650 "the umps umps over
651 the ˇog"
652 });
653
654 // block mode (3 rows)
655 cx.set_shared_state(indoc! {
656 "ˇthe quick brown
657 fox jumps over
658 the lazy dog"
659 })
660 .await;
661 cx.simulate_shared_keystrokes("ctrl-v j j shift-i o escape")
662 .await;
663 cx.shared_state().await.assert_eq(indoc! {
664 "ˇothe quick brown
665 ofox jumps over
666 othe lazy dog"
667 });
668 cx.simulate_shared_keystrokes("j 4 l .").await;
669 cx.shared_state().await.assert_eq(indoc! {
670 "othe quick brown
671 ofoxˇo jumps over
672 otheo lazy dog"
673 });
674
675 // line mode
676 cx.set_shared_state(indoc! {
677 "ˇthe quick brown
678 fox jumps over
679 the lazy dog"
680 })
681 .await;
682 cx.simulate_shared_keystrokes("shift-v shift-r o escape")
683 .await;
684 cx.shared_state().await.assert_eq(indoc! {
685 "ˇo
686 fox jumps over
687 the lazy dog"
688 });
689 cx.simulate_shared_keystrokes("j .").await;
690 cx.shared_state().await.assert_eq(indoc! {
691 "o
692 ˇo
693 the lazy dog"
694 });
695 }
696
697 #[gpui::test]
698 async fn test_repeat_motion_counts(cx: &mut gpui::TestAppContext) {
699 let mut cx = NeovimBackedTestContext::new(cx).await;
700
701 cx.set_shared_state(indoc! {
702 "ˇthe quick brown
703 fox jumps over
704 the lazy dog"
705 })
706 .await;
707 cx.simulate_shared_keystrokes("3 d 3 l").await;
708 cx.shared_state().await.assert_eq(indoc! {
709 "ˇ brown
710 fox jumps over
711 the lazy dog"
712 });
713 cx.simulate_shared_keystrokes("j .").await;
714 cx.shared_state().await.assert_eq(indoc! {
715 " brown
716 ˇ over
717 the lazy dog"
718 });
719 cx.simulate_shared_keystrokes("j 2 .").await;
720 cx.shared_state().await.assert_eq(indoc! {
721 " brown
722 over
723 ˇe lazy dog"
724 });
725 }
726
727 #[gpui::test]
728 async fn test_record_interrupted(cx: &mut gpui::TestAppContext) {
729 let mut cx = VimTestContext::new(cx, true).await;
730
731 cx.set_state("ˇhello\n", Mode::Normal);
732 cx.simulate_keystrokes("4 i j cmd-shift-p escape");
733 cx.simulate_keystrokes("escape");
734 cx.assert_state("ˇjhello\n", Mode::Normal);
735 }
736
737 #[gpui::test]
738 async fn test_repeat_over_blur(cx: &mut gpui::TestAppContext) {
739 let mut cx = NeovimBackedTestContext::new(cx).await;
740
741 cx.set_shared_state("ˇhello hello hello\n").await;
742 cx.simulate_shared_keystrokes("c f o x escape").await;
743 cx.shared_state().await.assert_eq("ˇx hello hello\n");
744 cx.simulate_shared_keystrokes(": escape").await;
745 cx.simulate_shared_keystrokes(".").await;
746 cx.shared_state().await.assert_eq("ˇx hello\n");
747 }
748
749 #[gpui::test]
750 async fn test_repeat_after_blur_resets_dot_replaying(cx: &mut gpui::TestAppContext) {
751 let mut cx = VimTestContext::new(cx, true).await;
752
753 // Bind `ctrl-f` to the `buffer_search::Deploy` action so that this can
754 // be triggered while in Insert mode, ensuring that an action which
755 // moves the focus away from the editor, gets recorded.
756 cx.update(|_, cx| {
757 cx.bind_keys([gpui::KeyBinding::new(
758 "ctrl-f",
759 search::buffer_search::Deploy::find(),
760 None,
761 )])
762 });
763
764 cx.set_state("ˇhello", Mode::Normal);
765
766 // We're going to enter insert mode, which will start recording, type a
767 // character and then immediately use `ctrl-f` to trigger the buffer
768 // search. Triggering the buffer search will move focus away from the
769 // editor, effectively stopping the recording immediately after
770 // `buffer_search::Deploy` is recorded. The first `escape` is used to
771 // dismiss the search bar, while the second is used to move from Insert
772 // to Normal mode.
773 cx.simulate_keystrokes("i x ctrl-f escape escape");
774 cx.run_until_parked();
775
776 // Using the `.` key will dispatch the `vim::Repeat` action, repeating
777 // the set of recorded actions. This will eventually focus on the search
778 // bar, preventing the `EndRepeat` action from being correctly handled.
779 cx.simulate_keystrokes(".");
780 cx.run_until_parked();
781
782 // After replay finishes, even though the `EndRepeat` action wasn't
783 // handled, seeing as the editor lost focus during replay, the
784 // `dot_replaying` value should be set back to `false`.
785 assert!(
786 !cx.update(|_, cx| cx.global::<VimGlobals>().dot_replaying),
787 "dot_replaying should be false after repeat completes"
788 );
789 }
790
791 #[gpui::test]
792 async fn test_undo_repeated_insert(cx: &mut gpui::TestAppContext) {
793 let mut cx = NeovimBackedTestContext::new(cx).await;
794
795 cx.set_shared_state("hellˇo").await;
796 cx.simulate_shared_keystrokes("3 a . escape").await;
797 cx.shared_state().await.assert_eq("hello..ˇ.");
798 cx.simulate_shared_keystrokes("u").await;
799 cx.shared_state().await.assert_eq("hellˇo");
800 }
801
802 #[gpui::test]
803 async fn test_record_replay(cx: &mut gpui::TestAppContext) {
804 let mut cx = NeovimBackedTestContext::new(cx).await;
805
806 cx.set_shared_state("ˇhello world").await;
807 cx.simulate_shared_keystrokes("q w c w j escape q").await;
808 cx.shared_state().await.assert_eq("ˇj world");
809 cx.simulate_shared_keystrokes("2 l @ w").await;
810 cx.shared_state().await.assert_eq("j ˇj");
811 }
812
813 #[gpui::test]
814 async fn test_record_replay_count(cx: &mut gpui::TestAppContext) {
815 let mut cx = NeovimBackedTestContext::new(cx).await;
816
817 cx.set_shared_state("ˇhello world!!").await;
818 cx.simulate_shared_keystrokes("q a v 3 l s 0 escape l q")
819 .await;
820 cx.shared_state().await.assert_eq("0ˇo world!!");
821 cx.simulate_shared_keystrokes("2 @ a").await;
822 cx.shared_state().await.assert_eq("000ˇ!");
823 }
824
825 #[gpui::test]
826 async fn test_record_replay_dot(cx: &mut gpui::TestAppContext) {
827 let mut cx = NeovimBackedTestContext::new(cx).await;
828
829 cx.set_shared_state("ˇhello world").await;
830 cx.simulate_shared_keystrokes("q a r a l r b l q").await;
831 cx.shared_state().await.assert_eq("abˇllo world");
832 cx.simulate_shared_keystrokes(".").await;
833 cx.shared_state().await.assert_eq("abˇblo world");
834 cx.simulate_shared_keystrokes("shift-q").await;
835 cx.shared_state().await.assert_eq("ababˇo world");
836 cx.simulate_shared_keystrokes(".").await;
837 cx.shared_state().await.assert_eq("ababˇb world");
838 }
839
840 #[gpui::test]
841 async fn test_record_replay_of_dot(cx: &mut gpui::TestAppContext) {
842 let mut cx = NeovimBackedTestContext::new(cx).await;
843
844 cx.set_shared_state("ˇhello world").await;
845 cx.simulate_shared_keystrokes("r o q w . q").await;
846 cx.shared_state().await.assert_eq("ˇoello world");
847 cx.simulate_shared_keystrokes("d l").await;
848 cx.shared_state().await.assert_eq("ˇello world");
849 cx.simulate_shared_keystrokes("@ w").await;
850 cx.shared_state().await.assert_eq("ˇllo world");
851 }
852
853 #[gpui::test]
854 async fn test_record_replay_interleaved(cx: &mut gpui::TestAppContext) {
855 let mut cx = NeovimBackedTestContext::new(cx).await;
856
857 cx.set_shared_state("ˇhello world").await;
858 cx.simulate_shared_keystrokes("q z r a l q").await;
859 cx.shared_state().await.assert_eq("aˇello world");
860 cx.simulate_shared_keystrokes("q b @ z @ z q").await;
861 cx.shared_state().await.assert_eq("aaaˇlo world");
862 cx.simulate_shared_keystrokes("@ @").await;
863 cx.shared_state().await.assert_eq("aaaaˇo world");
864 cx.simulate_shared_keystrokes("@ b").await;
865 cx.shared_state().await.assert_eq("aaaaaaˇworld");
866 cx.simulate_shared_keystrokes("@ @").await;
867 cx.shared_state().await.assert_eq("aaaaaaaˇorld");
868 cx.simulate_shared_keystrokes("q z r b l q").await;
869 cx.shared_state().await.assert_eq("aaaaaaabˇrld");
870 cx.simulate_shared_keystrokes("@ b").await;
871 cx.shared_state().await.assert_eq("aaaaaaabbbˇd");
872 }
873
874 #[gpui::test]
875 async fn test_repeat_clear(cx: &mut gpui::TestAppContext) {
876 let mut cx = VimTestContext::new(cx, true).await;
877
878 // Check that, when repeat is preceded by something other than a number,
879 // the current operator is cleared, in order to prevent infinite loops.
880 cx.set_state("ˇhello world", Mode::Normal);
881 cx.simulate_keystrokes("d .");
882 assert_eq!(cx.active_operator(), None);
883 }
884
885 #[gpui::test]
886 async fn test_repeat_clear_repeat(cx: &mut gpui::TestAppContext) {
887 let mut cx = NeovimBackedTestContext::new(cx).await;
888
889 cx.set_shared_state(indoc! {
890 "ˇthe quick brown
891 fox jumps over
892 the lazy dog"
893 })
894 .await;
895 cx.simulate_shared_keystrokes("d d").await;
896 cx.shared_state().await.assert_eq(indoc! {
897 "ˇfox jumps over
898 the lazy dog"
899 });
900 cx.simulate_shared_keystrokes("d . .").await;
901 cx.shared_state().await.assert_eq(indoc! {
902 "ˇthe lazy dog"
903 });
904 }
905
906 #[gpui::test]
907 async fn test_repeat_clear_count(cx: &mut gpui::TestAppContext) {
908 let mut cx = NeovimBackedTestContext::new(cx).await;
909
910 cx.set_shared_state(indoc! {
911 "ˇthe quick brown
912 fox jumps over
913 the lazy dog"
914 })
915 .await;
916 cx.simulate_shared_keystrokes("d d").await;
917 cx.shared_state().await.assert_eq(indoc! {
918 "ˇfox jumps over
919 the lazy dog"
920 });
921 cx.simulate_shared_keystrokes("2 d .").await;
922 cx.shared_state().await.assert_eq(indoc! {
923 "ˇfox jumps over
924 the lazy dog"
925 });
926 cx.simulate_shared_keystrokes(".").await;
927 cx.shared_state().await.assert_eq(indoc! {
928 "ˇthe lazy dog"
929 });
930
931 cx.set_shared_state(indoc! {
932 "ˇthe quick brown
933 fox jumps over
934 the lazy dog
935 the quick brown
936 fox jumps over
937 the lazy dog"
938 })
939 .await;
940 cx.simulate_shared_keystrokes("2 d d").await;
941 cx.shared_state().await.assert_eq(indoc! {
942 "ˇthe lazy dog
943 the quick brown
944 fox jumps over
945 the lazy dog"
946 });
947 cx.simulate_shared_keystrokes("5 d .").await;
948 cx.shared_state().await.assert_eq(indoc! {
949 "ˇthe lazy dog
950 the quick brown
951 fox jumps over
952 the lazy dog"
953 });
954 cx.simulate_shared_keystrokes(".").await;
955 cx.shared_state().await.assert_eq(indoc! {
956 "ˇfox jumps over
957 the lazy dog"
958 });
959 }
960}