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