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