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