1use crate::{
2 motion::{self, Motion},
3 object::Object,
4 state::Mode,
5 Vim,
6};
7use editor::{
8 display_map::ToDisplayPoint, scroll::Autoscroll, Anchor, Bias, Editor, EditorSnapshot,
9 ToOffset, ToPoint,
10};
11use gpui::{actions, Context, Window};
12use language::{Point, SelectionGoal};
13use std::ops::Range;
14use std::sync::Arc;
15
16actions!(vim, [ToggleReplace, UndoReplace]);
17
18pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
19 Vim::action(editor, cx, |vim, _: &ToggleReplace, window, cx| {
20 vim.replacements = vec![];
21 vim.start_recording(cx);
22 vim.switch_mode(Mode::Replace, false, window, cx);
23 });
24
25 Vim::action(editor, cx, |vim, _: &UndoReplace, window, cx| {
26 if vim.mode != Mode::Replace {
27 return;
28 }
29 let count = Vim::take_count(cx);
30 vim.undo_replace(count, window, cx)
31 });
32}
33
34struct VimExchange;
35
36impl Vim {
37 pub(crate) fn multi_replace(
38 &mut self,
39 text: Arc<str>,
40 window: &mut Window,
41 cx: &mut Context<Self>,
42 ) {
43 self.update_editor(window, cx, |vim, editor, window, cx| {
44 editor.transact(window, cx, |editor, window, cx| {
45 editor.set_clip_at_line_ends(false, cx);
46 let map = editor.snapshot(window, cx);
47 let display_selections = editor.selections.all::<Point>(cx);
48
49 // Handles all string that require manipulation, including inserts and replaces
50 let edits = display_selections
51 .into_iter()
52 .map(|selection| {
53 let is_new_line = text.as_ref() == "\n";
54 let mut range = selection.range();
55 // "\n" need to be handled separately, because when a "\n" is typing,
56 // we don't do a replace, we need insert a "\n"
57 if !is_new_line {
58 range.end.column += 1;
59 range.end = map.buffer_snapshot.clip_point(range.end, Bias::Right);
60 }
61 let replace_range = map.buffer_snapshot.anchor_before(range.start)
62 ..map.buffer_snapshot.anchor_after(range.end);
63 let current_text = map
64 .buffer_snapshot
65 .text_for_range(replace_range.clone())
66 .collect();
67 vim.replacements.push((replace_range.clone(), current_text));
68 (replace_range, text.clone())
69 })
70 .collect::<Vec<_>>();
71
72 editor.edit_with_block_indent(edits.clone(), Vec::new(), cx);
73
74 editor.change_selections(None, window, cx, |s| {
75 s.select_anchor_ranges(edits.iter().map(|(range, _)| range.end..range.end));
76 });
77 editor.set_clip_at_line_ends(true, cx);
78 });
79 });
80 }
81
82 fn undo_replace(
83 &mut self,
84 maybe_times: Option<usize>,
85 window: &mut Window,
86 cx: &mut Context<Self>,
87 ) {
88 self.update_editor(window, cx, |vim, editor, window, cx| {
89 editor.transact(window, cx, |editor, window, cx| {
90 editor.set_clip_at_line_ends(false, cx);
91 let map = editor.snapshot(window, cx);
92 let selections = editor.selections.all::<Point>(cx);
93 let mut new_selections = vec![];
94 let edits: Vec<(Range<Point>, String)> = selections
95 .into_iter()
96 .filter_map(|selection| {
97 let end = selection.head();
98 let start = motion::backspace(
99 &map,
100 end.to_display_point(&map),
101 maybe_times.unwrap_or(1),
102 )
103 .to_point(&map);
104 new_selections.push(
105 map.buffer_snapshot.anchor_before(start)
106 ..map.buffer_snapshot.anchor_before(start),
107 );
108
109 let mut undo = None;
110 let edit_range = start..end;
111 for (i, (range, inverse)) in vim.replacements.iter().rev().enumerate() {
112 if range.start.to_point(&map.buffer_snapshot) <= edit_range.start
113 && range.end.to_point(&map.buffer_snapshot) >= edit_range.end
114 {
115 undo = Some(inverse.clone());
116 vim.replacements.remove(vim.replacements.len() - i - 1);
117 break;
118 }
119 }
120 Some((edit_range, undo?))
121 })
122 .collect::<Vec<_>>();
123
124 editor.edit(edits, cx);
125
126 editor.change_selections(None, window, cx, |s| {
127 s.select_ranges(new_selections);
128 });
129 editor.set_clip_at_line_ends(true, cx);
130 });
131 });
132 }
133
134 pub fn exchange_object(
135 &mut self,
136 object: Object,
137 around: bool,
138 window: &mut Window,
139 cx: &mut Context<Self>,
140 ) {
141 self.stop_recording(cx);
142 self.update_editor(window, cx, |vim, editor, window, cx| {
143 editor.set_clip_at_line_ends(false, cx);
144 let mut selection = editor.selections.newest_display(cx);
145 let snapshot = editor.snapshot(window, cx);
146 object.expand_selection(&snapshot, &mut selection, around);
147 let start = snapshot
148 .buffer_snapshot
149 .anchor_before(selection.start.to_point(&snapshot));
150 let end = snapshot
151 .buffer_snapshot
152 .anchor_before(selection.end.to_point(&snapshot));
153 let new_range = start..end;
154 vim.exchange_impl(new_range, editor, &snapshot, window, cx);
155 editor.set_clip_at_line_ends(true, cx);
156 });
157 }
158
159 pub fn exchange_visual(&mut self, window: &mut Window, cx: &mut Context<Self>) {
160 self.stop_recording(cx);
161 self.update_editor(window, cx, |vim, editor, window, cx| {
162 let selection = editor.selections.newest_anchor();
163 let new_range = selection.start..selection.end;
164 let snapshot = editor.snapshot(window, cx);
165 vim.exchange_impl(new_range, editor, &snapshot, window, cx);
166 });
167 self.switch_mode(Mode::Normal, false, window, cx);
168 }
169
170 pub fn clear_exchange(&mut self, window: &mut Window, cx: &mut Context<Self>) {
171 self.stop_recording(cx);
172 self.update_editor(window, cx, |_, editor, _, cx| {
173 editor.clear_highlights::<VimExchange>(cx);
174 });
175 }
176
177 pub fn exchange_motion(
178 &mut self,
179 motion: Motion,
180 times: Option<usize>,
181 window: &mut Window,
182 cx: &mut Context<Self>,
183 ) {
184 self.stop_recording(cx);
185 self.update_editor(window, cx, |vim, editor, window, cx| {
186 editor.set_clip_at_line_ends(false, cx);
187 let text_layout_details = editor.text_layout_details(window);
188 let mut selection = editor.selections.newest_display(cx);
189 let snapshot = editor.snapshot(window, cx);
190 motion.expand_selection(
191 &snapshot,
192 &mut selection,
193 times,
194 false,
195 &text_layout_details,
196 );
197 let start = snapshot
198 .buffer_snapshot
199 .anchor_before(selection.start.to_point(&snapshot));
200 let end = snapshot
201 .buffer_snapshot
202 .anchor_before(selection.end.to_point(&snapshot));
203 let new_range = start..end;
204 vim.exchange_impl(new_range, editor, &snapshot, window, cx);
205 editor.set_clip_at_line_ends(true, cx);
206 });
207 }
208
209 pub fn exchange_impl(
210 &self,
211 new_range: Range<Anchor>,
212 editor: &mut Editor,
213 snapshot: &EditorSnapshot,
214 window: &mut Window,
215 cx: &mut Context<Editor>,
216 ) {
217 if let Some((_, ranges)) = editor.clear_background_highlights::<VimExchange>(cx) {
218 let previous_range = ranges[0].clone();
219
220 let new_range_start = new_range.start.to_offset(&snapshot.buffer_snapshot);
221 let new_range_end = new_range.end.to_offset(&snapshot.buffer_snapshot);
222 let previous_range_end = previous_range.end.to_offset(&snapshot.buffer_snapshot);
223 let previous_range_start = previous_range.start.to_offset(&snapshot.buffer_snapshot);
224
225 let text_for = |range: Range<Anchor>| {
226 snapshot
227 .buffer_snapshot
228 .text_for_range(range)
229 .collect::<String>()
230 };
231
232 let mut final_cursor_position = None;
233
234 if previous_range_end < new_range_start || new_range_end < previous_range_start {
235 let previous_text = text_for(previous_range.clone());
236 let new_text = text_for(new_range.clone());
237 final_cursor_position = Some(new_range.start.to_display_point(snapshot));
238
239 editor.edit([(previous_range, new_text), (new_range, previous_text)], cx);
240 } else if new_range_start <= previous_range_start && new_range_end >= previous_range_end
241 {
242 final_cursor_position = Some(new_range.start.to_display_point(snapshot));
243 editor.edit([(new_range, text_for(previous_range))], cx);
244 } else if previous_range_start <= new_range_start && previous_range_end >= new_range_end
245 {
246 final_cursor_position = Some(previous_range.start.to_display_point(snapshot));
247 editor.edit([(previous_range, text_for(new_range))], cx);
248 }
249
250 if let Some(position) = final_cursor_position {
251 editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
252 s.move_with(|_map, selection| {
253 selection.collapse_to(position, SelectionGoal::None);
254 });
255 })
256 }
257 } else {
258 let ranges = [new_range];
259 editor.highlight_background::<VimExchange>(
260 &ranges,
261 |theme| theme.editor_document_highlight_read_background,
262 cx,
263 );
264 }
265 }
266}
267
268#[cfg(test)]
269mod test {
270 use indoc::indoc;
271
272 use crate::{
273 state::Mode,
274 test::{NeovimBackedTestContext, VimTestContext},
275 };
276
277 #[gpui::test]
278 async fn test_enter_and_exit_replace_mode(cx: &mut gpui::TestAppContext) {
279 let mut cx = VimTestContext::new(cx, true).await;
280 cx.simulate_keystrokes("shift-r");
281 assert_eq!(cx.mode(), Mode::Replace);
282 cx.simulate_keystrokes("escape");
283 assert_eq!(cx.mode(), Mode::Normal);
284 }
285
286 #[gpui::test]
287 #[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
288 async fn test_replace_mode(cx: &mut gpui::TestAppContext) {
289 let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
290
291 // test normal replace
292 cx.set_shared_state(indoc! {"
293 ˇThe quick brown
294 fox jumps over
295 the lazy dog."})
296 .await;
297 cx.simulate_shared_keystrokes("shift-r O n e").await;
298 cx.shared_state().await.assert_eq(indoc! {"
299 Oneˇ quick brown
300 fox jumps over
301 the lazy dog."});
302
303 // test replace with line ending
304 cx.set_shared_state(indoc! {"
305 The quick browˇn
306 fox jumps over
307 the lazy dog."})
308 .await;
309 cx.simulate_shared_keystrokes("shift-r O n e").await;
310 cx.shared_state().await.assert_eq(indoc! {"
311 The quick browOneˇ
312 fox jumps over
313 the lazy dog."});
314
315 // test replace with blank line
316 cx.set_shared_state(indoc! {"
317 The quick brown
318 ˇ
319 fox jumps over
320 the lazy dog."})
321 .await;
322 cx.simulate_shared_keystrokes("shift-r O n e").await;
323 cx.shared_state().await.assert_eq(indoc! {"
324 The quick brown
325 Oneˇ
326 fox jumps over
327 the lazy dog."});
328
329 // test replace with newline
330 cx.set_shared_state(indoc! {"
331 The quˇick brown
332 fox jumps over
333 the lazy dog."})
334 .await;
335 cx.simulate_shared_keystrokes("shift-r enter O n e").await;
336 cx.shared_state().await.assert_eq(indoc! {"
337 The qu
338 Oneˇ brown
339 fox jumps over
340 the lazy dog."});
341
342 // test replace with multi cursor and newline
343 cx.set_state(
344 indoc! {"
345 ˇThe quick brown
346 fox jumps over
347 the lazy ˇdog."},
348 Mode::Normal,
349 );
350 cx.simulate_keystrokes("shift-r O n e");
351 cx.assert_state(
352 indoc! {"
353 Oneˇ quick brown
354 fox jumps over
355 the lazy Oneˇ."},
356 Mode::Replace,
357 );
358 cx.simulate_keystrokes("enter T w o");
359 cx.assert_state(
360 indoc! {"
361 One
362 Twoˇck brown
363 fox jumps over
364 the lazy One
365 Twoˇ"},
366 Mode::Replace,
367 );
368 }
369
370 #[gpui::test]
371 async fn test_replace_mode_with_counts(cx: &mut gpui::TestAppContext) {
372 let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
373
374 cx.set_shared_state("ˇhello\n").await;
375 cx.simulate_shared_keystrokes("3 shift-r - escape").await;
376 cx.shared_state().await.assert_eq("--ˇ-lo\n");
377
378 cx.set_shared_state("ˇhello\n").await;
379 cx.simulate_shared_keystrokes("3 shift-r a b c escape")
380 .await;
381 cx.shared_state().await.assert_eq("abcabcabˇc\n");
382 }
383
384 #[gpui::test]
385 async fn test_replace_mode_repeat(cx: &mut gpui::TestAppContext) {
386 let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
387
388 cx.set_shared_state("ˇhello world\n").await;
389 cx.simulate_shared_keystrokes("shift-r - - - escape 4 l .")
390 .await;
391 cx.shared_state().await.assert_eq("---lo --ˇ-ld\n");
392 }
393
394 #[gpui::test]
395 async fn test_replace_mode_undo(cx: &mut gpui::TestAppContext) {
396 let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
397
398 const UNDO_REPLACE_EXAMPLES: &[&str] = &[
399 // replace undo with single line
400 "ˇThe quick brown fox jumps over the lazy dog.",
401 // replace undo with ending line
402 indoc! {"
403 The quick browˇn
404 fox jumps over
405 the lazy dog."
406 },
407 // replace undo with empty line
408 indoc! {"
409 The quick brown
410 ˇ
411 fox jumps over
412 the lazy dog."
413 },
414 ];
415
416 for example in UNDO_REPLACE_EXAMPLES {
417 // normal undo
418 cx.simulate("shift-r O n e backspace backspace backspace", example)
419 .await
420 .assert_matches();
421 // undo with new line
422 cx.simulate("shift-r O enter e backspace backspace backspace", example)
423 .await
424 .assert_matches();
425 cx.simulate(
426 "shift-r O enter n enter e backspace backspace backspace backspace backspace",
427 example,
428 )
429 .await
430 .assert_matches();
431 }
432 }
433
434 #[gpui::test]
435 async fn test_replace_multicursor(cx: &mut gpui::TestAppContext) {
436 let mut cx = VimTestContext::new(cx, true).await;
437 cx.set_state("ˇabcˇabcabc", Mode::Normal);
438 cx.simulate_keystrokes("shift-r 1 2 3 4");
439 cx.assert_state("1234ˇ234ˇbc", Mode::Replace);
440 assert_eq!(cx.mode(), Mode::Replace);
441 cx.simulate_keystrokes("backspace backspace backspace backspace backspace");
442 cx.assert_state("ˇabˇcabcabc", Mode::Replace);
443 }
444
445 #[gpui::test]
446 async fn test_replace_undo(cx: &mut gpui::TestAppContext) {
447 let mut cx = VimTestContext::new(cx, true).await;
448
449 cx.set_state("ˇaaaa", Mode::Normal);
450 cx.simulate_keystrokes("0 shift-r b b b escape u");
451 cx.assert_state("ˇaaaa", Mode::Normal);
452 }
453
454 #[gpui::test]
455 async fn test_exchange_separate_range(cx: &mut gpui::TestAppContext) {
456 let mut cx = VimTestContext::new(cx, true).await;
457
458 cx.set_state("ˇhello world", Mode::Normal);
459 cx.simulate_keystrokes("c x i w w c x i w");
460 cx.assert_state("world ˇhello", Mode::Normal);
461 }
462
463 #[gpui::test]
464 async fn test_exchange_complete_overlap(cx: &mut gpui::TestAppContext) {
465 let mut cx = VimTestContext::new(cx, true).await;
466
467 cx.set_state("ˇhello world", Mode::Normal);
468 cx.simulate_keystrokes("c x x w c x i w");
469 cx.assert_state("ˇworld", Mode::Normal);
470
471 // the focus should still be at the start of the word if we reverse the
472 // order of selections (smaller -> larger)
473 cx.set_state("ˇhello world", Mode::Normal);
474 cx.simulate_keystrokes("c x i w c x x");
475 cx.assert_state("ˇhello", Mode::Normal);
476 }
477
478 #[gpui::test]
479 async fn test_exchange_partial_overlap(cx: &mut gpui::TestAppContext) {
480 let mut cx = VimTestContext::new(cx, true).await;
481
482 cx.set_state("ˇhello world", Mode::Normal);
483 cx.simulate_keystrokes("c x t r w c x i w");
484 cx.assert_state("hello ˇworld", Mode::Normal);
485 }
486}