1use crate::{
2 motion::Motion,
3 state::{Mode, RecordedSelection, ReplayableAction},
4 visual::visual_motion,
5 Vim,
6};
7use gpui::{actions, AppContext};
8use workspace::Workspace;
9
10actions!(vim, [Repeat, EndRepeat,]);
11
12pub(crate) fn init(cx: &mut AppContext) {
13 cx.add_action(|_: &mut Workspace, _: &EndRepeat, cx| {
14 Vim::update(cx, |vim, cx| {
15 vim.workspace_state.replaying = false;
16 vim.update_active_editor(cx, |editor, _| {
17 editor.show_local_selections = true;
18 });
19 vim.switch_mode(Mode::Normal, false, cx)
20 });
21 });
22
23 cx.add_action(|_: &mut Workspace, _: &Repeat, cx| {
24 let Some((actions, editor, selection)) = Vim::update(cx, |vim, cx| {
25 let actions = vim.workspace_state.recorded_actions.clone();
26 let Some(editor) = vim.active_editor.clone() else {
27 return None;
28 };
29 let count = vim.pop_number_operator(cx);
30
31 vim.workspace_state.replaying = true;
32
33 let selection = vim.workspace_state.recorded_selection.clone();
34 match selection {
35 RecordedSelection::SingleLine { .. } | RecordedSelection::Visual { .. } => {
36 vim.workspace_state.recorded_count = None;
37 vim.switch_mode(Mode::Visual, false, cx)
38 }
39 RecordedSelection::VisualLine { .. } => {
40 vim.workspace_state.recorded_count = None;
41 vim.switch_mode(Mode::VisualLine, false, cx)
42 }
43 RecordedSelection::VisualBlock { .. } => {
44 vim.workspace_state.recorded_count = None;
45 vim.switch_mode(Mode::VisualBlock, false, cx)
46 }
47 RecordedSelection::None => {
48 if let Some(count) = count {
49 vim.workspace_state.recorded_count = Some(count);
50 }
51 }
52 }
53
54 if let Some(editor) = editor.upgrade(cx) {
55 editor.update(cx, |editor, _| {
56 editor.show_local_selections = false;
57 })
58 } else {
59 return None;
60 }
61
62 Some((actions, editor, selection))
63 }) else {
64 return;
65 };
66
67 match selection {
68 RecordedSelection::SingleLine { cols } => {
69 if cols > 1 {
70 visual_motion(Motion::Right, Some(cols as usize - 1), cx)
71 }
72 }
73 RecordedSelection::Visual { rows, cols } => {
74 visual_motion(
75 Motion::Down {
76 display_lines: false,
77 },
78 Some(rows as usize),
79 cx,
80 );
81 visual_motion(
82 Motion::StartOfLine {
83 display_lines: false,
84 },
85 None,
86 cx,
87 );
88 if cols > 1 {
89 visual_motion(Motion::Right, Some(cols as usize - 1), cx)
90 }
91 }
92 RecordedSelection::VisualBlock { rows, cols } => {
93 visual_motion(
94 Motion::Down {
95 display_lines: false,
96 },
97 Some(rows as usize),
98 cx,
99 );
100 if cols > 1 {
101 visual_motion(Motion::Right, Some(cols as usize - 1), cx);
102 }
103 }
104 RecordedSelection::VisualLine { rows } => {
105 visual_motion(
106 Motion::Down {
107 display_lines: false,
108 },
109 Some(rows as usize),
110 cx,
111 );
112 }
113 RecordedSelection::None => {}
114 }
115
116 let window = cx.window();
117 cx.app_context()
118 .spawn(move |mut cx| async move {
119 for action in actions {
120 match action {
121 ReplayableAction::Action(action) => window
122 .dispatch_action(editor.id(), action.as_ref(), &mut cx)
123 .ok_or_else(|| anyhow::anyhow!("window was closed")),
124 ReplayableAction::Insertion {
125 text,
126 utf16_range_to_replace,
127 } => editor.update(&mut cx, |editor, cx| {
128 editor.replay_insert_event(&text, utf16_range_to_replace.clone(), cx)
129 }),
130 }?
131 }
132 window
133 .dispatch_action(editor.id(), &EndRepeat, &mut cx)
134 .ok_or_else(|| anyhow::anyhow!("window was closed"))
135 })
136 .detach_and_log_err(cx);
137 });
138}
139
140#[cfg(test)]
141mod test {
142 use std::sync::Arc;
143
144 use editor::test::editor_lsp_test_context::EditorLspTestContext;
145 use futures::StreamExt;
146 use indoc::indoc;
147
148 use gpui::{executor::Deterministic, View};
149
150 use crate::{
151 state::Mode,
152 test::{NeovimBackedTestContext, VimTestContext},
153 };
154
155 #[gpui::test]
156 async fn test_dot_repeat(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
157 let mut cx = NeovimBackedTestContext::new(cx).await;
158
159 // "o"
160 cx.set_shared_state("ˇhello").await;
161 cx.simulate_shared_keystrokes(["o", "w", "o", "r", "l", "d", "escape"])
162 .await;
163 cx.assert_shared_state("hello\nworlˇd").await;
164 cx.simulate_shared_keystrokes(["."]).await;
165 deterministic.run_until_parked();
166 cx.assert_shared_state("hello\nworld\nworlˇd").await;
167
168 // "d"
169 cx.simulate_shared_keystrokes(["^", "d", "f", "o"]).await;
170 cx.simulate_shared_keystrokes(["g", "g", "."]).await;
171 deterministic.run_until_parked();
172 cx.assert_shared_state("ˇ\nworld\nrld").await;
173
174 // "p" (note that it pastes the current clipboard)
175 cx.simulate_shared_keystrokes(["j", "y", "y", "p"]).await;
176 cx.simulate_shared_keystrokes(["shift-g", "y", "y", "."])
177 .await;
178 deterministic.run_until_parked();
179 cx.assert_shared_state("\nworld\nworld\nrld\nˇrld").await;
180
181 // "~" (note that counts apply to the action taken, not . itself)
182 cx.set_shared_state("ˇthe quick brown fox").await;
183 cx.simulate_shared_keystrokes(["2", "~", "."]).await;
184 deterministic.run_until_parked();
185 cx.set_shared_state("THE ˇquick brown fox").await;
186 cx.simulate_shared_keystrokes(["3", "."]).await;
187 deterministic.run_until_parked();
188 cx.set_shared_state("THE QUIˇck brown fox").await;
189 deterministic.run_until_parked();
190 cx.simulate_shared_keystrokes(["."]).await;
191 deterministic.run_until_parked();
192 cx.set_shared_state("THE QUICK ˇbrown fox").await;
193 }
194
195 #[gpui::test]
196 async fn test_repeat_ime(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
197 let mut cx = VimTestContext::new(cx, true).await;
198
199 cx.set_state("hˇllo", Mode::Normal);
200 cx.simulate_keystrokes(["i"]);
201
202 // simulate brazilian input for ä.
203 cx.update_editor(|editor, cx| {
204 editor.replace_and_mark_text_in_range(None, "\"", Some(1..1), cx);
205 editor.replace_text_in_range(None, "ä", cx);
206 });
207 cx.simulate_keystrokes(["escape"]);
208 cx.assert_state("hˇällo", Mode::Normal);
209 cx.simulate_keystrokes(["."]);
210 deterministic.run_until_parked();
211 cx.assert_state("hˇäällo", Mode::Normal);
212 }
213
214 #[gpui::test]
215 async fn test_repeat_completion(
216 deterministic: Arc<Deterministic>,
217 cx: &mut gpui::TestAppContext,
218 ) {
219 let cx = EditorLspTestContext::new_rust(
220 lsp::ServerCapabilities {
221 completion_provider: Some(lsp::CompletionOptions {
222 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
223 resolve_provider: Some(true),
224 ..Default::default()
225 }),
226 ..Default::default()
227 },
228 cx,
229 )
230 .await;
231 let mut cx = VimTestContext::new_with_lsp(cx, true);
232
233 cx.set_state(
234 indoc! {"
235 onˇe
236 two
237 three
238 "},
239 Mode::Normal,
240 );
241
242 let mut request =
243 cx.handle_request::<lsp::request::Completion, _, _>(move |_, params, _| async move {
244 let position = params.text_document_position.position;
245 Ok(Some(lsp::CompletionResponse::Array(vec![
246 lsp::CompletionItem {
247 label: "first".to_string(),
248 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
249 range: lsp::Range::new(position.clone(), position.clone()),
250 new_text: "first".to_string(),
251 })),
252 ..Default::default()
253 },
254 lsp::CompletionItem {
255 label: "second".to_string(),
256 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
257 range: lsp::Range::new(position.clone(), position.clone()),
258 new_text: "second".to_string(),
259 })),
260 ..Default::default()
261 },
262 ])))
263 });
264 cx.simulate_keystrokes(["a", "."]);
265 request.next().await;
266 cx.condition(|editor, _| editor.context_menu_visible())
267 .await;
268 cx.simulate_keystrokes(["down", "enter", "!", "escape"]);
269
270 cx.assert_state(
271 indoc! {"
272 one.secondˇ!
273 two
274 three
275 "},
276 Mode::Normal,
277 );
278 cx.simulate_keystrokes(["j", "."]);
279 deterministic.run_until_parked();
280 cx.assert_state(
281 indoc! {"
282 one.second!
283 two.secondˇ!
284 three
285 "},
286 Mode::Normal,
287 );
288 }
289
290 #[gpui::test]
291 async fn test_repeat_visual(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
292 let mut cx = NeovimBackedTestContext::new(cx).await;
293
294 // single-line (3 columns)
295 cx.set_shared_state(indoc! {
296 "ˇthe quick brown
297 fox jumps over
298 the lazy dog"
299 })
300 .await;
301 cx.simulate_shared_keystrokes(["v", "i", "w", "s", "o", "escape"])
302 .await;
303 cx.assert_shared_state(indoc! {
304 "ˇo quick brown
305 fox jumps over
306 the lazy dog"
307 })
308 .await;
309 cx.simulate_shared_keystrokes(["j", "w", "."]).await;
310 deterministic.run_until_parked();
311 cx.assert_shared_state(indoc! {
312 "o quick brown
313 fox ˇops over
314 the lazy dog"
315 })
316 .await;
317 cx.simulate_shared_keystrokes(["f", "r", "."]).await;
318 deterministic.run_until_parked();
319 cx.assert_shared_state(indoc! {
320 "o quick brown
321 fox ops oveˇothe lazy dog"
322 })
323 .await;
324
325 // visual
326 cx.set_shared_state(indoc! {
327 "the ˇquick brown
328 fox jumps over
329 fox jumps over
330 fox jumps over
331 the lazy dog"
332 })
333 .await;
334 cx.simulate_shared_keystrokes(["v", "j", "x"]).await;
335 cx.assert_shared_state(indoc! {
336 "the ˇumps over
337 fox jumps over
338 fox jumps over
339 the lazy dog"
340 })
341 .await;
342 cx.simulate_shared_keystrokes(["."]).await;
343 deterministic.run_until_parked();
344 cx.assert_shared_state(indoc! {
345 "the ˇumps over
346 fox jumps over
347 the lazy dog"
348 })
349 .await;
350 cx.simulate_shared_keystrokes(["w", "."]).await;
351 deterministic.run_until_parked();
352 cx.assert_shared_state(indoc! {
353 "the umps ˇumps over
354 the lazy dog"
355 })
356 .await;
357 cx.simulate_shared_keystrokes(["j", "."]).await;
358 deterministic.run_until_parked();
359 cx.assert_shared_state(indoc! {
360 "the umps umps over
361 the ˇog"
362 })
363 .await;
364
365 // block mode (3 rows)
366 cx.set_shared_state(indoc! {
367 "ˇthe quick brown
368 fox jumps over
369 the lazy dog"
370 })
371 .await;
372 cx.simulate_shared_keystrokes(["ctrl-v", "j", "j", "shift-i", "o", "escape"])
373 .await;
374 cx.assert_shared_state(indoc! {
375 "ˇothe quick brown
376 ofox jumps over
377 othe lazy dog"
378 })
379 .await;
380 cx.simulate_shared_keystrokes(["j", "4", "l", "."]).await;
381 deterministic.run_until_parked();
382 cx.assert_shared_state(indoc! {
383 "othe quick brown
384 ofoxˇo jumps over
385 otheo lazy dog"
386 })
387 .await;
388
389 // line mode
390 cx.set_shared_state(indoc! {
391 "ˇthe quick brown
392 fox jumps over
393 the lazy dog"
394 })
395 .await;
396 cx.simulate_shared_keystrokes(["shift-v", "shift-r", "o", "escape"])
397 .await;
398 cx.assert_shared_state(indoc! {
399 "ˇo
400 fox jumps over
401 the lazy dog"
402 })
403 .await;
404 cx.simulate_shared_keystrokes(["j", "."]).await;
405 deterministic.run_until_parked();
406 cx.assert_shared_state(indoc! {
407 "o
408 ˇo
409 the lazy dog"
410 })
411 .await;
412 }
413}