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