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