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, 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}