Skip to repository content1208 lines · 40.9 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T04:34:42.555Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
repeat.rs
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(workspace) = Workspace::for_window(window, cx) 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 // The `globals.dot_replaying = false` is a fail-safe to ensure that
149 // this value is always reset, in the case that the focus is moved
150 // away from the editor, effectively preventing the `EndRepeat`
151 // action from being handled.
152 let globals = Vim::globals(cx);
153 globals.replayer.take();
154 globals.dot_replaying = false;
155 return;
156 };
157 match action {
158 ReplayableAction::Action(action) => {
159 if should_replay(&*action) {
160 window.dispatch_action(action.boxed_clone(), cx);
161 cx.defer(move |cx| Vim::globals(cx).observe_action(action.boxed_clone()));
162 }
163 }
164 ReplayableAction::Insertion {
165 text,
166 utf16_range_to_replace,
167 } => {
168 let Some(workspace) = Workspace::for_window(window, cx) else {
169 return;
170 };
171 let Some(editor) = workspace
172 .read(cx)
173 .active_item(cx)
174 .and_then(|item| item.act_as::<Editor>(cx))
175 else {
176 return;
177 };
178 editor.update(cx, |editor, cx| {
179 editor.replay_insert_event(&text, utf16_range_to_replace.clone(), window, cx)
180 })
181 }
182 }
183 window.defer(cx, move |window, cx| self.next(window, cx));
184 }
185}
186
187impl Vim {
188 pub(crate) fn record_register(
189 &mut self,
190 register: char,
191 window: &mut Window,
192 cx: &mut Context<Self>,
193 ) {
194 let globals = Vim::globals(cx);
195 globals.recording_register = Some(register);
196 globals.recordings.remove(®ister);
197 globals.ignore_current_insertion = true;
198 self.clear_operator(window, cx)
199 }
200
201 pub(crate) fn replay_register(
202 &mut self,
203 mut register: char,
204 window: &mut Window,
205 cx: &mut Context<Self>,
206 ) {
207 let mut count = Vim::take_count(cx).unwrap_or(1);
208 Vim::take_forced_motion(cx);
209 self.clear_operator(window, cx);
210
211 let globals = Vim::globals(cx);
212 if register == '@' {
213 let Some(last) = globals.last_replayed_register else {
214 return;
215 };
216 register = last;
217 }
218 let Some(actions) = globals.recordings.get(®ister) else {
219 return;
220 };
221
222 let mut repeated_actions = vec![];
223 while count > 0 {
224 repeated_actions.extend(actions.iter().cloned());
225 count -= 1
226 }
227
228 globals.last_replayed_register = Some(register);
229 let mut replayer = globals.replayer.get_or_insert_with(Replayer::new).clone();
230 replayer.replay(repeated_actions, window, cx);
231 }
232
233 pub(crate) fn repeat(
234 &mut self,
235 from_insert_mode: bool,
236 window: &mut Window,
237 cx: &mut Context<Self>,
238 ) {
239 if self.active_operator().is_some() {
240 Vim::update_globals(cx, |globals, _| {
241 globals.recording_actions.clear();
242 globals.recording_count = None;
243 globals.dot_recording = false;
244 globals.stop_recording_after_next_action = false;
245 });
246 self.clear_operator(window, cx);
247 return;
248 }
249
250 Vim::take_forced_motion(cx);
251 let count = Vim::take_count(cx);
252
253 let Some((mut actions, selection, mode)) = Vim::update_globals(cx, |globals, _| {
254 let actions = globals.recorded_actions.clone();
255 if actions.is_empty() {
256 return None;
257 }
258 if globals.replayer.is_none()
259 && let Some(recording_register) = globals.recording_register
260 {
261 globals
262 .recordings
263 .entry(recording_register)
264 .or_default()
265 .push(ReplayableAction::Action(Repeat.boxed_clone()));
266 }
267
268 let mut mode = None;
269 let selection = globals.recorded_selection.clone();
270 match selection {
271 RecordedSelection::SingleLine { .. } | RecordedSelection::Visual { .. } => {
272 globals.recorded_count = None;
273 mode = Some(Mode::Visual);
274 }
275 RecordedSelection::VisualLine { .. } => {
276 globals.recorded_count = None;
277 mode = Some(Mode::VisualLine)
278 }
279 RecordedSelection::VisualBlock { .. } => {
280 globals.recorded_count = None;
281 mode = Some(Mode::VisualBlock)
282 }
283 RecordedSelection::None => {
284 if let Some(count) = count {
285 globals.recorded_count = Some(count);
286 }
287 }
288 }
289
290 Some((actions, selection, mode))
291 }) else {
292 return;
293 };
294
295 // Dot repeat always uses the recorded register, ignoring any "X
296 // override, as the register is an inherent part of the recorded action.
297 // For numbered registers, Neovim increments on each dot repeat so after
298 // using `"1p`, using `.` will equate to `"2p", the next `.` to `"3p`,
299 // etc..
300 let recorded_register = cx.global::<VimGlobals>().recorded_register_for_dot;
301 let next_register = recorded_register
302 .filter(|c| matches!(c, '1'..='9'))
303 .map(|c| ((c as u8 + 1).min(b'9')) as char);
304
305 self.selected_register = next_register.or(recorded_register);
306 if let Some(next_register) = next_register {
307 Vim::update_globals(cx, |globals, _| {
308 globals.recorded_register_for_dot = Some(next_register)
309 })
310 };
311
312 if mode != Some(self.mode) {
313 if let Some(mode) = mode {
314 self.switch_mode(mode, false, window, cx)
315 }
316
317 match selection {
318 RecordedSelection::SingleLine { cols } => {
319 if cols > 1 {
320 self.visual_motion(Motion::Right, Some(cols as usize - 1), window, cx)
321 }
322 }
323 RecordedSelection::Visual { rows, cols } => {
324 self.visual_motion(
325 Motion::Down {
326 display_lines: false,
327 },
328 Some(rows as usize),
329 window,
330 cx,
331 );
332 self.visual_motion(
333 Motion::StartOfLine {
334 display_lines: false,
335 },
336 None,
337 window,
338 cx,
339 );
340 if cols > 1 {
341 self.visual_motion(Motion::Right, Some(cols as usize - 1), window, cx)
342 }
343 }
344 RecordedSelection::VisualBlock { rows, cols } => {
345 self.visual_motion(
346 Motion::Down {
347 display_lines: false,
348 },
349 Some(rows as usize),
350 window,
351 cx,
352 );
353 if cols > 1 {
354 self.visual_motion(Motion::Right, Some(cols as usize - 1), window, cx);
355 }
356 }
357 RecordedSelection::VisualLine { rows } => {
358 self.visual_motion(
359 Motion::Down {
360 display_lines: false,
361 },
362 Some(rows as usize),
363 window,
364 cx,
365 );
366 }
367 RecordedSelection::None => {}
368 }
369 }
370
371 // insert internally uses repeat to handle counts
372 // vim doesn't treat 3a1 as though you literally repeated a1
373 // 3 times, instead it inserts the content thrice at the insert position.
374 if let Some(to_repeat) = repeatable_insert(&actions[0]) {
375 if let Some(ReplayableAction::Action(action)) = actions.last()
376 && NormalBefore.partial_eq(&**action)
377 {
378 actions.pop();
379 }
380
381 let mut new_actions = actions.clone();
382 actions[0] = ReplayableAction::Action(to_repeat.boxed_clone());
383
384 let mut count = cx.global::<VimGlobals>().recorded_count.unwrap_or(1);
385
386 // if we came from insert mode we're just doing repetitions 2 onwards.
387 if from_insert_mode {
388 count -= 1;
389 new_actions[0] = actions[0].clone();
390 }
391
392 for _ in 1..count {
393 new_actions.append(actions.clone().as_mut());
394 }
395 new_actions.push(ReplayableAction::Action(NormalBefore.boxed_clone()));
396 actions = new_actions;
397 }
398
399 actions.push(ReplayableAction::Action(EndRepeat.boxed_clone()));
400
401 if self.temp_mode {
402 self.temp_mode = false;
403 actions.push(ReplayableAction::Action(InsertBefore.boxed_clone()));
404 }
405
406 let globals = Vim::globals(cx);
407 globals.dot_replaying = true;
408 let mut replayer = globals.replayer.get_or_insert_with(Replayer::new).clone();
409
410 replayer.replay(actions, window, cx);
411 }
412}
413
414#[cfg(test)]
415mod test {
416 use editor::test::editor_lsp_test_context::EditorLspTestContext;
417 use futures::StreamExt;
418 use indoc::indoc;
419
420 use gpui::EntityInputHandler;
421
422 use crate::{
423 VimGlobals,
424 state::Mode,
425 test::{NeovimBackedTestContext, VimTestContext},
426 };
427
428 #[gpui::test]
429 async fn test_dot_repeat(cx: &mut gpui::TestAppContext) {
430 let mut cx = NeovimBackedTestContext::new(cx).await;
431
432 // "o"
433 cx.set_shared_state("ˇhello").await;
434 cx.simulate_shared_keystrokes("o w o r l d escape").await;
435 cx.shared_state().await.assert_eq("hello\nworlˇd");
436 cx.simulate_shared_keystrokes(".").await;
437 cx.shared_state().await.assert_eq("hello\nworld\nworlˇd");
438
439 // "d"
440 cx.simulate_shared_keystrokes("^ d f o").await;
441 cx.simulate_shared_keystrokes("g g .").await;
442 cx.shared_state().await.assert_eq("ˇ\nworld\nrld");
443
444 // "p" (note that it pastes the current clipboard)
445 cx.simulate_shared_keystrokes("j y y p").await;
446 cx.simulate_shared_keystrokes("shift-g y y .").await;
447 cx.shared_state()
448 .await
449 .assert_eq("\nworld\nworld\nrld\nˇrld");
450
451 // "~" (note that counts apply to the action taken, not . itself)
452 cx.set_shared_state("ˇthe quick brown fox").await;
453 cx.simulate_shared_keystrokes("2 ~ .").await;
454 cx.set_shared_state("THE ˇquick brown fox").await;
455 cx.simulate_shared_keystrokes("3 .").await;
456 cx.set_shared_state("THE QUIˇck brown fox").await;
457 cx.run_until_parked();
458 cx.simulate_shared_keystrokes(".").await;
459 cx.shared_state().await.assert_eq("THE QUICK ˇbrown fox");
460
461 // "q l" (note after macro should be used last change made by macro)
462 cx.set_shared_state("ˇ").await;
463 cx.simulate_shared_keystrokes("q l shift-o h e l l o space w o r l d escape q")
464 .await;
465 cx.simulate_shared_keystrokes("@ l").await;
466 cx.shared_state()
467 .await
468 .assert_eq("hello worlˇd\nhello world\n");
469 cx.simulate_shared_keystrokes(".").await;
470 cx.shared_state()
471 .await
472 .assert_eq("hello worlˇd\nhello world\nhello world\n");
473 }
474
475 #[gpui::test]
476 async fn test_dot_repeat_after_macro_change_motion(cx: &mut gpui::TestAppContext) {
477 let mut cx = VimTestContext::new(cx, true).await;
478
479 cx.set_state("ˇfoo foo", Mode::Normal);
480 cx.simulate_keystrokes("q l c f o x escape q");
481 cx.assert_state("ˇxo foo", Mode::Normal);
482
483 cx.simulate_keystrokes("w @ l");
484 cx.assert_state("xo ˇxo", Mode::Normal);
485
486 cx.simulate_keystrokes(".");
487 cx.assert_state("xo ˇx", Mode::Normal);
488 }
489
490 #[gpui::test]
491 async fn test_dot_repeat_registers_paste(cx: &mut gpui::TestAppContext) {
492 let mut cx = NeovimBackedTestContext::new(cx).await;
493
494 // basic paste repeat uses the unnamed register
495 cx.set_shared_state("ˇhello\n").await;
496 cx.simulate_shared_keystrokes("y y p").await;
497 cx.shared_state().await.assert_eq("hello\nˇhello\n");
498 cx.simulate_shared_keystrokes(".").await;
499 cx.shared_state().await.assert_eq("hello\nhello\nˇhello\n");
500
501 // "_ (blackhole) is recorded and replayed, so the pasted text is still
502 // the original yanked line.
503 cx.set_shared_state(indoc! {"
504 ˇone
505 two
506 three
507 four
508 "})
509 .await;
510 cx.simulate_shared_keystrokes("y y j \" _ d d . p").await;
511 cx.shared_state().await.assert_eq(indoc! {"
512 one
513 four
514 ˇone
515 "});
516
517 // the recorded register is replayed, not whatever is in the unnamed register
518 cx.set_shared_state(indoc! {"
519 ˇone
520 two
521 "})
522 .await;
523 cx.simulate_shared_keystrokes("y y j \" a y y \" a p .")
524 .await;
525 cx.shared_state().await.assert_eq(indoc! {"
526 one
527 two
528 two
529 ˇtwo
530 "});
531
532 // `"X.` ignores the override and always uses the recorded register.
533 // Both `dd` calls go into register `a`, so register `b` is empty and
534 // `"bp` pastes nothing.
535 cx.set_shared_state(indoc! {"
536 ˇone
537 two
538 three
539 "})
540 .await;
541 cx.simulate_shared_keystrokes("\" a d d \" b .").await;
542 cx.shared_state().await.assert_eq(indoc! {"
543 ˇthree
544 "});
545 cx.simulate_shared_keystrokes("\" a p \" b p").await;
546 cx.shared_state().await.assert_eq(indoc! {"
547 three
548 ˇtwo
549 "});
550
551 // numbered registers cycle on each dot repeat: "1p . . uses registers 2, 3, …
552 // Since the cycling behavior caps at register 9, the first line to be
553 // deleted `1`, is no longer in any of the registers.
554 cx.set_shared_state(indoc! {"
555 ˇone
556 two
557 three
558 four
559 five
560 six
561 seven
562 eight
563 nine
564 ten
565 "})
566 .await;
567 cx.simulate_shared_keystrokes("d d . . . . . . . . .").await;
568 cx.shared_state().await.assert_eq(indoc! {"ˇ"});
569 cx.simulate_shared_keystrokes("\" 1 p . . . . . . . . .")
570 .await;
571 cx.shared_state().await.assert_eq(indoc! {"
572
573 ten
574 nine
575 eight
576 seven
577 six
578 five
579 four
580 three
581 two
582 ˇtwo"});
583
584 // unnamed register repeat: dd records None, so . pastes the same
585 // deleted text
586 cx.set_shared_state(indoc! {"
587 ˇone
588 two
589 three
590 "})
591 .await;
592 cx.simulate_shared_keystrokes("d d p .").await;
593 cx.shared_state().await.assert_eq(indoc! {"
594 two
595 one
596 ˇone
597 three
598 "});
599
600 // After `"1p` cycles to `2`, using `"ap` resets recorded_register to `a`,
601 // so the next `.` uses `a` and not 3.
602 cx.set_shared_state(indoc! {"
603 one
604 two
605 ˇthree
606 "})
607 .await;
608 cx.simulate_shared_keystrokes("\" 2 y y k k \" a y y j \" 1 y y k \" 1 p . \" a p .")
609 .await;
610 cx.shared_state().await.assert_eq(indoc! {"
611 one
612 two
613 three
614 one
615 ˇone
616 two
617 three
618 "});
619 }
620
621 // This needs to be a separate test from `test_dot_repeat_registers_paste`
622 // as Neovim doesn't have support for using registers in replace operations
623 // by default.
624 #[gpui::test]
625 async fn test_dot_repeat_registers_replace(cx: &mut gpui::TestAppContext) {
626 let mut cx = VimTestContext::new(cx, true).await;
627
628 cx.set_state(
629 indoc! {"
630 line ˇone
631 line two
632 line three
633 "},
634 Mode::Normal,
635 );
636
637 // 1. Yank `one` into register `a`
638 // 2. Move down and yank `two` into the default register
639 // 3. Replace `two` with the contents of register `a`
640 cx.simulate_keystrokes("\" a y w j y w \" a g R w");
641 cx.assert_state(
642 indoc! {"
643 line one
644 line onˇe
645 line three
646 "},
647 Mode::Normal,
648 );
649
650 // 1. Move down to `three`
651 // 2. Repeat the replace operation
652 cx.simulate_keystrokes("j .");
653 cx.assert_state(
654 indoc! {"
655 line one
656 line one
657 line onˇe
658 "},
659 Mode::Normal,
660 );
661
662 // Similar test, but this time using numbered registers, as those should
663 // automatically increase on successive uses of `.` .
664 cx.set_state(
665 indoc! {"
666 line ˇone
667 line two
668 line three
669 line four
670 "},
671 Mode::Normal,
672 );
673
674 // 1. Yank `one` into register `1`
675 // 2. Yank `two` into register `2`
676 // 3. Move down and yank `three` into the default register
677 // 4. Replace `three` with the contents of register `1`
678 // 5. Move down and repeat
679 cx.simulate_keystrokes("\" 1 y w j \" 2 y w j y w \" 1 g R w j .");
680 cx.assert_state(
681 indoc! {"
682 line one
683 line two
684 line one
685 line twˇo
686 "},
687 Mode::Normal,
688 );
689 }
690
691 #[gpui::test]
692 async fn test_repeat_ime(cx: &mut gpui::TestAppContext) {
693 let mut cx = VimTestContext::new(cx, true).await;
694
695 cx.set_state("hˇllo", Mode::Normal);
696 cx.simulate_keystrokes("i");
697
698 // simulate brazilian input for ä.
699 cx.update_editor(|editor, window, cx| {
700 editor.replace_and_mark_text_in_range(None, "\"", Some(1..1), window, cx);
701 editor.replace_text_in_range(None, "ä", window, cx);
702 });
703 cx.simulate_keystrokes("escape");
704 cx.assert_state("hˇällo", Mode::Normal);
705 cx.simulate_keystrokes(".");
706 cx.assert_state("hˇäällo", Mode::Normal);
707 }
708
709 #[gpui::test]
710 async fn test_repeat_completion(cx: &mut gpui::TestAppContext) {
711 VimTestContext::init(cx);
712 let cx = EditorLspTestContext::new_rust(
713 lsp::ServerCapabilities {
714 completion_provider: Some(lsp::CompletionOptions {
715 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
716 resolve_provider: Some(true),
717 ..Default::default()
718 }),
719 ..Default::default()
720 },
721 cx,
722 )
723 .await;
724 let mut cx = VimTestContext::new_with_lsp(cx, true);
725
726 cx.set_state(
727 indoc! {"
728 onˇe
729 two
730 three
731 "},
732 Mode::Normal,
733 );
734
735 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(
736 move |_, params, _| async move {
737 let position = params.text_document_position.position;
738 Ok(Some(lsp::CompletionResponse::Array(vec![
739 lsp::CompletionItem {
740 label: "first".to_string(),
741 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
742 range: lsp::Range::new(position, position),
743 new_text: "first".to_string(),
744 })),
745 ..Default::default()
746 },
747 lsp::CompletionItem {
748 label: "second".to_string(),
749 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
750 range: lsp::Range::new(position, position),
751 new_text: "second".to_string(),
752 })),
753 ..Default::default()
754 },
755 ])))
756 },
757 );
758 cx.simulate_keystrokes("a .");
759 request.next().await;
760 cx.condition(|editor, _| editor.context_menu_visible())
761 .await;
762 cx.simulate_keystrokes("down enter ! escape");
763
764 cx.assert_state(
765 indoc! {"
766 one.secondˇ!
767 two
768 three
769 "},
770 Mode::Normal,
771 );
772 cx.simulate_keystrokes("j .");
773 cx.assert_state(
774 indoc! {"
775 one.second!
776 two.secondˇ!
777 three
778 "},
779 Mode::Normal,
780 );
781 }
782
783 #[gpui::test]
784 async fn test_repeat_completion_unicode_bug(cx: &mut gpui::TestAppContext) {
785 VimTestContext::init(cx);
786 let cx = EditorLspTestContext::new_rust(
787 lsp::ServerCapabilities {
788 completion_provider: Some(lsp::CompletionOptions {
789 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
790 resolve_provider: Some(true),
791 ..Default::default()
792 }),
793 ..Default::default()
794 },
795 cx,
796 )
797 .await;
798 let mut cx = VimTestContext::new_with_lsp(cx, true);
799
800 cx.set_state(
801 indoc! {"
802 ĩлˇк
803 ĩлк
804 "},
805 Mode::Normal,
806 );
807
808 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(
809 move |_, params, _| async move {
810 let position = params.text_document_position.position;
811 let mut to_the_left = position;
812 to_the_left.character -= 2;
813 Ok(Some(lsp::CompletionResponse::Array(vec![
814 lsp::CompletionItem {
815 label: "oops".to_string(),
816 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
817 range: lsp::Range::new(to_the_left, position),
818 new_text: "к!".to_string(),
819 })),
820 ..Default::default()
821 },
822 ])))
823 },
824 );
825 cx.simulate_keystrokes("i .");
826 request.next().await;
827 cx.condition(|editor, _| editor.context_menu_visible())
828 .await;
829 cx.simulate_keystrokes("enter escape");
830 cx.assert_state(
831 indoc! {"
832 ĩкˇ!к
833 ĩлк
834 "},
835 Mode::Normal,
836 );
837 }
838
839 #[gpui::test]
840 async fn test_repeat_visual(cx: &mut gpui::TestAppContext) {
841 let mut cx = NeovimBackedTestContext::new(cx).await;
842
843 // single-line (3 columns)
844 cx.set_shared_state(indoc! {
845 "ˇthe quick brown
846 fox jumps over
847 the lazy dog"
848 })
849 .await;
850 cx.simulate_shared_keystrokes("v i w s o escape").await;
851 cx.shared_state().await.assert_eq(indoc! {
852 "ˇo quick brown
853 fox jumps over
854 the lazy dog"
855 });
856 cx.simulate_shared_keystrokes("j w .").await;
857 cx.shared_state().await.assert_eq(indoc! {
858 "o quick brown
859 fox ˇops over
860 the lazy dog"
861 });
862 cx.simulate_shared_keystrokes("f r .").await;
863 cx.shared_state().await.assert_eq(indoc! {
864 "o quick brown
865 fox ops oveˇothe lazy dog"
866 });
867
868 // visual
869 cx.set_shared_state(indoc! {
870 "the ˇquick brown
871 fox jumps over
872 fox jumps over
873 fox jumps over
874 the lazy dog"
875 })
876 .await;
877 cx.simulate_shared_keystrokes("v j x").await;
878 cx.shared_state().await.assert_eq(indoc! {
879 "the ˇumps over
880 fox jumps over
881 fox jumps over
882 the lazy dog"
883 });
884 cx.simulate_shared_keystrokes(".").await;
885 cx.shared_state().await.assert_eq(indoc! {
886 "the ˇumps over
887 fox jumps over
888 the lazy dog"
889 });
890 cx.simulate_shared_keystrokes("w .").await;
891 cx.shared_state().await.assert_eq(indoc! {
892 "the umps ˇumps over
893 the lazy dog"
894 });
895 cx.simulate_shared_keystrokes("j .").await;
896 cx.shared_state().await.assert_eq(indoc! {
897 "the umps umps over
898 the ˇog"
899 });
900
901 // block mode (3 rows)
902 cx.set_shared_state(indoc! {
903 "ˇthe quick brown
904 fox jumps over
905 the lazy dog"
906 })
907 .await;
908 cx.simulate_shared_keystrokes("ctrl-v j j shift-i o escape")
909 .await;
910 cx.shared_state().await.assert_eq(indoc! {
911 "ˇothe quick brown
912 ofox jumps over
913 othe lazy dog"
914 });
915 cx.simulate_shared_keystrokes("j 4 l .").await;
916 cx.shared_state().await.assert_eq(indoc! {
917 "othe quick brown
918 ofoxˇo jumps over
919 otheo lazy dog"
920 });
921
922 // line mode
923 cx.set_shared_state(indoc! {
924 "ˇthe quick brown
925 fox jumps over
926 the lazy dog"
927 })
928 .await;
929 cx.simulate_shared_keystrokes("shift-v shift-r o escape")
930 .await;
931 cx.shared_state().await.assert_eq(indoc! {
932 "ˇo
933 fox jumps over
934 the lazy dog"
935 });
936 cx.simulate_shared_keystrokes("j .").await;
937 cx.shared_state().await.assert_eq(indoc! {
938 "o
939 ˇo
940 the lazy dog"
941 });
942 }
943
944 #[gpui::test]
945 async fn test_repeat_motion_counts(cx: &mut gpui::TestAppContext) {
946 let mut cx = NeovimBackedTestContext::new(cx).await;
947
948 cx.set_shared_state(indoc! {
949 "ˇthe quick brown
950 fox jumps over
951 the lazy dog"
952 })
953 .await;
954 cx.simulate_shared_keystrokes("3 d 3 l").await;
955 cx.shared_state().await.assert_eq(indoc! {
956 "ˇ brown
957 fox jumps over
958 the lazy dog"
959 });
960 cx.simulate_shared_keystrokes("j .").await;
961 cx.shared_state().await.assert_eq(indoc! {
962 " brown
963 ˇ over
964 the lazy dog"
965 });
966 cx.simulate_shared_keystrokes("j 2 .").await;
967 cx.shared_state().await.assert_eq(indoc! {
968 " brown
969 over
970 ˇe lazy dog"
971 });
972 }
973
974 #[gpui::test]
975 async fn test_record_interrupted(cx: &mut gpui::TestAppContext) {
976 let mut cx = VimTestContext::new(cx, true).await;
977
978 cx.set_state("ˇhello\n", Mode::Normal);
979 cx.simulate_keystrokes("4 i j cmd-shift-p escape");
980 cx.simulate_keystrokes("escape");
981 cx.assert_state("ˇjhello\n", Mode::Normal);
982 }
983
984 #[gpui::test]
985 async fn test_repeat_over_blur(cx: &mut gpui::TestAppContext) {
986 let mut cx = NeovimBackedTestContext::new(cx).await;
987
988 cx.set_shared_state("ˇhello hello hello\n").await;
989 cx.simulate_shared_keystrokes("c f o x escape").await;
990 cx.shared_state().await.assert_eq("ˇx hello hello\n");
991 cx.simulate_shared_keystrokes(": escape").await;
992 cx.simulate_shared_keystrokes(".").await;
993 cx.shared_state().await.assert_eq("ˇx hello\n");
994 }
995
996 #[gpui::test]
997 async fn test_repeat_after_blur_resets_dot_replaying(cx: &mut gpui::TestAppContext) {
998 let mut cx = VimTestContext::new(cx, true).await;
999
1000 // Bind `ctrl-f` to the `buffer_search::Deploy` action so that this can
1001 // be triggered while in Insert mode, ensuring that an action which
1002 // moves the focus away from the editor, gets recorded.
1003 cx.update(|_, cx| {
1004 cx.bind_keys([gpui::KeyBinding::new(
1005 "ctrl-f",
1006 search::buffer_search::Deploy::find(),
1007 None,
1008 )])
1009 });
1010
1011 cx.set_state("ˇhello", Mode::Normal);
1012
1013 // We're going to enter insert mode, which will start recording, type a
1014 // character and then immediately use `ctrl-f` to trigger the buffer
1015 // search. Triggering the buffer search will move focus away from the
1016 // editor, effectively stopping the recording immediately after
1017 // `buffer_search::Deploy` is recorded. The first `escape` is used to
1018 // dismiss the search bar, while the second is used to move from Insert
1019 // to Normal mode.
1020 cx.simulate_keystrokes("i x ctrl-f escape escape");
1021 cx.run_until_parked();
1022
1023 // Using the `.` key will dispatch the `vim::Repeat` action, repeating
1024 // the set of recorded actions. This will eventually focus on the search
1025 // bar, preventing the `EndRepeat` action from being correctly handled.
1026 cx.simulate_keystrokes(".");
1027 cx.run_until_parked();
1028
1029 // After replay finishes, even though the `EndRepeat` action wasn't
1030 // handled, seeing as the editor lost focus during replay, the
1031 // `dot_replaying` value should be set back to `false`.
1032 assert!(
1033 !cx.update(|_, cx| cx.global::<VimGlobals>().dot_replaying),
1034 "dot_replaying should be false after repeat completes"
1035 );
1036 }
1037
1038 #[gpui::test]
1039 async fn test_undo_repeated_insert(cx: &mut gpui::TestAppContext) {
1040 let mut cx = NeovimBackedTestContext::new(cx).await;
1041
1042 cx.set_shared_state("hellˇo").await;
1043 cx.simulate_shared_keystrokes("3 a . escape").await;
1044 cx.shared_state().await.assert_eq("hello..ˇ.");
1045 cx.simulate_shared_keystrokes("u").await;
1046 cx.shared_state().await.assert_eq("hellˇo");
1047 }
1048
1049 #[gpui::test]
1050 async fn test_record_replay(cx: &mut gpui::TestAppContext) {
1051 let mut cx = NeovimBackedTestContext::new(cx).await;
1052
1053 cx.set_shared_state("ˇhello world").await;
1054 cx.simulate_shared_keystrokes("q w c w j escape q").await;
1055 cx.shared_state().await.assert_eq("ˇj world");
1056 cx.simulate_shared_keystrokes("2 l @ w").await;
1057 cx.shared_state().await.assert_eq("j ˇj");
1058 }
1059
1060 #[gpui::test]
1061 async fn test_record_replay_count(cx: &mut gpui::TestAppContext) {
1062 let mut cx = NeovimBackedTestContext::new(cx).await;
1063
1064 cx.set_shared_state("ˇhello world!!").await;
1065 cx.simulate_shared_keystrokes("q a v 3 l s 0 escape l q")
1066 .await;
1067 cx.shared_state().await.assert_eq("0ˇo world!!");
1068 cx.simulate_shared_keystrokes("2 @ a").await;
1069 cx.shared_state().await.assert_eq("000ˇ!");
1070 }
1071
1072 #[gpui::test]
1073 async fn test_record_replay_dot(cx: &mut gpui::TestAppContext) {
1074 let mut cx = NeovimBackedTestContext::new(cx).await;
1075
1076 cx.set_shared_state("ˇhello world").await;
1077 cx.simulate_shared_keystrokes("q a r a l r b l q").await;
1078 cx.shared_state().await.assert_eq("abˇllo world");
1079 cx.simulate_shared_keystrokes(".").await;
1080 cx.shared_state().await.assert_eq("abˇblo world");
1081 cx.simulate_shared_keystrokes("shift-q").await;
1082 cx.shared_state().await.assert_eq("ababˇo world");
1083 cx.simulate_shared_keystrokes(".").await;
1084 cx.shared_state().await.assert_eq("ababˇb world");
1085 }
1086
1087 #[gpui::test]
1088 async fn test_record_replay_of_dot(cx: &mut gpui::TestAppContext) {
1089 let mut cx = NeovimBackedTestContext::new(cx).await;
1090
1091 cx.set_shared_state("ˇhello world").await;
1092 cx.simulate_shared_keystrokes("r o q w . q").await;
1093 cx.shared_state().await.assert_eq("ˇoello world");
1094 cx.simulate_shared_keystrokes("d l").await;
1095 cx.shared_state().await.assert_eq("ˇello world");
1096 cx.simulate_shared_keystrokes("@ w").await;
1097 cx.shared_state().await.assert_eq("ˇllo world");
1098 }
1099
1100 #[gpui::test]
1101 async fn test_record_replay_interleaved(cx: &mut gpui::TestAppContext) {
1102 let mut cx = NeovimBackedTestContext::new(cx).await;
1103
1104 cx.set_shared_state("ˇhello world").await;
1105 cx.simulate_shared_keystrokes("q z r a l q").await;
1106 cx.shared_state().await.assert_eq("aˇello world");
1107 cx.simulate_shared_keystrokes("q b @ z @ z q").await;
1108 cx.shared_state().await.assert_eq("aaaˇlo world");
1109 cx.simulate_shared_keystrokes("@ @").await;
1110 cx.shared_state().await.assert_eq("aaaaˇo world");
1111 cx.simulate_shared_keystrokes("@ b").await;
1112 cx.shared_state().await.assert_eq("aaaaaaˇworld");
1113 cx.simulate_shared_keystrokes("@ @").await;
1114 cx.shared_state().await.assert_eq("aaaaaaaˇorld");
1115 cx.simulate_shared_keystrokes("q z r b l q").await;
1116 cx.shared_state().await.assert_eq("aaaaaaabˇrld");
1117 cx.simulate_shared_keystrokes("@ b").await;
1118 cx.shared_state().await.assert_eq("aaaaaaabbbˇd");
1119 }
1120
1121 #[gpui::test]
1122 async fn test_repeat_clear(cx: &mut gpui::TestAppContext) {
1123 let mut cx = VimTestContext::new(cx, true).await;
1124
1125 // Check that, when repeat is preceded by something other than a number,
1126 // the current operator is cleared, in order to prevent infinite loops.
1127 cx.set_state("ˇhello world", Mode::Normal);
1128 cx.simulate_keystrokes("d .");
1129 assert_eq!(cx.active_operator(), None);
1130 }
1131
1132 #[gpui::test]
1133 async fn test_repeat_clear_repeat(cx: &mut gpui::TestAppContext) {
1134 let mut cx = NeovimBackedTestContext::new(cx).await;
1135
1136 cx.set_shared_state(indoc! {
1137 "ˇthe quick brown
1138 fox jumps over
1139 the lazy dog"
1140 })
1141 .await;
1142 cx.simulate_shared_keystrokes("d d").await;
1143 cx.shared_state().await.assert_eq(indoc! {
1144 "ˇfox jumps over
1145 the lazy dog"
1146 });
1147 cx.simulate_shared_keystrokes("d . .").await;
1148 cx.shared_state().await.assert_eq(indoc! {
1149 "ˇthe lazy dog"
1150 });
1151 }
1152
1153 #[gpui::test]
1154 async fn test_repeat_clear_count(cx: &mut gpui::TestAppContext) {
1155 let mut cx = NeovimBackedTestContext::new(cx).await;
1156
1157 cx.set_shared_state(indoc! {
1158 "ˇthe quick brown
1159 fox jumps over
1160 the lazy dog"
1161 })
1162 .await;
1163 cx.simulate_shared_keystrokes("d d").await;
1164 cx.shared_state().await.assert_eq(indoc! {
1165 "ˇfox jumps over
1166 the lazy dog"
1167 });
1168 cx.simulate_shared_keystrokes("2 d .").await;
1169 cx.shared_state().await.assert_eq(indoc! {
1170 "ˇfox jumps over
1171 the lazy dog"
1172 });
1173 cx.simulate_shared_keystrokes(".").await;
1174 cx.shared_state().await.assert_eq(indoc! {
1175 "ˇthe lazy dog"
1176 });
1177
1178 cx.set_shared_state(indoc! {
1179 "ˇthe quick brown
1180 fox jumps over
1181 the lazy dog
1182 the quick brown
1183 fox jumps over
1184 the lazy dog"
1185 })
1186 .await;
1187 cx.simulate_shared_keystrokes("2 d d").await;
1188 cx.shared_state().await.assert_eq(indoc! {
1189 "ˇthe lazy dog
1190 the quick brown
1191 fox jumps over
1192 the lazy dog"
1193 });
1194 cx.simulate_shared_keystrokes("5 d .").await;
1195 cx.shared_state().await.assert_eq(indoc! {
1196 "ˇthe lazy dog
1197 the quick brown
1198 fox jumps over
1199 the lazy dog"
1200 });
1201 cx.simulate_shared_keystrokes(".").await;
1202 cx.shared_state().await.assert_eq(indoc! {
1203 "ˇfox jumps over
1204 the lazy dog"
1205 });
1206 }
1207}
1208