repeat.rs

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