repeat.rs

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