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