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