1use crate::{
2 insert::NormalBefore,
3 motion::Motion,
4 state::{Mode, RecordedSelection, ReplayableAction},
5 visual::visual_motion,
6 Vim,
7};
8use gpui::{actions, Action, ViewContext, WindowContext};
9use workspace::Workspace;
10
11actions!(vim, [Repeat, EndRepeat]);
12
13fn should_replay(action: &Box<dyn Action>) -> bool {
14 // skip so that we don't leave the character palette open
15 if editor::actions::ShowCharacterPalette.partial_eq(&**action) {
16 return false;
17 }
18 true
19}
20
21fn repeatable_insert(action: &ReplayableAction) -> Option<Box<dyn Action>> {
22 match action {
23 ReplayableAction::Action(action) => {
24 if super::InsertBefore.partial_eq(&**action)
25 || super::InsertAfter.partial_eq(&**action)
26 || super::InsertFirstNonWhitespace.partial_eq(&**action)
27 || super::InsertEndOfLine.partial_eq(&**action)
28 {
29 Some(super::InsertBefore.boxed_clone())
30 } else if super::InsertLineAbove.partial_eq(&**action)
31 || super::InsertLineBelow.partial_eq(&**action)
32 {
33 Some(super::InsertLineBelow.boxed_clone())
34 } else if crate::replace::ToggleReplace.partial_eq(&**action) {
35 Some(crate::replace::ToggleReplace.boxed_clone())
36 } else {
37 None
38 }
39 }
40 ReplayableAction::Insertion { .. } => None,
41 }
42}
43
44pub(crate) fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
45 workspace.register_action(|_: &mut Workspace, _: &EndRepeat, cx| {
46 Vim::update(cx, |vim, cx| {
47 vim.workspace_state.replaying = false;
48 vim.switch_mode(Mode::Normal, false, cx)
49 });
50 });
51
52 workspace.register_action(|_: &mut Workspace, _: &Repeat, cx| repeat(cx, false));
53}
54
55pub(crate) fn repeat(cx: &mut WindowContext, from_insert_mode: bool) {
56 let Some((mut actions, editor, selection)) = Vim::update(cx, |vim, cx| {
57 let actions = vim.workspace_state.recorded_actions.clone();
58 if actions.is_empty() {
59 return None;
60 }
61
62 let Some(editor) = vim.active_editor.clone() else {
63 return None;
64 };
65 let count = vim.take_count(cx);
66
67 let selection = vim.workspace_state.recorded_selection.clone();
68 match selection {
69 RecordedSelection::SingleLine { .. } | RecordedSelection::Visual { .. } => {
70 vim.workspace_state.recorded_count = None;
71 vim.switch_mode(Mode::Visual, false, cx)
72 }
73 RecordedSelection::VisualLine { .. } => {
74 vim.workspace_state.recorded_count = None;
75 vim.switch_mode(Mode::VisualLine, false, cx)
76 }
77 RecordedSelection::VisualBlock { .. } => {
78 vim.workspace_state.recorded_count = None;
79 vim.switch_mode(Mode::VisualBlock, false, cx)
80 }
81 RecordedSelection::None => {
82 if let Some(count) = count {
83 vim.workspace_state.recorded_count = Some(count);
84 }
85 }
86 }
87
88 Some((actions, editor, selection))
89 }) else {
90 return;
91 };
92
93 match selection {
94 RecordedSelection::SingleLine { cols } => {
95 if cols > 1 {
96 visual_motion(Motion::Right, Some(cols as usize - 1), cx)
97 }
98 }
99 RecordedSelection::Visual { rows, cols } => {
100 visual_motion(
101 Motion::Down {
102 display_lines: false,
103 },
104 Some(rows as usize),
105 cx,
106 );
107 visual_motion(
108 Motion::StartOfLine {
109 display_lines: false,
110 },
111 None,
112 cx,
113 );
114 if cols > 1 {
115 visual_motion(Motion::Right, Some(cols as usize - 1), cx)
116 }
117 }
118 RecordedSelection::VisualBlock { rows, cols } => {
119 visual_motion(
120 Motion::Down {
121 display_lines: false,
122 },
123 Some(rows as usize),
124 cx,
125 );
126 if cols > 1 {
127 visual_motion(Motion::Right, Some(cols as usize - 1), cx);
128 }
129 }
130 RecordedSelection::VisualLine { rows } => {
131 visual_motion(
132 Motion::Down {
133 display_lines: false,
134 },
135 Some(rows as usize),
136 cx,
137 );
138 }
139 RecordedSelection::None => {}
140 }
141
142 // insert internally uses repeat to handle counts
143 // vim doesn't treat 3a1 as though you literally repeated a1
144 // 3 times, instead it inserts the content thrice at the insert position.
145 if let Some(to_repeat) = repeatable_insert(&actions[0]) {
146 if let Some(ReplayableAction::Action(action)) = actions.last() {
147 if NormalBefore.partial_eq(&**action) {
148 actions.pop();
149 }
150 }
151
152 let mut new_actions = actions.clone();
153 actions[0] = ReplayableAction::Action(to_repeat.boxed_clone());
154
155 let mut count = Vim::read(cx).workspace_state.recorded_count.unwrap_or(1);
156
157 // if we came from insert mode we're just doing repetitions 2 onwards.
158 if from_insert_mode {
159 count -= 1;
160 new_actions[0] = actions[0].clone();
161 }
162
163 for _ in 1..count {
164 new_actions.append(actions.clone().as_mut());
165 }
166 new_actions.push(ReplayableAction::Action(NormalBefore.boxed_clone()));
167 actions = new_actions;
168 }
169
170 Vim::update(cx, |vim, _| vim.workspace_state.replaying = true);
171 let window = cx.window_handle();
172 cx.spawn(move |mut cx| async move {
173 editor.update(&mut cx, |editor, _| {
174 editor.show_local_selections = false;
175 })?;
176 for action in actions {
177 if !matches!(
178 cx.update(|cx| Vim::read(cx).workspace_state.replaying),
179 Ok(true)
180 ) {
181 break;
182 }
183
184 match action {
185 ReplayableAction::Action(action) => {
186 if should_replay(&action) {
187 window.update(&mut cx, |_, cx| cx.dispatch_action(action))
188 } else {
189 Ok(())
190 }
191 }
192 ReplayableAction::Insertion {
193 text,
194 utf16_range_to_replace,
195 } => editor.update(&mut cx, |editor, cx| {
196 editor.replay_insert_event(&text, utf16_range_to_replace.clone(), cx)
197 }),
198 }?
199 }
200 editor.update(&mut cx, |editor, _| {
201 editor.show_local_selections = true;
202 })?;
203 window.update(&mut cx, |_, cx| cx.dispatch_action(EndRepeat.boxed_clone()))
204 })
205 .detach_and_log_err(cx);
206}
207
208#[cfg(test)]
209mod test {
210 use editor::test::editor_lsp_test_context::EditorLspTestContext;
211 use futures::StreamExt;
212 use indoc::indoc;
213
214 use gpui::ViewInputHandler;
215
216 use crate::{
217 state::Mode,
218 test::{NeovimBackedTestContext, VimTestContext},
219 };
220
221 #[gpui::test]
222 async fn test_dot_repeat(cx: &mut gpui::TestAppContext) {
223 let mut cx = NeovimBackedTestContext::new(cx).await;
224
225 // "o"
226 cx.set_shared_state("ˇhello").await;
227 cx.simulate_shared_keystrokes("o w o r l d escape").await;
228 cx.shared_state().await.assert_eq("hello\nworlˇd");
229 cx.simulate_shared_keystrokes(".").await;
230 cx.shared_state().await.assert_eq("hello\nworld\nworlˇd");
231
232 // "d"
233 cx.simulate_shared_keystrokes("^ d f o").await;
234 cx.simulate_shared_keystrokes("g g .").await;
235 cx.shared_state().await.assert_eq("ˇ\nworld\nrld");
236
237 // "p" (note that it pastes the current clipboard)
238 cx.simulate_shared_keystrokes("j y y p").await;
239 cx.simulate_shared_keystrokes("shift-g y y .").await;
240 cx.shared_state()
241 .await
242 .assert_eq("\nworld\nworld\nrld\nˇrld");
243
244 // "~" (note that counts apply to the action taken, not . itself)
245 cx.set_shared_state("ˇthe quick brown fox").await;
246 cx.simulate_shared_keystrokes("2 ~ .").await;
247 cx.set_shared_state("THE ˇquick brown fox").await;
248 cx.simulate_shared_keystrokes("3 .").await;
249 cx.set_shared_state("THE QUIˇck brown fox").await;
250 cx.run_until_parked();
251 cx.simulate_shared_keystrokes(".").await;
252 cx.shared_state().await.assert_eq("THE QUICK ˇbrown fox");
253 }
254
255 #[gpui::test]
256 async fn test_repeat_ime(cx: &mut gpui::TestAppContext) {
257 let mut cx = VimTestContext::new(cx, true).await;
258
259 cx.set_state("hˇllo", Mode::Normal);
260 cx.simulate_keystrokes("i");
261
262 // simulate brazilian input for ä.
263 cx.update_editor(|editor, cx| {
264 editor.replace_and_mark_text_in_range(None, "\"", Some(1..1), cx);
265 editor.replace_text_in_range(None, "ä", cx);
266 });
267 cx.simulate_keystrokes("escape");
268 cx.assert_state("hˇällo", Mode::Normal);
269 cx.simulate_keystrokes(".");
270 cx.assert_state("hˇäällo", Mode::Normal);
271 }
272
273 #[gpui::test]
274 async fn test_repeat_completion(cx: &mut gpui::TestAppContext) {
275 VimTestContext::init(cx);
276 let cx = EditorLspTestContext::new_rust(
277 lsp::ServerCapabilities {
278 completion_provider: Some(lsp::CompletionOptions {
279 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
280 resolve_provider: Some(true),
281 ..Default::default()
282 }),
283 ..Default::default()
284 },
285 cx,
286 )
287 .await;
288 let mut cx = VimTestContext::new_with_lsp(cx, true);
289
290 cx.set_state(
291 indoc! {"
292 onˇe
293 two
294 three
295 "},
296 Mode::Normal,
297 );
298
299 let mut request =
300 cx.handle_request::<lsp::request::Completion, _, _>(move |_, params, _| async move {
301 let position = params.text_document_position.position;
302 Ok(Some(lsp::CompletionResponse::Array(vec![
303 lsp::CompletionItem {
304 label: "first".to_string(),
305 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
306 range: lsp::Range::new(position, position),
307 new_text: "first".to_string(),
308 })),
309 ..Default::default()
310 },
311 lsp::CompletionItem {
312 label: "second".to_string(),
313 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
314 range: lsp::Range::new(position, position),
315 new_text: "second".to_string(),
316 })),
317 ..Default::default()
318 },
319 ])))
320 });
321 cx.simulate_keystrokes("a .");
322 request.next().await;
323 cx.condition(|editor, _| editor.context_menu_visible())
324 .await;
325 cx.simulate_keystrokes("down enter ! escape");
326
327 cx.assert_state(
328 indoc! {"
329 one.secondˇ!
330 two
331 three
332 "},
333 Mode::Normal,
334 );
335 cx.simulate_keystrokes("j .");
336 cx.assert_state(
337 indoc! {"
338 one.second!
339 two.secondˇ!
340 three
341 "},
342 Mode::Normal,
343 );
344 }
345
346 #[gpui::test]
347 async fn test_repeat_visual(cx: &mut gpui::TestAppContext) {
348 let mut cx = NeovimBackedTestContext::new(cx).await;
349
350 // single-line (3 columns)
351 cx.set_shared_state(indoc! {
352 "ˇthe quick brown
353 fox jumps over
354 the lazy dog"
355 })
356 .await;
357 cx.simulate_shared_keystrokes("v i w s o escape").await;
358 cx.shared_state().await.assert_eq(indoc! {
359 "ˇo quick brown
360 fox jumps over
361 the lazy dog"
362 });
363 cx.simulate_shared_keystrokes("j w .").await;
364 cx.shared_state().await.assert_eq(indoc! {
365 "o quick brown
366 fox ˇops over
367 the lazy dog"
368 });
369 cx.simulate_shared_keystrokes("f r .").await;
370 cx.shared_state().await.assert_eq(indoc! {
371 "o quick brown
372 fox ops oveˇothe lazy dog"
373 });
374
375 // visual
376 cx.set_shared_state(indoc! {
377 "the ˇquick brown
378 fox jumps over
379 fox jumps over
380 fox jumps over
381 the lazy dog"
382 })
383 .await;
384 cx.simulate_shared_keystrokes("v j x").await;
385 cx.shared_state().await.assert_eq(indoc! {
386 "the ˇumps over
387 fox jumps over
388 fox jumps over
389 the lazy dog"
390 });
391 cx.simulate_shared_keystrokes(".").await;
392 cx.shared_state().await.assert_eq(indoc! {
393 "the ˇumps over
394 fox jumps over
395 the lazy dog"
396 });
397 cx.simulate_shared_keystrokes("w .").await;
398 cx.shared_state().await.assert_eq(indoc! {
399 "the umps ˇumps over
400 the lazy dog"
401 });
402 cx.simulate_shared_keystrokes("j .").await;
403 cx.shared_state().await.assert_eq(indoc! {
404 "the umps umps over
405 the ˇog"
406 });
407
408 // block mode (3 rows)
409 cx.set_shared_state(indoc! {
410 "ˇthe quick brown
411 fox jumps over
412 the lazy dog"
413 })
414 .await;
415 cx.simulate_shared_keystrokes("ctrl-v j j shift-i o escape")
416 .await;
417 cx.shared_state().await.assert_eq(indoc! {
418 "ˇothe quick brown
419 ofox jumps over
420 othe lazy dog"
421 });
422 cx.simulate_shared_keystrokes("j 4 l .").await;
423 cx.shared_state().await.assert_eq(indoc! {
424 "othe quick brown
425 ofoxˇo jumps over
426 otheo lazy dog"
427 });
428
429 // line mode
430 cx.set_shared_state(indoc! {
431 "ˇthe quick brown
432 fox jumps over
433 the lazy dog"
434 })
435 .await;
436 cx.simulate_shared_keystrokes("shift-v shift-r o escape")
437 .await;
438 cx.shared_state().await.assert_eq(indoc! {
439 "ˇo
440 fox jumps over
441 the lazy dog"
442 });
443 cx.simulate_shared_keystrokes("j .").await;
444 cx.shared_state().await.assert_eq(indoc! {
445 "o
446 ˇo
447 the lazy dog"
448 });
449 }
450
451 #[gpui::test]
452 async fn test_repeat_motion_counts(cx: &mut gpui::TestAppContext) {
453 let mut cx = NeovimBackedTestContext::new(cx).await;
454
455 cx.set_shared_state(indoc! {
456 "ˇthe quick brown
457 fox jumps over
458 the lazy dog"
459 })
460 .await;
461 cx.simulate_shared_keystrokes("3 d 3 l").await;
462 cx.shared_state().await.assert_eq(indoc! {
463 "ˇ brown
464 fox jumps over
465 the lazy dog"
466 });
467 cx.simulate_shared_keystrokes("j .").await;
468 cx.shared_state().await.assert_eq(indoc! {
469 " brown
470 ˇ over
471 the lazy dog"
472 });
473 cx.simulate_shared_keystrokes("j 2 .").await;
474 cx.shared_state().await.assert_eq(indoc! {
475 " brown
476 over
477 ˇe lazy dog"
478 });
479 }
480
481 #[gpui::test]
482 async fn test_record_interrupted(cx: &mut gpui::TestAppContext) {
483 let mut cx = VimTestContext::new(cx, true).await;
484
485 cx.set_state("ˇhello\n", Mode::Normal);
486 cx.simulate_keystrokes("4 i j cmd-shift-p escape");
487 cx.simulate_keystrokes("escape");
488 cx.assert_state("ˇjhello\n", Mode::Normal);
489 }
490
491 #[gpui::test]
492 async fn test_repeat_over_blur(cx: &mut gpui::TestAppContext) {
493 let mut cx = NeovimBackedTestContext::new(cx).await;
494
495 cx.set_shared_state("ˇhello hello hello\n").await;
496 cx.simulate_shared_keystrokes("c f o x escape").await;
497 cx.shared_state().await.assert_eq("ˇx hello hello\n");
498 cx.simulate_shared_keystrokes(": escape").await;
499 cx.simulate_shared_keystrokes(".").await;
500 cx.shared_state().await.assert_eq("ˇx hello\n");
501 }
502
503 #[gpui::test]
504 async fn test_undo_repeated_insert(cx: &mut gpui::TestAppContext) {
505 let mut cx = NeovimBackedTestContext::new(cx).await;
506
507 cx.set_shared_state("hellˇo").await;
508 cx.simulate_shared_keystrokes("3 a . escape").await;
509 cx.shared_state().await.assert_eq("hello..ˇ.");
510 cx.simulate_shared_keystrokes("u").await;
511 cx.shared_state().await.assert_eq("hellˇo");
512 }
513}