1use crate::{
2 Operator, 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::{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
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, 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 /// 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}