1use crate::{
2 motion::{self},
3 state::Mode,
4 Vim,
5};
6use editor::{display_map::ToDisplayPoint, Bias, ToPoint};
7use gpui::{actions, ViewContext, WindowContext};
8use language::{AutoindentMode, Point};
9use std::ops::Range;
10use std::sync::Arc;
11use workspace::Workspace;
12
13actions!(vim, [ToggleReplace, UndoReplace]);
14
15pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
16 workspace.register_action(|_, _: &ToggleReplace, cx: &mut ViewContext<Workspace>| {
17 Vim::update(cx, |vim, cx| {
18 vim.update_state(|state| state.replacements = vec![]);
19 vim.switch_mode(Mode::Replace, false, cx);
20 });
21 });
22
23 workspace.register_action(|_, _: &UndoReplace, cx: &mut ViewContext<Workspace>| {
24 Vim::update(cx, |vim, cx| {
25 if vim.state().mode != Mode::Replace {
26 return;
27 }
28 let count = vim.take_count(cx);
29 undo_replace(vim, count, cx)
30 });
31 });
32}
33
34pub(crate) fn multi_replace(text: Arc<str>, cx: &mut WindowContext) {
35 Vim::update(cx, |vim, cx| {
36 vim.update_active_editor(cx, |vim, editor, cx| {
37 editor.transact(cx, |editor, cx| {
38 editor.set_clip_at_line_ends(false, cx);
39 let map = editor.snapshot(cx);
40 let display_selections = editor.selections.all::<Point>(cx);
41
42 // Handles all string that require manipulation, including inserts and replaces
43 let edits = display_selections
44 .into_iter()
45 .map(|selection| {
46 let is_new_line = text.as_ref() == "\n";
47 let mut range = selection.range();
48 // "\n" need to be handled separately, because when a "\n" is typing,
49 // we don't do a replace, we need insert a "\n"
50 if !is_new_line {
51 range.end.column += 1;
52 range.end = map.buffer_snapshot.clip_point(range.end, Bias::Right);
53 }
54 let replace_range = map.buffer_snapshot.anchor_before(range.start)
55 ..map.buffer_snapshot.anchor_after(range.end);
56 let current_text = map
57 .buffer_snapshot
58 .text_for_range(replace_range.clone())
59 .collect();
60 vim.update_state(|state| {
61 state
62 .replacements
63 .push((replace_range.clone(), current_text))
64 });
65 (replace_range, text.clone())
66 })
67 .collect::<Vec<_>>();
68
69 editor.buffer().update(cx, |buffer, cx| {
70 buffer.edit(
71 edits.clone(),
72 Some(AutoindentMode::Block {
73 original_indent_columns: Vec::new(),
74 }),
75 cx,
76 );
77 });
78
79 editor.change_selections(None, cx, |s| {
80 s.select_anchor_ranges(edits.iter().map(|(range, _)| range.end..range.end));
81 });
82 editor.set_clip_at_line_ends(true, cx);
83 });
84 });
85 });
86}
87
88fn undo_replace(vim: &mut Vim, maybe_times: Option<usize>, cx: &mut WindowContext) {
89 vim.update_active_editor(cx, |vim, editor, cx| {
90 editor.transact(cx, |editor, cx| {
91 editor.set_clip_at_line_ends(false, cx);
92 let map = editor.snapshot(cx);
93 let selections = editor.selections.all::<Point>(cx);
94 let mut new_selections = vec![];
95 let edits: Vec<(Range<Point>, String)> = selections
96 .into_iter()
97 .filter_map(|selection| {
98 let end = selection.head();
99 let start = motion::backspace(
100 &map,
101 end.to_display_point(&map),
102 maybe_times.unwrap_or(1),
103 )
104 .to_point(&map);
105 new_selections.push(
106 map.buffer_snapshot.anchor_before(start)
107 ..map.buffer_snapshot.anchor_before(start),
108 );
109
110 let mut undo = None;
111 let edit_range = start..end;
112 for (i, (range, inverse)) in vim.state().replacements.iter().rev().enumerate() {
113 if range.start.to_point(&map.buffer_snapshot) <= edit_range.start
114 && range.end.to_point(&map.buffer_snapshot) >= edit_range.end
115 {
116 undo = Some(inverse.clone());
117 vim.update_state(|state| {
118 state.replacements.remove(state.replacements.len() - i - 1);
119 });
120 break;
121 }
122 }
123 Some((edit_range, undo?))
124 })
125 .collect::<Vec<_>>();
126
127 editor.buffer().update(cx, |buffer, cx| {
128 buffer.edit(edits, None, cx);
129 });
130
131 editor.change_selections(None, cx, |s| {
132 s.select_ranges(new_selections);
133 });
134 editor.set_clip_at_line_ends(true, cx);
135 });
136 });
137}
138
139#[cfg(test)]
140mod test {
141 use indoc::indoc;
142
143 use crate::{
144 state::Mode,
145 test::{NeovimBackedTestContext, VimTestContext},
146 };
147
148 #[gpui::test]
149 async fn test_enter_and_exit_replace_mode(cx: &mut gpui::TestAppContext) {
150 let mut cx = VimTestContext::new(cx, true).await;
151 cx.simulate_keystroke("shift-r");
152 assert_eq!(cx.mode(), Mode::Replace);
153 cx.simulate_keystroke("escape");
154 assert_eq!(cx.mode(), Mode::Normal);
155 }
156
157 #[gpui::test]
158 async fn test_replace_mode(cx: &mut gpui::TestAppContext) {
159 let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
160
161 // test normal replace
162 cx.set_shared_state(indoc! {"
163 ˇThe quick brown
164 fox jumps over
165 the lazy dog."})
166 .await;
167 cx.simulate_shared_keystrokes(["shift-r", "O", "n", "e"])
168 .await;
169 cx.assert_shared_state(indoc! {"
170 Oneˇ quick brown
171 fox jumps over
172 the lazy dog."})
173 .await;
174 assert_eq!(Mode::Replace, cx.neovim_mode().await);
175
176 // test replace with line ending
177 cx.set_shared_state(indoc! {"
178 The quick browˇn
179 fox jumps over
180 the lazy dog."})
181 .await;
182 cx.simulate_shared_keystrokes(["shift-r", "O", "n", "e"])
183 .await;
184 cx.assert_shared_state(indoc! {"
185 The quick browOneˇ
186 fox jumps over
187 the lazy dog."})
188 .await;
189
190 // test replace with blank line
191 cx.set_shared_state(indoc! {"
192 The quick brown
193 ˇ
194 fox jumps over
195 the lazy dog."})
196 .await;
197 cx.simulate_shared_keystrokes(["shift-r", "O", "n", "e"])
198 .await;
199 cx.assert_shared_state(indoc! {"
200 The quick brown
201 Oneˇ
202 fox jumps over
203 the lazy dog."})
204 .await;
205
206 // test replace with multi cursor
207 cx.set_shared_state(indoc! {"
208 ˇThe quick brown
209 fox jumps over
210 the lazy ˇdog."})
211 .await;
212 cx.simulate_shared_keystrokes(["shift-r", "O", "n", "e"])
213 .await;
214 cx.assert_shared_state(indoc! {"
215 Oneˇ quick brown
216 fox jumps over
217 the lazy Oneˇ."})
218 .await;
219
220 // test replace with newline
221 cx.set_shared_state(indoc! {"
222 The quˇick brown
223 fox jumps over
224 the lazy dog."})
225 .await;
226 cx.simulate_shared_keystrokes(["shift-r", "enter", "O", "n", "e"])
227 .await;
228 cx.assert_shared_state(indoc! {"
229 The qu
230 Oneˇ brown
231 fox jumps over
232 the lazy dog."})
233 .await;
234
235 // test replace with multi cursor and newline
236 cx.set_shared_state(indoc! {"
237 ˇThe quick brown
238 fox jumps over
239 the lazy ˇdog."})
240 .await;
241 cx.simulate_shared_keystrokes(["shift-r", "O", "n", "e"])
242 .await;
243 cx.assert_shared_state(indoc! {"
244 Oneˇ quick brown
245 fox jumps over
246 the lazy Oneˇ."})
247 .await;
248 cx.simulate_shared_keystrokes(["enter", "T", "w", "o"])
249 .await;
250 cx.assert_shared_state(indoc! {"
251 One
252 Twoˇck brown
253 fox jumps over
254 the lazy One
255 Twoˇ"})
256 .await;
257 }
258
259 #[gpui::test]
260 async fn test_replace_mode_undo(cx: &mut gpui::TestAppContext) {
261 let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
262
263 const UNDO_REPLACE_EXAMPLES: &[&'static str] = &[
264 // replace undo with single line
265 "ˇThe quick brown fox jumps over the lazy dog.",
266 // replace undo with ending line
267 indoc! {"
268 The quick browˇn
269 fox jumps over
270 the lazy dog."
271 },
272 // replace undo with empty line
273 indoc! {"
274 The quick brown
275 ˇ
276 fox jumps over
277 the lazy dog."
278 },
279 // replace undo with multi cursor
280 indoc! {"
281 The quick browˇn
282 fox jumps over
283 the lazy ˇdog."
284 },
285 ];
286
287 for example in UNDO_REPLACE_EXAMPLES {
288 // normal undo
289 cx.assert_binding_matches(
290 [
291 "shift-r",
292 "O",
293 "n",
294 "e",
295 "backspace",
296 "backspace",
297 "backspace",
298 ],
299 example,
300 )
301 .await;
302 // undo with new line
303 cx.assert_binding_matches(
304 [
305 "shift-r",
306 "O",
307 "enter",
308 "e",
309 "backspace",
310 "backspace",
311 "backspace",
312 ],
313 example,
314 )
315 .await;
316 cx.assert_binding_matches(
317 [
318 "shift-r",
319 "O",
320 "enter",
321 "n",
322 "enter",
323 "e",
324 "backspace",
325 "backspace",
326 "backspace",
327 "backspace",
328 "backspace",
329 ],
330 example,
331 )
332 .await;
333 }
334 }
335
336 #[gpui::test]
337 async fn test_replace_multicursor(cx: &mut gpui::TestAppContext) {
338 let mut cx = VimTestContext::new(cx, true).await;
339 cx.set_state("ˇabcˇabcabc", Mode::Normal);
340 cx.simulate_keystrokes(["shift-r", "1", "2", "3", "4"]);
341 cx.assert_state("1234ˇ234ˇbc", Mode::Replace);
342 assert_eq!(cx.mode(), Mode::Replace);
343 cx.simulate_keystrokes([
344 "backspace",
345 "backspace",
346 "backspace",
347 "backspace",
348 "backspace",
349 ]);
350 cx.assert_state("ˇabˇcabcabc", Mode::Replace);
351 }
352
353 #[gpui::test]
354 async fn test_replace_undo(cx: &mut gpui::TestAppContext) {
355 let mut cx = VimTestContext::new(cx, true).await;
356
357 cx.set_state("ˇaaaa", Mode::Normal);
358 cx.simulate_keystrokes(["0", "shift-r", "b", "b", "b", "escape", "u"]);
359 cx.assert_state("ˇaaaa", Mode::Normal);
360 }
361}