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