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