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