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