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 Vim::globals(cx).replayer.take();
149 return;
150 };
151 match action {
152 ReplayableAction::Action(action) => {
153 if should_replay(&*action) {
154 window.dispatch_action(action.boxed_clone(), cx);
155 cx.defer(move |cx| Vim::globals(cx).observe_action(action.boxed_clone()));
156 }
157 }
158 ReplayableAction::Insertion {
159 text,
160 utf16_range_to_replace,
161 } => {
162 let Some(Some(workspace)) = window.root::<Workspace>() else {
163 return;
164 };
165 let Some(editor) = workspace
166 .read(cx)
167 .active_item(cx)
168 .and_then(|item| item.act_as::<Editor>(cx))
169 else {
170 return;
171 };
172 editor.update(cx, |editor, cx| {
173 editor.replay_insert_event(&text, utf16_range_to_replace.clone(), window, cx)
174 })
175 }
176 }
177 window.defer(cx, move |window, cx| self.next(window, cx));
178 }
179}
180
181impl Vim {
182 pub(crate) fn record_register(
183 &mut self,
184 register: char,
185 window: &mut Window,
186 cx: &mut Context<Self>,
187 ) {
188 let globals = Vim::globals(cx);
189 globals.recording_register = Some(register);
190 globals.recordings.remove(®ister);
191 globals.ignore_current_insertion = true;
192 self.clear_operator(window, cx)
193 }
194
195 pub(crate) fn replay_register(
196 &mut self,
197 mut register: char,
198 window: &mut Window,
199 cx: &mut Context<Self>,
200 ) {
201 let mut count = Vim::take_count(cx).unwrap_or(1);
202 Vim::take_forced_motion(cx);
203 self.clear_operator(window, cx);
204
205 let globals = Vim::globals(cx);
206 if register == '@' {
207 let Some(last) = globals.last_replayed_register else {
208 return;
209 };
210 register = last;
211 }
212 let Some(actions) = globals.recordings.get(®ister) else {
213 return;
214 };
215
216 let mut repeated_actions = vec![];
217 while count > 0 {
218 repeated_actions.extend(actions.iter().cloned());
219 count -= 1
220 }
221
222 globals.last_replayed_register = Some(register);
223 let mut replayer = globals.replayer.get_or_insert_with(Replayer::new).clone();
224 replayer.replay(repeated_actions, window, cx);
225 }
226
227 pub(crate) fn repeat(
228 &mut self,
229 from_insert_mode: bool,
230 window: &mut Window,
231 cx: &mut Context<Self>,
232 ) {
233 if self.active_operator().is_some() {
234 Vim::update_globals(cx, |globals, _| {
235 globals.recording_actions.clear();
236 globals.recording_count = None;
237 globals.dot_recording = false;
238 globals.stop_recording_after_next_action = false;
239 });
240 self.clear_operator(window, cx);
241 return;
242 }
243
244 Vim::take_forced_motion(cx);
245 let count = Vim::take_count(cx);
246
247 let Some((mut actions, selection, mode)) = Vim::update_globals(cx, |globals, _| {
248 let actions = globals.recorded_actions.clone();
249 if actions.is_empty() {
250 return None;
251 }
252 if globals.replayer.is_none()
253 && let Some(recording_register) = globals.recording_register
254 {
255 globals
256 .recordings
257 .entry(recording_register)
258 .or_default()
259 .push(ReplayableAction::Action(Repeat.boxed_clone()));
260 }
261
262 let mut mode = None;
263 let selection = globals.recorded_selection.clone();
264 match selection {
265 RecordedSelection::SingleLine { .. } | RecordedSelection::Visual { .. } => {
266 globals.recorded_count = None;
267 mode = Some(Mode::Visual);
268 }
269 RecordedSelection::VisualLine { .. } => {
270 globals.recorded_count = None;
271 mode = Some(Mode::VisualLine)
272 }
273 RecordedSelection::VisualBlock { .. } => {
274 globals.recorded_count = None;
275 mode = Some(Mode::VisualBlock)
276 }
277 RecordedSelection::None => {
278 if let Some(count) = count {
279 globals.recorded_count = Some(count);
280 }
281 }
282 }
283
284 Some((actions, selection, mode))
285 }) else {
286 return;
287 };
288 if mode != Some(self.mode) {
289 if let Some(mode) = mode {
290 self.switch_mode(mode, false, window, cx)
291 }
292
293 match selection {
294 RecordedSelection::SingleLine { cols } => {
295 if cols > 1 {
296 self.visual_motion(Motion::Right, Some(cols as usize - 1), window, cx)
297 }
298 }
299 RecordedSelection::Visual { rows, cols } => {
300 self.visual_motion(
301 Motion::Down {
302 display_lines: false,
303 },
304 Some(rows as usize),
305 window,
306 cx,
307 );
308 self.visual_motion(
309 Motion::StartOfLine {
310 display_lines: false,
311 },
312 None,
313 window,
314 cx,
315 );
316 if cols > 1 {
317 self.visual_motion(Motion::Right, Some(cols as usize - 1), window, cx)
318 }
319 }
320 RecordedSelection::VisualBlock { rows, cols } => {
321 self.visual_motion(
322 Motion::Down {
323 display_lines: false,
324 },
325 Some(rows as usize),
326 window,
327 cx,
328 );
329 if cols > 1 {
330 self.visual_motion(Motion::Right, Some(cols as usize - 1), window, cx);
331 }
332 }
333 RecordedSelection::VisualLine { rows } => {
334 self.visual_motion(
335 Motion::Down {
336 display_lines: false,
337 },
338 Some(rows as usize),
339 window,
340 cx,
341 );
342 }
343 RecordedSelection::None => {}
344 }
345 }
346
347 // insert internally uses repeat to handle counts
348 // vim doesn't treat 3a1 as though you literally repeated a1
349 // 3 times, instead it inserts the content thrice at the insert position.
350 if let Some(to_repeat) = repeatable_insert(&actions[0]) {
351 if let Some(ReplayableAction::Action(action)) = actions.last()
352 && NormalBefore.partial_eq(&**action)
353 {
354 actions.pop();
355 }
356
357 let mut new_actions = actions.clone();
358 actions[0] = ReplayableAction::Action(to_repeat.boxed_clone());
359
360 let mut count = cx.global::<VimGlobals>().recorded_count.unwrap_or(1);
361
362 // if we came from insert mode we're just doing repetitions 2 onwards.
363 if from_insert_mode {
364 count -= 1;
365 new_actions[0] = actions[0].clone();
366 }
367
368 for _ in 1..count {
369 new_actions.append(actions.clone().as_mut());
370 }
371 new_actions.push(ReplayableAction::Action(NormalBefore.boxed_clone()));
372 actions = new_actions;
373 }
374
375 actions.push(ReplayableAction::Action(EndRepeat.boxed_clone()));
376
377 if self.temp_mode {
378 self.temp_mode = false;
379 actions.push(ReplayableAction::Action(InsertBefore.boxed_clone()));
380 }
381
382 let globals = Vim::globals(cx);
383 globals.dot_replaying = true;
384 let mut replayer = globals.replayer.get_or_insert_with(Replayer::new).clone();
385
386 replayer.replay(actions, window, cx);
387 }
388}
389
390#[cfg(test)]
391mod test {
392 use editor::test::editor_lsp_test_context::EditorLspTestContext;
393 use futures::StreamExt;
394 use indoc::indoc;
395
396 use gpui::EntityInputHandler;
397
398 use crate::{
399 state::Mode,
400 test::{NeovimBackedTestContext, VimTestContext},
401 };
402
403 #[gpui::test]
404 async fn test_dot_repeat(cx: &mut gpui::TestAppContext) {
405 let mut cx = NeovimBackedTestContext::new(cx).await;
406
407 // "o"
408 cx.set_shared_state("ˇhello").await;
409 cx.simulate_shared_keystrokes("o w o r l d escape").await;
410 cx.shared_state().await.assert_eq("hello\nworlˇd");
411 cx.simulate_shared_keystrokes(".").await;
412 cx.shared_state().await.assert_eq("hello\nworld\nworlˇd");
413
414 // "d"
415 cx.simulate_shared_keystrokes("^ d f o").await;
416 cx.simulate_shared_keystrokes("g g .").await;
417 cx.shared_state().await.assert_eq("ˇ\nworld\nrld");
418
419 // "p" (note that it pastes the current clipboard)
420 cx.simulate_shared_keystrokes("j y y p").await;
421 cx.simulate_shared_keystrokes("shift-g y y .").await;
422 cx.shared_state()
423 .await
424 .assert_eq("\nworld\nworld\nrld\nˇrld");
425
426 // "~" (note that counts apply to the action taken, not . itself)
427 cx.set_shared_state("ˇthe quick brown fox").await;
428 cx.simulate_shared_keystrokes("2 ~ .").await;
429 cx.set_shared_state("THE ˇquick brown fox").await;
430 cx.simulate_shared_keystrokes("3 .").await;
431 cx.set_shared_state("THE QUIˇck brown fox").await;
432 cx.run_until_parked();
433 cx.simulate_shared_keystrokes(".").await;
434 cx.shared_state().await.assert_eq("THE QUICK ˇbrown fox");
435 }
436
437 #[gpui::test]
438 async fn test_repeat_ime(cx: &mut gpui::TestAppContext) {
439 let mut cx = VimTestContext::new(cx, true).await;
440
441 cx.set_state("hˇllo", Mode::Normal);
442 cx.simulate_keystrokes("i");
443
444 // simulate brazilian input for ä.
445 cx.update_editor(|editor, window, cx| {
446 editor.replace_and_mark_text_in_range(None, "\"", Some(1..1), window, cx);
447 editor.replace_text_in_range(None, "ä", window, cx);
448 });
449 cx.simulate_keystrokes("escape");
450 cx.assert_state("hˇällo", Mode::Normal);
451 cx.simulate_keystrokes(".");
452 cx.assert_state("hˇäällo", Mode::Normal);
453 }
454
455 #[gpui::test]
456 async fn test_repeat_completion(cx: &mut gpui::TestAppContext) {
457 VimTestContext::init(cx);
458 let cx = EditorLspTestContext::new_rust(
459 lsp::ServerCapabilities {
460 completion_provider: Some(lsp::CompletionOptions {
461 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
462 resolve_provider: Some(true),
463 ..Default::default()
464 }),
465 ..Default::default()
466 },
467 cx,
468 )
469 .await;
470 let mut cx = VimTestContext::new_with_lsp(cx, true);
471
472 cx.set_state(
473 indoc! {"
474 onˇe
475 two
476 three
477 "},
478 Mode::Normal,
479 );
480
481 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(
482 move |_, params, _| async move {
483 let position = params.text_document_position.position;
484 Ok(Some(lsp::CompletionResponse::Array(vec![
485 lsp::CompletionItem {
486 label: "first".to_string(),
487 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
488 range: lsp::Range::new(position, position),
489 new_text: "first".to_string(),
490 })),
491 ..Default::default()
492 },
493 lsp::CompletionItem {
494 label: "second".to_string(),
495 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
496 range: lsp::Range::new(position, position),
497 new_text: "second".to_string(),
498 })),
499 ..Default::default()
500 },
501 ])))
502 },
503 );
504 cx.simulate_keystrokes("a .");
505 request.next().await;
506 cx.condition(|editor, _| editor.context_menu_visible())
507 .await;
508 cx.simulate_keystrokes("down enter ! escape");
509
510 cx.assert_state(
511 indoc! {"
512 one.secondˇ!
513 two
514 three
515 "},
516 Mode::Normal,
517 );
518 cx.simulate_keystrokes("j .");
519 cx.assert_state(
520 indoc! {"
521 one.second!
522 two.secondˇ!
523 three
524 "},
525 Mode::Normal,
526 );
527 }
528
529 #[gpui::test]
530 async fn test_repeat_completion_unicode_bug(cx: &mut gpui::TestAppContext) {
531 VimTestContext::init(cx);
532 let cx = EditorLspTestContext::new_rust(
533 lsp::ServerCapabilities {
534 completion_provider: Some(lsp::CompletionOptions {
535 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
536 resolve_provider: Some(true),
537 ..Default::default()
538 }),
539 ..Default::default()
540 },
541 cx,
542 )
543 .await;
544 let mut cx = VimTestContext::new_with_lsp(cx, true);
545
546 cx.set_state(
547 indoc! {"
548 ĩлˇк
549 ĩлк
550 "},
551 Mode::Normal,
552 );
553
554 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(
555 move |_, params, _| async move {
556 let position = params.text_document_position.position;
557 let mut to_the_left = position;
558 to_the_left.character -= 2;
559 Ok(Some(lsp::CompletionResponse::Array(vec![
560 lsp::CompletionItem {
561 label: "oops".to_string(),
562 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
563 range: lsp::Range::new(to_the_left, position),
564 new_text: "к!".to_string(),
565 })),
566 ..Default::default()
567 },
568 ])))
569 },
570 );
571 cx.simulate_keystrokes("i .");
572 request.next().await;
573 cx.condition(|editor, _| editor.context_menu_visible())
574 .await;
575 cx.simulate_keystrokes("enter escape");
576 cx.assert_state(
577 indoc! {"
578 ĩкˇ!к
579 ĩлк
580 "},
581 Mode::Normal,
582 );
583 }
584
585 #[gpui::test]
586 async fn test_repeat_visual(cx: &mut gpui::TestAppContext) {
587 let mut cx = NeovimBackedTestContext::new(cx).await;
588
589 // single-line (3 columns)
590 cx.set_shared_state(indoc! {
591 "ˇthe quick brown
592 fox jumps over
593 the lazy dog"
594 })
595 .await;
596 cx.simulate_shared_keystrokes("v i w s o escape").await;
597 cx.shared_state().await.assert_eq(indoc! {
598 "ˇo quick brown
599 fox jumps over
600 the lazy dog"
601 });
602 cx.simulate_shared_keystrokes("j w .").await;
603 cx.shared_state().await.assert_eq(indoc! {
604 "o quick brown
605 fox ˇops over
606 the lazy dog"
607 });
608 cx.simulate_shared_keystrokes("f r .").await;
609 cx.shared_state().await.assert_eq(indoc! {
610 "o quick brown
611 fox ops oveˇothe lazy dog"
612 });
613
614 // visual
615 cx.set_shared_state(indoc! {
616 "the ˇquick brown
617 fox jumps over
618 fox jumps over
619 fox jumps over
620 the lazy dog"
621 })
622 .await;
623 cx.simulate_shared_keystrokes("v j x").await;
624 cx.shared_state().await.assert_eq(indoc! {
625 "the ˇumps over
626 fox jumps over
627 fox jumps over
628 the lazy dog"
629 });
630 cx.simulate_shared_keystrokes(".").await;
631 cx.shared_state().await.assert_eq(indoc! {
632 "the ˇumps over
633 fox jumps over
634 the lazy dog"
635 });
636 cx.simulate_shared_keystrokes("w .").await;
637 cx.shared_state().await.assert_eq(indoc! {
638 "the umps ˇumps over
639 the lazy dog"
640 });
641 cx.simulate_shared_keystrokes("j .").await;
642 cx.shared_state().await.assert_eq(indoc! {
643 "the umps umps over
644 the ˇog"
645 });
646
647 // block mode (3 rows)
648 cx.set_shared_state(indoc! {
649 "ˇthe quick brown
650 fox jumps over
651 the lazy dog"
652 })
653 .await;
654 cx.simulate_shared_keystrokes("ctrl-v j j shift-i o escape")
655 .await;
656 cx.shared_state().await.assert_eq(indoc! {
657 "ˇothe quick brown
658 ofox jumps over
659 othe lazy dog"
660 });
661 cx.simulate_shared_keystrokes("j 4 l .").await;
662 cx.shared_state().await.assert_eq(indoc! {
663 "othe quick brown
664 ofoxˇo jumps over
665 otheo lazy dog"
666 });
667
668 // line mode
669 cx.set_shared_state(indoc! {
670 "ˇthe quick brown
671 fox jumps over
672 the lazy dog"
673 })
674 .await;
675 cx.simulate_shared_keystrokes("shift-v shift-r o escape")
676 .await;
677 cx.shared_state().await.assert_eq(indoc! {
678 "ˇo
679 fox jumps over
680 the lazy dog"
681 });
682 cx.simulate_shared_keystrokes("j .").await;
683 cx.shared_state().await.assert_eq(indoc! {
684 "o
685 ˇo
686 the lazy dog"
687 });
688 }
689
690 #[gpui::test]
691 async fn test_repeat_motion_counts(cx: &mut gpui::TestAppContext) {
692 let mut cx = NeovimBackedTestContext::new(cx).await;
693
694 cx.set_shared_state(indoc! {
695 "ˇthe quick brown
696 fox jumps over
697 the lazy dog"
698 })
699 .await;
700 cx.simulate_shared_keystrokes("3 d 3 l").await;
701 cx.shared_state().await.assert_eq(indoc! {
702 "ˇ brown
703 fox jumps over
704 the lazy dog"
705 });
706 cx.simulate_shared_keystrokes("j .").await;
707 cx.shared_state().await.assert_eq(indoc! {
708 " brown
709 ˇ over
710 the lazy dog"
711 });
712 cx.simulate_shared_keystrokes("j 2 .").await;
713 cx.shared_state().await.assert_eq(indoc! {
714 " brown
715 over
716 ˇe lazy dog"
717 });
718 }
719
720 #[gpui::test]
721 async fn test_record_interrupted(cx: &mut gpui::TestAppContext) {
722 let mut cx = VimTestContext::new(cx, true).await;
723
724 cx.set_state("ˇhello\n", Mode::Normal);
725 cx.simulate_keystrokes("4 i j cmd-shift-p escape");
726 cx.simulate_keystrokes("escape");
727 cx.assert_state("ˇjhello\n", Mode::Normal);
728 }
729
730 #[gpui::test]
731 async fn test_repeat_over_blur(cx: &mut gpui::TestAppContext) {
732 let mut cx = NeovimBackedTestContext::new(cx).await;
733
734 cx.set_shared_state("ˇhello hello hello\n").await;
735 cx.simulate_shared_keystrokes("c f o x escape").await;
736 cx.shared_state().await.assert_eq("ˇx hello hello\n");
737 cx.simulate_shared_keystrokes(": escape").await;
738 cx.simulate_shared_keystrokes(".").await;
739 cx.shared_state().await.assert_eq("ˇx hello\n");
740 }
741
742 #[gpui::test]
743 async fn test_undo_repeated_insert(cx: &mut gpui::TestAppContext) {
744 let mut cx = NeovimBackedTestContext::new(cx).await;
745
746 cx.set_shared_state("hellˇo").await;
747 cx.simulate_shared_keystrokes("3 a . escape").await;
748 cx.shared_state().await.assert_eq("hello..ˇ.");
749 cx.simulate_shared_keystrokes("u").await;
750 cx.shared_state().await.assert_eq("hellˇo");
751 }
752
753 #[gpui::test]
754 async fn test_record_replay(cx: &mut gpui::TestAppContext) {
755 let mut cx = NeovimBackedTestContext::new(cx).await;
756
757 cx.set_shared_state("ˇhello world").await;
758 cx.simulate_shared_keystrokes("q w c w j escape q").await;
759 cx.shared_state().await.assert_eq("ˇj world");
760 cx.simulate_shared_keystrokes("2 l @ w").await;
761 cx.shared_state().await.assert_eq("j ˇj");
762 }
763
764 #[gpui::test]
765 async fn test_record_replay_count(cx: &mut gpui::TestAppContext) {
766 let mut cx = NeovimBackedTestContext::new(cx).await;
767
768 cx.set_shared_state("ˇhello world!!").await;
769 cx.simulate_shared_keystrokes("q a v 3 l s 0 escape l q")
770 .await;
771 cx.shared_state().await.assert_eq("0ˇo world!!");
772 cx.simulate_shared_keystrokes("2 @ a").await;
773 cx.shared_state().await.assert_eq("000ˇ!");
774 }
775
776 #[gpui::test]
777 async fn test_record_replay_dot(cx: &mut gpui::TestAppContext) {
778 let mut cx = NeovimBackedTestContext::new(cx).await;
779
780 cx.set_shared_state("ˇhello world").await;
781 cx.simulate_shared_keystrokes("q a r a l r b l q").await;
782 cx.shared_state().await.assert_eq("abˇllo world");
783 cx.simulate_shared_keystrokes(".").await;
784 cx.shared_state().await.assert_eq("abˇblo world");
785 cx.simulate_shared_keystrokes("shift-q").await;
786 cx.shared_state().await.assert_eq("ababˇo world");
787 cx.simulate_shared_keystrokes(".").await;
788 cx.shared_state().await.assert_eq("ababˇb world");
789 }
790
791 #[gpui::test]
792 async fn test_record_replay_of_dot(cx: &mut gpui::TestAppContext) {
793 let mut cx = NeovimBackedTestContext::new(cx).await;
794
795 cx.set_shared_state("ˇhello world").await;
796 cx.simulate_shared_keystrokes("r o q w . q").await;
797 cx.shared_state().await.assert_eq("ˇoello world");
798 cx.simulate_shared_keystrokes("d l").await;
799 cx.shared_state().await.assert_eq("ˇello world");
800 cx.simulate_shared_keystrokes("@ w").await;
801 cx.shared_state().await.assert_eq("ˇllo world");
802 }
803
804 #[gpui::test]
805 async fn test_record_replay_interleaved(cx: &mut gpui::TestAppContext) {
806 let mut cx = NeovimBackedTestContext::new(cx).await;
807
808 cx.set_shared_state("ˇhello world").await;
809 cx.simulate_shared_keystrokes("q z r a l q").await;
810 cx.shared_state().await.assert_eq("aˇello world");
811 cx.simulate_shared_keystrokes("q b @ z @ z q").await;
812 cx.shared_state().await.assert_eq("aaaˇlo world");
813 cx.simulate_shared_keystrokes("@ @").await;
814 cx.shared_state().await.assert_eq("aaaaˇo world");
815 cx.simulate_shared_keystrokes("@ b").await;
816 cx.shared_state().await.assert_eq("aaaaaaˇworld");
817 cx.simulate_shared_keystrokes("@ @").await;
818 cx.shared_state().await.assert_eq("aaaaaaaˇorld");
819 cx.simulate_shared_keystrokes("q z r b l q").await;
820 cx.shared_state().await.assert_eq("aaaaaaabˇrld");
821 cx.simulate_shared_keystrokes("@ b").await;
822 cx.shared_state().await.assert_eq("aaaaaaabbbˇd");
823 }
824
825 #[gpui::test]
826 async fn test_repeat_clear(cx: &mut gpui::TestAppContext) {
827 let mut cx = VimTestContext::new(cx, true).await;
828
829 // Check that, when repeat is preceded by something other than a number,
830 // the current operator is cleared, in order to prevent infinite loops.
831 cx.set_state("ˇhello world", Mode::Normal);
832 cx.simulate_keystrokes("d .");
833 assert_eq!(cx.active_operator(), None);
834 }
835
836 #[gpui::test]
837 async fn test_repeat_clear_repeat(cx: &mut gpui::TestAppContext) {
838 let mut cx = NeovimBackedTestContext::new(cx).await;
839
840 cx.set_shared_state(indoc! {
841 "ˇthe quick brown
842 fox jumps over
843 the lazy dog"
844 })
845 .await;
846 cx.simulate_shared_keystrokes("d d").await;
847 cx.shared_state().await.assert_eq(indoc! {
848 "ˇfox jumps over
849 the lazy dog"
850 });
851 cx.simulate_shared_keystrokes("d . .").await;
852 cx.shared_state().await.assert_eq(indoc! {
853 "ˇthe lazy dog"
854 });
855 }
856
857 #[gpui::test]
858 async fn test_repeat_clear_count(cx: &mut gpui::TestAppContext) {
859 let mut cx = NeovimBackedTestContext::new(cx).await;
860
861 cx.set_shared_state(indoc! {
862 "ˇthe quick brown
863 fox jumps over
864 the lazy dog"
865 })
866 .await;
867 cx.simulate_shared_keystrokes("d d").await;
868 cx.shared_state().await.assert_eq(indoc! {
869 "ˇfox jumps over
870 the lazy dog"
871 });
872 cx.simulate_shared_keystrokes("2 d .").await;
873 cx.shared_state().await.assert_eq(indoc! {
874 "ˇfox jumps over
875 the lazy dog"
876 });
877 cx.simulate_shared_keystrokes(".").await;
878 cx.shared_state().await.assert_eq(indoc! {
879 "ˇthe lazy dog"
880 });
881
882 cx.set_shared_state(indoc! {
883 "ˇthe quick brown
884 fox jumps over
885 the lazy dog
886 the quick brown
887 fox jumps over
888 the lazy dog"
889 })
890 .await;
891 cx.simulate_shared_keystrokes("2 d d").await;
892 cx.shared_state().await.assert_eq(indoc! {
893 "ˇthe lazy dog
894 the quick brown
895 fox jumps over
896 the lazy dog"
897 });
898 cx.simulate_shared_keystrokes("5 d .").await;
899 cx.shared_state().await.assert_eq(indoc! {
900 "ˇthe lazy dog
901 the quick brown
902 fox jumps over
903 the lazy dog"
904 });
905 cx.simulate_shared_keystrokes(".").await;
906 cx.shared_state().await.assert_eq(indoc! {
907 "ˇfox jumps over
908 the lazy dog"
909 });
910 }
911}