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