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