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