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}