1use crate::{
2 state::{Mode, ReplayableAction},
3 Vim,
4};
5use gpui::{actions, AppContext};
6use workspace::Workspace;
7
8actions!(vim, [Repeat, EndRepeat,]);
9
10pub(crate) fn init(cx: &mut AppContext) {
11 cx.add_action(|_: &mut Workspace, _: &EndRepeat, cx| {
12 Vim::update(cx, |vim, cx| {
13 vim.workspace_state.replaying = false;
14 vim.switch_mode(Mode::Normal, false, cx)
15 });
16 });
17
18 cx.add_action(|_: &mut Workspace, _: &Repeat, cx| {
19 Vim::update(cx, |vim, cx| {
20 let actions = vim.workspace_state.repeat_actions.clone();
21 let Some(editor) = vim.active_editor.clone() else {
22 return;
23 };
24 if let Some(new_count) = vim.pop_number_operator(cx) {
25 vim.workspace_state.recorded_count = Some(new_count);
26 }
27 vim.workspace_state.replaying = true;
28
29 let window = cx.window();
30 cx.app_context()
31 .spawn(move |mut cx| async move {
32 for action in actions {
33 match action {
34 ReplayableAction::Action(action) => window
35 .dispatch_action(editor.id(), action.as_ref(), &mut cx)
36 .ok_or_else(|| anyhow::anyhow!("window was closed")),
37 ReplayableAction::Insertion {
38 text,
39 utf16_range_to_replace,
40 } => editor.update(&mut cx, |editor, cx| {
41 editor.replay_insert_event(
42 &text,
43 utf16_range_to_replace.clone(),
44 cx,
45 )
46 }),
47 }?
48 }
49 window
50 .dispatch_action(editor.id(), &EndRepeat, &mut cx)
51 .ok_or_else(|| anyhow::anyhow!("window was closed"))
52 })
53 .detach_and_log_err(cx);
54 });
55 });
56}
57
58#[cfg(test)]
59mod test {
60 use std::sync::Arc;
61
62 use editor::test::editor_lsp_test_context::EditorLspTestContext;
63 use futures::StreamExt;
64 use indoc::indoc;
65
66 use gpui::{executor::Deterministic, View};
67
68 use crate::{
69 state::Mode,
70 test::{NeovimBackedTestContext, VimTestContext},
71 };
72
73 #[gpui::test]
74 async fn test_dot_repeat(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
75 let mut cx = NeovimBackedTestContext::new(cx).await;
76
77 // "o"
78 cx.set_shared_state("ˇhello").await;
79 cx.simulate_shared_keystrokes(["o", "w", "o", "r", "l", "d", "escape"])
80 .await;
81 cx.assert_shared_state("hello\nworlˇd").await;
82 cx.simulate_shared_keystrokes(["."]).await;
83 deterministic.run_until_parked();
84 cx.assert_shared_state("hello\nworld\nworlˇd").await;
85
86 // "d"
87 cx.simulate_shared_keystrokes(["^", "d", "f", "o"]).await;
88 cx.simulate_shared_keystrokes(["g", "g", "."]).await;
89 deterministic.run_until_parked();
90 cx.assert_shared_state("ˇ\nworld\nrld").await;
91
92 // "p" (note that it pastes the current clipboard)
93 cx.simulate_shared_keystrokes(["j", "y", "y", "p"]).await;
94 cx.simulate_shared_keystrokes(["shift-g", "y", "y", "."])
95 .await;
96 deterministic.run_until_parked();
97 cx.assert_shared_state("\nworld\nworld\nrld\nˇrld").await;
98
99 // "~" (note that counts apply to the action taken, not . itself)
100 cx.set_shared_state("ˇthe quick brown fox").await;
101 cx.simulate_shared_keystrokes(["2", "~", "."]).await;
102 deterministic.run_until_parked();
103 cx.set_shared_state("THE ˇquick brown fox").await;
104 cx.simulate_shared_keystrokes(["3", "."]).await;
105 deterministic.run_until_parked();
106 cx.set_shared_state("THE QUIˇck brown fox").await;
107 deterministic.run_until_parked();
108 cx.simulate_shared_keystrokes(["."]).await;
109 deterministic.run_until_parked();
110 cx.set_shared_state("THE QUICK ˇbrown fox").await;
111 }
112
113 #[gpui::test]
114 async fn test_repeat_ime(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
115 let mut cx = VimTestContext::new(cx, true).await;
116
117 cx.set_state("hˇllo", Mode::Normal);
118 cx.simulate_keystrokes(["i"]);
119
120 // simulate brazilian input for ä.
121 cx.update_editor(|editor, cx| {
122 editor.replace_and_mark_text_in_range(None, "\"", Some(1..1), cx);
123 editor.replace_text_in_range(None, "ä", cx);
124 });
125 cx.simulate_keystrokes(["escape"]);
126 cx.assert_state("hˇällo", Mode::Normal);
127 cx.simulate_keystrokes(["."]);
128 deterministic.run_until_parked();
129 cx.assert_state("hˇäällo", Mode::Normal);
130 }
131
132 #[gpui::test]
133 async fn test_repeat_completion(
134 deterministic: Arc<Deterministic>,
135 cx: &mut gpui::TestAppContext,
136 ) {
137 let cx = EditorLspTestContext::new_rust(
138 lsp::ServerCapabilities {
139 completion_provider: Some(lsp::CompletionOptions {
140 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
141 resolve_provider: Some(true),
142 ..Default::default()
143 }),
144 ..Default::default()
145 },
146 cx,
147 )
148 .await;
149 let mut cx = VimTestContext::new_with_lsp(cx, true);
150
151 cx.set_state(
152 indoc! {"
153 onˇe
154 two
155 three
156 "},
157 Mode::Normal,
158 );
159
160 let mut request =
161 cx.handle_request::<lsp::request::Completion, _, _>(move |_, params, _| async move {
162 let position = params.text_document_position.position;
163 Ok(Some(lsp::CompletionResponse::Array(vec![
164 lsp::CompletionItem {
165 label: "first".to_string(),
166 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
167 range: lsp::Range::new(position.clone(), position.clone()),
168 new_text: "first".to_string(),
169 })),
170 ..Default::default()
171 },
172 lsp::CompletionItem {
173 label: "second".to_string(),
174 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
175 range: lsp::Range::new(position.clone(), position.clone()),
176 new_text: "second".to_string(),
177 })),
178 ..Default::default()
179 },
180 ])))
181 });
182 cx.simulate_keystrokes(["a", "."]);
183 request.next().await;
184 cx.condition(|editor, _| editor.context_menu_visible())
185 .await;
186 cx.simulate_keystrokes(["down", "enter", "!", "escape"]);
187
188 cx.assert_state(
189 indoc! {"
190 one.secondˇ!
191 two
192 three
193 "},
194 Mode::Normal,
195 );
196 cx.simulate_keystrokes(["j", "."]);
197 deterministic.run_until_parked();
198 cx.assert_state(
199 indoc! {"
200 one.second!
201 two.secondˇ!
202 three
203 "},
204 Mode::Normal,
205 );
206 }
207}