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::wrapping_left(
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_background_highlights::<VimExchange>(cx);
174 });
175 self.clear_operator(window, cx);
176 }
177
178 pub fn exchange_motion(
179 &mut self,
180 motion: Motion,
181 times: Option<usize>,
182 window: &mut Window,
183 cx: &mut Context<Self>,
184 ) {
185 self.stop_recording(cx);
186 self.update_editor(window, cx, |vim, editor, window, cx| {
187 editor.set_clip_at_line_ends(false, cx);
188 let text_layout_details = editor.text_layout_details(window);
189 let mut selection = editor.selections.newest_display(cx);
190 let snapshot = editor.snapshot(window, cx);
191 motion.expand_selection(
192 &snapshot,
193 &mut selection,
194 times,
195 false,
196 &text_layout_details,
197 );
198 let start = snapshot
199 .buffer_snapshot
200 .anchor_before(selection.start.to_point(&snapshot));
201 let end = snapshot
202 .buffer_snapshot
203 .anchor_before(selection.end.to_point(&snapshot));
204 let new_range = start..end;
205 vim.exchange_impl(new_range, editor, &snapshot, window, cx);
206 editor.set_clip_at_line_ends(true, cx);
207 });
208 }
209
210 pub fn exchange_impl(
211 &self,
212 new_range: Range<Anchor>,
213 editor: &mut Editor,
214 snapshot: &EditorSnapshot,
215 window: &mut Window,
216 cx: &mut Context<Editor>,
217 ) {
218 if let Some((_, ranges)) = editor.clear_background_highlights::<VimExchange>(cx) {
219 let previous_range = ranges[0].clone();
220
221 let new_range_start = new_range.start.to_offset(&snapshot.buffer_snapshot);
222 let new_range_end = new_range.end.to_offset(&snapshot.buffer_snapshot);
223 let previous_range_end = previous_range.end.to_offset(&snapshot.buffer_snapshot);
224 let previous_range_start = previous_range.start.to_offset(&snapshot.buffer_snapshot);
225
226 let text_for = |range: Range<Anchor>| {
227 snapshot
228 .buffer_snapshot
229 .text_for_range(range)
230 .collect::<String>()
231 };
232
233 let mut final_cursor_position = None;
234
235 if previous_range_end < new_range_start || new_range_end < previous_range_start {
236 let previous_text = text_for(previous_range.clone());
237 let new_text = text_for(new_range.clone());
238 final_cursor_position = Some(new_range.start.to_display_point(snapshot));
239
240 editor.edit([(previous_range, new_text), (new_range, previous_text)], cx);
241 } else if new_range_start <= previous_range_start && new_range_end >= previous_range_end
242 {
243 final_cursor_position = Some(new_range.start.to_display_point(snapshot));
244 editor.edit([(new_range, text_for(previous_range))], cx);
245 } else if previous_range_start <= new_range_start && previous_range_end >= new_range_end
246 {
247 final_cursor_position = Some(previous_range.start.to_display_point(snapshot));
248 editor.edit([(previous_range, text_for(new_range))], cx);
249 }
250
251 if let Some(position) = final_cursor_position {
252 editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
253 s.move_with(|_map, selection| {
254 selection.collapse_to(position, SelectionGoal::None);
255 });
256 })
257 }
258 } else {
259 let ranges = [new_range];
260 editor.highlight_background::<VimExchange>(
261 &ranges,
262 |theme| theme.editor_document_highlight_read_background,
263 cx,
264 );
265 }
266 }
267}
268
269#[cfg(test)]
270mod test {
271 use indoc::indoc;
272
273 use crate::{
274 state::Mode,
275 test::{NeovimBackedTestContext, VimTestContext},
276 };
277
278 #[gpui::test]
279 async fn test_enter_and_exit_replace_mode(cx: &mut gpui::TestAppContext) {
280 let mut cx = VimTestContext::new(cx, true).await;
281 cx.simulate_keystrokes("shift-r");
282 assert_eq!(cx.mode(), Mode::Replace);
283 cx.simulate_keystrokes("escape");
284 assert_eq!(cx.mode(), Mode::Normal);
285 }
286
287 #[gpui::test]
288 #[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
289 async fn test_replace_mode(cx: &mut gpui::TestAppContext) {
290 let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
291
292 // test normal replace
293 cx.set_shared_state(indoc! {"
294 ˇThe quick brown
295 fox jumps over
296 the lazy dog."})
297 .await;
298 cx.simulate_shared_keystrokes("shift-r O n e").await;
299 cx.shared_state().await.assert_eq(indoc! {"
300 Oneˇ quick brown
301 fox jumps over
302 the lazy dog."});
303
304 // test replace with line ending
305 cx.set_shared_state(indoc! {"
306 The quick browˇn
307 fox jumps over
308 the lazy dog."})
309 .await;
310 cx.simulate_shared_keystrokes("shift-r O n e").await;
311 cx.shared_state().await.assert_eq(indoc! {"
312 The quick browOneˇ
313 fox jumps over
314 the lazy dog."});
315
316 // test replace with blank line
317 cx.set_shared_state(indoc! {"
318 The quick brown
319 ˇ
320 fox jumps over
321 the lazy dog."})
322 .await;
323 cx.simulate_shared_keystrokes("shift-r O n e").await;
324 cx.shared_state().await.assert_eq(indoc! {"
325 The quick brown
326 Oneˇ
327 fox jumps over
328 the lazy dog."});
329
330 // test replace with newline
331 cx.set_shared_state(indoc! {"
332 The quˇick brown
333 fox jumps over
334 the lazy dog."})
335 .await;
336 cx.simulate_shared_keystrokes("shift-r enter O n e").await;
337 cx.shared_state().await.assert_eq(indoc! {"
338 The qu
339 Oneˇ brown
340 fox jumps over
341 the lazy dog."});
342
343 // test replace with multi cursor and newline
344 cx.set_state(
345 indoc! {"
346 ˇThe quick brown
347 fox jumps over
348 the lazy ˇdog."},
349 Mode::Normal,
350 );
351 cx.simulate_keystrokes("shift-r O n e");
352 cx.assert_state(
353 indoc! {"
354 Oneˇ quick brown
355 fox jumps over
356 the lazy Oneˇ."},
357 Mode::Replace,
358 );
359 cx.simulate_keystrokes("enter T w o");
360 cx.assert_state(
361 indoc! {"
362 One
363 Twoˇck brown
364 fox jumps over
365 the lazy One
366 Twoˇ"},
367 Mode::Replace,
368 );
369 }
370
371 #[gpui::test]
372 async fn test_replace_mode_with_counts(cx: &mut gpui::TestAppContext) {
373 let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
374
375 cx.set_shared_state("ˇhello\n").await;
376 cx.simulate_shared_keystrokes("3 shift-r - escape").await;
377 cx.shared_state().await.assert_eq("--ˇ-lo\n");
378
379 cx.set_shared_state("ˇhello\n").await;
380 cx.simulate_shared_keystrokes("3 shift-r a b c escape")
381 .await;
382 cx.shared_state().await.assert_eq("abcabcabˇc\n");
383 }
384
385 #[gpui::test]
386 async fn test_replace_mode_repeat(cx: &mut gpui::TestAppContext) {
387 let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
388
389 cx.set_shared_state("ˇhello world\n").await;
390 cx.simulate_shared_keystrokes("shift-r - - - escape 4 l .")
391 .await;
392 cx.shared_state().await.assert_eq("---lo --ˇ-ld\n");
393 }
394
395 #[gpui::test]
396 async fn test_replace_mode_undo(cx: &mut gpui::TestAppContext) {
397 let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
398
399 const UNDO_REPLACE_EXAMPLES: &[&str] = &[
400 // replace undo with single line
401 "ˇThe quick brown fox jumps over the lazy dog.",
402 // replace undo with ending line
403 indoc! {"
404 The quick browˇn
405 fox jumps over
406 the lazy dog."
407 },
408 // replace undo with empty line
409 indoc! {"
410 The quick brown
411 ˇ
412 fox jumps over
413 the lazy dog."
414 },
415 ];
416
417 for example in UNDO_REPLACE_EXAMPLES {
418 // normal undo
419 cx.simulate("shift-r O n e backspace backspace backspace", example)
420 .await
421 .assert_matches();
422 // undo with new line
423 cx.simulate("shift-r O enter e backspace backspace backspace", example)
424 .await
425 .assert_matches();
426 cx.simulate(
427 "shift-r O enter n enter e backspace backspace backspace backspace backspace",
428 example,
429 )
430 .await
431 .assert_matches();
432 }
433 }
434
435 #[gpui::test]
436 async fn test_replace_multicursor(cx: &mut gpui::TestAppContext) {
437 let mut cx = VimTestContext::new(cx, true).await;
438 cx.set_state("ˇabcˇabcabc", Mode::Normal);
439 cx.simulate_keystrokes("shift-r 1 2 3 4");
440 cx.assert_state("1234ˇ234ˇbc", Mode::Replace);
441 assert_eq!(cx.mode(), Mode::Replace);
442 cx.simulate_keystrokes("backspace backspace backspace backspace backspace");
443 cx.assert_state("ˇabˇcabcabc", Mode::Replace);
444 }
445
446 #[gpui::test]
447 async fn test_replace_undo(cx: &mut gpui::TestAppContext) {
448 let mut cx = VimTestContext::new(cx, true).await;
449
450 cx.set_state("ˇaaaa", Mode::Normal);
451 cx.simulate_keystrokes("0 shift-r b b b escape u");
452 cx.assert_state("ˇaaaa", Mode::Normal);
453 }
454
455 #[gpui::test]
456 async fn test_exchange_separate_range(cx: &mut gpui::TestAppContext) {
457 let mut cx = VimTestContext::new(cx, true).await;
458
459 cx.set_state("ˇhello world", Mode::Normal);
460 cx.simulate_keystrokes("c x i w w c x i w");
461 cx.assert_state("world ˇhello", Mode::Normal);
462 }
463
464 #[gpui::test]
465 async fn test_exchange_complete_overlap(cx: &mut gpui::TestAppContext) {
466 let mut cx = VimTestContext::new(cx, true).await;
467
468 cx.set_state("ˇhello world", Mode::Normal);
469 cx.simulate_keystrokes("c x x w c x i w");
470 cx.assert_state("ˇworld", Mode::Normal);
471
472 // the focus should still be at the start of the word if we reverse the
473 // order of selections (smaller -> larger)
474 cx.set_state("ˇhello world", Mode::Normal);
475 cx.simulate_keystrokes("c x i w c x x");
476 cx.assert_state("ˇhello", Mode::Normal);
477 }
478
479 #[gpui::test]
480 async fn test_exchange_partial_overlap(cx: &mut gpui::TestAppContext) {
481 let mut cx = VimTestContext::new(cx, true).await;
482
483 cx.set_state("ˇhello world", Mode::Normal);
484 cx.simulate_keystrokes("c x t r w c x i w");
485 cx.assert_state("hello ˇworld", Mode::Normal);
486 }
487
488 #[gpui::test]
489 async fn test_clear_exchange_clears_operator(cx: &mut gpui::TestAppContext) {
490 let mut cx = VimTestContext::new(cx, true).await;
491
492 cx.set_state("ˇirrelevant", Mode::Normal);
493 cx.simulate_keystrokes("c x c");
494
495 assert_eq!(cx.active_operator(), None);
496 }
497
498 #[gpui::test]
499 async fn test_clear_exchange(cx: &mut gpui::TestAppContext) {
500 let mut cx = VimTestContext::new(cx, true).await;
501
502 cx.set_state("ˇhello world", Mode::Normal);
503 cx.simulate_keystrokes("c x i w c x c");
504
505 cx.update_editor(|editor, window, cx| {
506 let highlights = editor.all_text_background_highlights(window, cx);
507 assert_eq!(0, highlights.len());
508 });
509 }
510}