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