repeat.rs

  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::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 {
 35                None
 36            }
 37        }
 38        ReplayableAction::Insertion { .. } => None,
 39    }
 40}
 41
 42pub(crate) fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
 43    workspace.register_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    workspace.register_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 NormalBefore.partial_eq(&**action) {
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_handle();
170    cx.spawn(move |mut cx| async move {
171        editor.update(&mut cx, |editor, _| {
172            editor.show_local_selections = false;
173        })?;
174        for action in actions {
175            match action {
176                ReplayableAction::Action(action) => {
177                    if should_replay(&action) {
178                        window.update(&mut cx, |_, cx| cx.dispatch_action(action))
179                    } else {
180                        Ok(())
181                    }
182                }
183                ReplayableAction::Insertion {
184                    text,
185                    utf16_range_to_replace,
186                } => editor.update(&mut cx, |editor, cx| {
187                    editor.replay_insert_event(&text, utf16_range_to_replace.clone(), cx)
188                }),
189            }?
190        }
191        editor.update(&mut cx, |editor, _| {
192            editor.show_local_selections = true;
193        })?;
194        window.update(&mut cx, |_, cx| cx.dispatch_action(EndRepeat.boxed_clone()))
195    })
196    .detach_and_log_err(cx);
197}
198
199// #[cfg(test)]
200// mod test {
201//     use std::sync::Arc;
202
203//     use editor::test::editor_lsp_test_context::EditorLspTestContext;
204//     use futures::StreamExt;
205//     use indoc::indoc;
206
207//     use gpui::{executor::Deterministic, View};
208
209//     use crate::{
210//         state::Mode,
211//         test::{NeovimBackedTestContext, VimTestContext},
212//     };
213
214//     #[gpui::test]
215//     async fn test_dot_repeat(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
216//         let mut cx = NeovimBackedTestContext::new(cx).await;
217
218//         // "o"
219//         cx.set_shared_state("ˇhello").await;
220//         cx.simulate_shared_keystrokes(["o", "w", "o", "r", "l", "d", "escape"])
221//             .await;
222//         cx.assert_shared_state("hello\nworlˇd").await;
223//         cx.simulate_shared_keystrokes(["."]).await;
224//         deterministic.run_until_parked();
225//         cx.assert_shared_state("hello\nworld\nworlˇd").await;
226
227//         // "d"
228//         cx.simulate_shared_keystrokes(["^", "d", "f", "o"]).await;
229//         cx.simulate_shared_keystrokes(["g", "g", "."]).await;
230//         deterministic.run_until_parked();
231//         cx.assert_shared_state("ˇ\nworld\nrld").await;
232
233//         // "p" (note that it pastes the current clipboard)
234//         cx.simulate_shared_keystrokes(["j", "y", "y", "p"]).await;
235//         cx.simulate_shared_keystrokes(["shift-g", "y", "y", "."])
236//             .await;
237//         deterministic.run_until_parked();
238//         cx.assert_shared_state("\nworld\nworld\nrld\nˇrld").await;
239
240//         // "~" (note that counts apply to the action taken, not . itself)
241//         cx.set_shared_state("ˇthe quick brown fox").await;
242//         cx.simulate_shared_keystrokes(["2", "~", "."]).await;
243//         deterministic.run_until_parked();
244//         cx.set_shared_state("THE ˇquick brown fox").await;
245//         cx.simulate_shared_keystrokes(["3", "."]).await;
246//         deterministic.run_until_parked();
247//         cx.set_shared_state("THE QUIˇck brown fox").await;
248//         deterministic.run_until_parked();
249//         cx.simulate_shared_keystrokes(["."]).await;
250//         deterministic.run_until_parked();
251//         cx.assert_shared_state("THE QUICK ˇbrown fox").await;
252//     }
253
254//     #[gpui::test]
255//     async fn test_repeat_ime(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
256//         let mut cx = VimTestContext::new(cx, true).await;
257
258//         cx.set_state("hˇllo", Mode::Normal);
259//         cx.simulate_keystrokes(["i"]);
260
261//         // simulate brazilian input for ä.
262//         cx.update_editor(|editor, cx| {
263//             editor.replace_and_mark_text_in_range(None, "\"", Some(1..1), cx);
264//             editor.replace_text_in_range(None, "ä", cx);
265//         });
266//         cx.simulate_keystrokes(["escape"]);
267//         cx.assert_state("hˇällo", Mode::Normal);
268//         cx.simulate_keystrokes(["."]);
269//         deterministic.run_until_parked();
270//         cx.assert_state("hˇäällo", Mode::Normal);
271//     }
272
273//     #[gpui::test]
274//     async fn test_repeat_completion(
275//         deterministic: Arc<Deterministic>,
276//         cx: &mut gpui::TestAppContext,
277//     ) {
278//         let cx = EditorLspTestContext::new_rust(
279//             lsp::ServerCapabilities {
280//                 completion_provider: Some(lsp::CompletionOptions {
281//                     trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
282//                     resolve_provider: Some(true),
283//                     ..Default::default()
284//                 }),
285//                 ..Default::default()
286//             },
287//             cx,
288//         )
289//         .await;
290//         let mut cx = VimTestContext::new_with_lsp(cx, true);
291
292//         cx.set_state(
293//             indoc! {"
294//             onˇe
295//             two
296//             three
297//         "},
298//             Mode::Normal,
299//         );
300
301//         let mut request =
302//             cx.handle_request::<lsp::request::Completion, _, _>(move |_, params, _| async move {
303//                 let position = params.text_document_position.position;
304//                 Ok(Some(lsp::CompletionResponse::Array(vec![
305//                     lsp::CompletionItem {
306//                         label: "first".to_string(),
307//                         text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
308//                             range: lsp::Range::new(position.clone(), position.clone()),
309//                             new_text: "first".to_string(),
310//                         })),
311//                         ..Default::default()
312//                     },
313//                     lsp::CompletionItem {
314//                         label: "second".to_string(),
315//                         text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
316//                             range: lsp::Range::new(position.clone(), position.clone()),
317//                             new_text: "second".to_string(),
318//                         })),
319//                         ..Default::default()
320//                     },
321//                 ])))
322//             });
323//         cx.simulate_keystrokes(["a", "."]);
324//         request.next().await;
325//         cx.condition(|editor, _| editor.context_menu_visible())
326//             .await;
327//         cx.simulate_keystrokes(["down", "enter", "!", "escape"]);
328
329//         cx.assert_state(
330//             indoc! {"
331//                 one.secondˇ!
332//                 two
333//                 three
334//             "},
335//             Mode::Normal,
336//         );
337//         cx.simulate_keystrokes(["j", "."]);
338//         deterministic.run_until_parked();
339//         cx.assert_state(
340//             indoc! {"
341//                 one.second!
342//                 two.secondˇ!
343//                 three
344//             "},
345//             Mode::Normal,
346//         );
347//     }
348
349//     #[gpui::test]
350//     async fn test_repeat_visual(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
351//         let mut cx = NeovimBackedTestContext::new(cx).await;
352
353//         // single-line (3 columns)
354//         cx.set_shared_state(indoc! {
355//             "ˇthe quick brown
356//             fox jumps over
357//             the lazy dog"
358//         })
359//         .await;
360//         cx.simulate_shared_keystrokes(["v", "i", "w", "s", "o", "escape"])
361//             .await;
362//         cx.assert_shared_state(indoc! {
363//             "ˇo quick brown
364//             fox jumps over
365//             the lazy dog"
366//         })
367//         .await;
368//         cx.simulate_shared_keystrokes(["j", "w", "."]).await;
369//         deterministic.run_until_parked();
370//         cx.assert_shared_state(indoc! {
371//             "o quick brown
372//             fox ˇops over
373//             the lazy dog"
374//         })
375//         .await;
376//         cx.simulate_shared_keystrokes(["f", "r", "."]).await;
377//         deterministic.run_until_parked();
378//         cx.assert_shared_state(indoc! {
379//             "o quick brown
380//             fox ops oveˇothe lazy dog"
381//         })
382//         .await;
383
384//         // visual
385//         cx.set_shared_state(indoc! {
386//             "the ˇquick brown
387//             fox jumps over
388//             fox jumps over
389//             fox jumps over
390//             the lazy dog"
391//         })
392//         .await;
393//         cx.simulate_shared_keystrokes(["v", "j", "x"]).await;
394//         cx.assert_shared_state(indoc! {
395//             "the ˇumps over
396//             fox jumps over
397//             fox jumps over
398//             the lazy dog"
399//         })
400//         .await;
401//         cx.simulate_shared_keystrokes(["."]).await;
402//         deterministic.run_until_parked();
403//         cx.assert_shared_state(indoc! {
404//             "the ˇumps over
405//             fox jumps over
406//             the lazy dog"
407//         })
408//         .await;
409//         cx.simulate_shared_keystrokes(["w", "."]).await;
410//         deterministic.run_until_parked();
411//         cx.assert_shared_state(indoc! {
412//             "the umps ˇumps over
413//             the lazy dog"
414//         })
415//         .await;
416//         cx.simulate_shared_keystrokes(["j", "."]).await;
417//         deterministic.run_until_parked();
418//         cx.assert_shared_state(indoc! {
419//             "the umps umps over
420//             the ˇog"
421//         })
422//         .await;
423
424//         // block mode (3 rows)
425//         cx.set_shared_state(indoc! {
426//             "ˇthe quick brown
427//             fox jumps over
428//             the lazy dog"
429//         })
430//         .await;
431//         cx.simulate_shared_keystrokes(["ctrl-v", "j", "j", "shift-i", "o", "escape"])
432//             .await;
433//         cx.assert_shared_state(indoc! {
434//             "ˇothe quick brown
435//             ofox jumps over
436//             othe lazy dog"
437//         })
438//         .await;
439//         cx.simulate_shared_keystrokes(["j", "4", "l", "."]).await;
440//         deterministic.run_until_parked();
441//         cx.assert_shared_state(indoc! {
442//             "othe quick brown
443//             ofoxˇo jumps over
444//             otheo lazy dog"
445//         })
446//         .await;
447
448//         // line mode
449//         cx.set_shared_state(indoc! {
450//             "ˇthe quick brown
451//             fox jumps over
452//             the lazy dog"
453//         })
454//         .await;
455//         cx.simulate_shared_keystrokes(["shift-v", "shift-r", "o", "escape"])
456//             .await;
457//         cx.assert_shared_state(indoc! {
458//             "ˇo
459//             fox jumps over
460//             the lazy dog"
461//         })
462//         .await;
463//         cx.simulate_shared_keystrokes(["j", "."]).await;
464//         deterministic.run_until_parked();
465//         cx.assert_shared_state(indoc! {
466//             "o
467//             ˇo
468//             the lazy dog"
469//         })
470//         .await;
471//     }
472
473//     #[gpui::test]
474//     async fn test_repeat_motion_counts(
475//         deterministic: Arc<Deterministic>,
476//         cx: &mut gpui::TestAppContext,
477//     ) {
478//         let mut cx = NeovimBackedTestContext::new(cx).await;
479
480//         cx.set_shared_state(indoc! {
481//             "ˇthe quick brown
482//             fox jumps over
483//             the lazy dog"
484//         })
485//         .await;
486//         cx.simulate_shared_keystrokes(["3", "d", "3", "l"]).await;
487//         cx.assert_shared_state(indoc! {
488//             "ˇ brown
489//             fox jumps over
490//             the lazy dog"
491//         })
492//         .await;
493//         cx.simulate_shared_keystrokes(["j", "."]).await;
494//         deterministic.run_until_parked();
495//         cx.assert_shared_state(indoc! {
496//             " brown
497//             ˇ over
498//             the lazy dog"
499//         })
500//         .await;
501//         cx.simulate_shared_keystrokes(["j", "2", "."]).await;
502//         deterministic.run_until_parked();
503//         cx.assert_shared_state(indoc! {
504//             " brown
505//              over
506//             ˇe lazy dog"
507//         })
508//         .await;
509//     }
510
511//     #[gpui::test]
512//     async fn test_record_interrupted(
513//         deterministic: Arc<Deterministic>,
514//         cx: &mut gpui::TestAppContext,
515//     ) {
516//         let mut cx = VimTestContext::new(cx, true).await;
517
518//         cx.set_state("ˇhello\n", Mode::Normal);
519//         cx.simulate_keystrokes(["4", "i", "j", "cmd-shift-p", "escape", "escape"]);
520//         deterministic.run_until_parked();
521//         cx.assert_state("ˇjhello\n", Mode::Normal);
522//     }
523// }