repeat.rs

  1use crate::{
  2    state::{Mode, ReplayableAction},
  3    Vim,
  4};
  5use gpui::{actions, AppContext};
  6use workspace::Workspace;
  7
  8actions!(vim, [Repeat, EndRepeat,]);
  9
 10pub(crate) fn init(cx: &mut AppContext) {
 11    cx.add_action(|_: &mut Workspace, _: &EndRepeat, cx| {
 12        Vim::update(cx, |vim, cx| {
 13            vim.workspace_state.replaying = false;
 14            vim.switch_mode(Mode::Normal, false, cx)
 15        });
 16    });
 17
 18    cx.add_action(|_: &mut Workspace, _: &Repeat, cx| {
 19        Vim::update(cx, |vim, cx| {
 20            let actions = vim.workspace_state.repeat_actions.clone();
 21            let Some(editor) = vim.active_editor.clone() else {
 22                return;
 23            };
 24            if let Some(new_count) = vim.pop_number_operator(cx) {
 25                vim.workspace_state.recorded_count = Some(new_count);
 26            }
 27            vim.workspace_state.replaying = true;
 28
 29            let window = cx.window();
 30            cx.app_context()
 31                .spawn(move |mut cx| async move {
 32                    for action in actions {
 33                        match action {
 34                            ReplayableAction::Action(action) => window
 35                                .dispatch_action(editor.id(), action.as_ref(), &mut cx)
 36                                .ok_or_else(|| anyhow::anyhow!("window was closed")),
 37                            ReplayableAction::Insertion {
 38                                text,
 39                                utf16_range_to_replace,
 40                            } => editor.update(&mut cx, |editor, cx| {
 41                                editor.replay_insert_event(
 42                                    &text,
 43                                    utf16_range_to_replace.clone(),
 44                                    cx,
 45                                )
 46                            }),
 47                        }?
 48                    }
 49                    window
 50                        .dispatch_action(editor.id(), &EndRepeat, &mut cx)
 51                        .ok_or_else(|| anyhow::anyhow!("window was closed"))
 52                })
 53                .detach_and_log_err(cx);
 54        });
 55    });
 56}
 57
 58#[cfg(test)]
 59mod test {
 60    use std::sync::Arc;
 61
 62    use editor::test::editor_lsp_test_context::EditorLspTestContext;
 63    use futures::StreamExt;
 64    use indoc::indoc;
 65
 66    use gpui::{executor::Deterministic, View};
 67
 68    use crate::{
 69        state::Mode,
 70        test::{NeovimBackedTestContext, VimTestContext},
 71    };
 72
 73    #[gpui::test]
 74    async fn test_dot_repeat(cx: &mut gpui::TestAppContext) {
 75        let mut cx = NeovimBackedTestContext::new(cx).await;
 76
 77        // "o"
 78        cx.set_shared_state("ˇhello").await;
 79        cx.simulate_shared_keystrokes(["o", "w", "o", "r", "l", "d", "escape"])
 80            .await;
 81        cx.assert_shared_state("hello\nworlˇd").await;
 82        cx.simulate_shared_keystrokes(["."]).await;
 83        cx.assert_shared_state("hello\nworld\nworlˇd").await;
 84
 85        // "d"
 86        cx.simulate_shared_keystrokes(["^", "d", "f", "o"]).await;
 87        cx.simulate_shared_keystrokes(["g", "g", "."]).await;
 88        cx.assert_shared_state("ˇ\nworld\nrld").await;
 89
 90        // "p" (note that it pastes the current clipboard)
 91        cx.simulate_shared_keystrokes(["j", "y", "y", "p"]).await;
 92        cx.simulate_shared_keystrokes(["shift-g", "y", "y", "."])
 93            .await;
 94        cx.assert_shared_state("\nworld\nworld\nrld\nˇrld").await;
 95
 96        // "~" (note that counts apply to the action taken, not . itself)
 97        cx.set_shared_state("ˇthe quick brown fox").await;
 98        cx.simulate_shared_keystrokes(["2", "~", "."]).await;
 99        cx.set_shared_state("THE ˇquick brown fox").await;
100        cx.simulate_shared_keystrokes(["3", "."]).await;
101        cx.set_shared_state("THE QUIˇck brown fox").await;
102        cx.simulate_shared_keystrokes(["."]).await;
103        cx.set_shared_state("THE QUICK ˇbrown fox").await;
104    }
105
106    #[gpui::test]
107    async fn test_repeat_ime(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
108        let mut cx = VimTestContext::new(cx, true).await;
109
110        cx.set_state("hˇllo", Mode::Normal);
111        cx.simulate_keystrokes(["i"]);
112
113        // simulate brazilian input for ä.
114        cx.update_editor(|editor, cx| {
115            editor.replace_and_mark_text_in_range(None, "\"", Some(1..1), cx);
116            editor.replace_text_in_range(None, "ä", cx);
117        });
118        cx.simulate_keystrokes(["escape"]);
119        cx.assert_state("hˇällo", Mode::Normal);
120        cx.simulate_keystrokes(["."]);
121        deterministic.run_until_parked();
122        cx.assert_state("hˇäällo", Mode::Normal);
123    }
124
125    #[gpui::test]
126    async fn test_repeat_completion(
127        deterministic: Arc<Deterministic>,
128        cx: &mut gpui::TestAppContext,
129    ) {
130        let cx = EditorLspTestContext::new_rust(
131            lsp::ServerCapabilities {
132                completion_provider: Some(lsp::CompletionOptions {
133                    trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
134                    resolve_provider: Some(true),
135                    ..Default::default()
136                }),
137                ..Default::default()
138            },
139            cx,
140        )
141        .await;
142        let mut cx = VimTestContext::new_with_lsp(cx, true);
143
144        cx.set_state(
145            indoc! {"
146            onˇe
147            two
148            three
149        "},
150            Mode::Normal,
151        );
152
153        let mut request =
154            cx.handle_request::<lsp::request::Completion, _, _>(move |_, params, _| async move {
155                let position = params.text_document_position.position;
156                Ok(Some(lsp::CompletionResponse::Array(vec![
157                    lsp::CompletionItem {
158                        label: "first".to_string(),
159                        text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
160                            range: lsp::Range::new(position.clone(), position.clone()),
161                            new_text: "first".to_string(),
162                        })),
163                        ..Default::default()
164                    },
165                    lsp::CompletionItem {
166                        label: "second".to_string(),
167                        text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
168                            range: lsp::Range::new(position.clone(), position.clone()),
169                            new_text: "second".to_string(),
170                        })),
171                        ..Default::default()
172                    },
173                ])))
174            });
175        cx.simulate_keystrokes(["a", "."]);
176        request.next().await;
177        cx.condition(|editor, _| editor.context_menu_visible())
178            .await;
179        cx.simulate_keystrokes(["down", "enter", "!", "escape"]);
180
181        cx.assert_state(
182            indoc! {"
183                one.secondˇ!
184                two
185                three
186            "},
187            Mode::Normal,
188        );
189        cx.simulate_keystrokes(["j", "."]);
190        deterministic.run_until_parked();
191        cx.assert_state(
192            indoc! {"
193                one.second!
194                two.secondˇ!
195                three
196            "},
197            Mode::Normal,
198        );
199    }
200}