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