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}