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