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(deterministic: Arc<Deterministic>, 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        deterministic.run_until_parked();
 84        cx.assert_shared_state("hello\nworld\nworlˇd").await;
 85
 86        // "d"
 87        cx.simulate_shared_keystrokes(["^", "d", "f", "o"]).await;
 88        cx.simulate_shared_keystrokes(["g", "g", "."]).await;
 89        deterministic.run_until_parked();
 90        cx.assert_shared_state("ˇ\nworld\nrld").await;
 91
 92        // "p" (note that it pastes the current clipboard)
 93        cx.simulate_shared_keystrokes(["j", "y", "y", "p"]).await;
 94        cx.simulate_shared_keystrokes(["shift-g", "y", "y", "."])
 95            .await;
 96        deterministic.run_until_parked();
 97        cx.assert_shared_state("\nworld\nworld\nrld\nˇrld").await;
 98
 99        // "~" (note that counts apply to the action taken, not . itself)
100        cx.set_shared_state("ˇthe quick brown fox").await;
101        cx.simulate_shared_keystrokes(["2", "~", "."]).await;
102        deterministic.run_until_parked();
103        cx.set_shared_state("THE ˇquick brown fox").await;
104        cx.simulate_shared_keystrokes(["3", "."]).await;
105        deterministic.run_until_parked();
106        cx.set_shared_state("THE QUIˇck brown fox").await;
107        deterministic.run_until_parked();
108        cx.simulate_shared_keystrokes(["."]).await;
109        deterministic.run_until_parked();
110        cx.set_shared_state("THE QUICK ˇbrown fox").await;
111    }
112
113    #[gpui::test]
114    async fn test_repeat_ime(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
115        let mut cx = VimTestContext::new(cx, true).await;
116
117        cx.set_state("hˇllo", Mode::Normal);
118        cx.simulate_keystrokes(["i"]);
119
120        // simulate brazilian input for ä.
121        cx.update_editor(|editor, cx| {
122            editor.replace_and_mark_text_in_range(None, "\"", Some(1..1), cx);
123            editor.replace_text_in_range(None, "ä", cx);
124        });
125        cx.simulate_keystrokes(["escape"]);
126        cx.assert_state("hˇällo", Mode::Normal);
127        cx.simulate_keystrokes(["."]);
128        deterministic.run_until_parked();
129        cx.assert_state("hˇäällo", Mode::Normal);
130    }
131
132    #[gpui::test]
133    async fn test_repeat_completion(
134        deterministic: Arc<Deterministic>,
135        cx: &mut gpui::TestAppContext,
136    ) {
137        let cx = EditorLspTestContext::new_rust(
138            lsp::ServerCapabilities {
139                completion_provider: Some(lsp::CompletionOptions {
140                    trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
141                    resolve_provider: Some(true),
142                    ..Default::default()
143                }),
144                ..Default::default()
145            },
146            cx,
147        )
148        .await;
149        let mut cx = VimTestContext::new_with_lsp(cx, true);
150
151        cx.set_state(
152            indoc! {"
153            onˇe
154            two
155            three
156        "},
157            Mode::Normal,
158        );
159
160        let mut request =
161            cx.handle_request::<lsp::request::Completion, _, _>(move |_, params, _| async move {
162                let position = params.text_document_position.position;
163                Ok(Some(lsp::CompletionResponse::Array(vec![
164                    lsp::CompletionItem {
165                        label: "first".to_string(),
166                        text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
167                            range: lsp::Range::new(position.clone(), position.clone()),
168                            new_text: "first".to_string(),
169                        })),
170                        ..Default::default()
171                    },
172                    lsp::CompletionItem {
173                        label: "second".to_string(),
174                        text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
175                            range: lsp::Range::new(position.clone(), position.clone()),
176                            new_text: "second".to_string(),
177                        })),
178                        ..Default::default()
179                    },
180                ])))
181            });
182        cx.simulate_keystrokes(["a", "."]);
183        request.next().await;
184        cx.condition(|editor, _| editor.context_menu_visible())
185            .await;
186        cx.simulate_keystrokes(["down", "enter", "!", "escape"]);
187
188        cx.assert_state(
189            indoc! {"
190                one.secondˇ!
191                two
192                three
193            "},
194            Mode::Normal,
195        );
196        cx.simulate_keystrokes(["j", "."]);
197        deterministic.run_until_parked();
198        cx.assert_state(
199            indoc! {"
200                one.second!
201                two.secondˇ!
202                three
203            "},
204            Mode::Normal,
205        );
206    }
207}