Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T04:34:42.555Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

repeat.rs

1208 lines · 40.9 KB · rust
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(&register);
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(&register) 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
Served at tenant.openagents/omega Member data and write actions are omitted.