1use std::borrow::Cow;
2
3use collections::HashMap;
4use editor::{display_map::ToDisplayPoint, Autoscroll, Bias, ClipboardSelection};
5use gpui::{actions, MutableAppContext, ViewContext};
6use language::{AutoindentMode, SelectionGoal};
7use workspace::Workspace;
8
9use crate::{motion::Motion, state::Mode, utils::copy_selections_content, Vim};
10
11actions!(vim, [VisualDelete, VisualChange, VisualYank, VisualPaste]);
12
13pub fn init(cx: &mut MutableAppContext) {
14 cx.add_action(change);
15 cx.add_action(delete);
16 cx.add_action(yank);
17 cx.add_action(paste);
18}
19
20pub fn visual_motion(motion: Motion, cx: &mut MutableAppContext) {
21 Vim::update(cx, |vim, cx| {
22 vim.update_active_editor(cx, |editor, cx| {
23 editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
24 s.move_with(|map, selection| {
25 let (new_head, goal) = motion.move_point(map, selection.head(), selection.goal);
26 let was_reversed = selection.reversed;
27 selection.set_head(new_head, goal);
28
29 if was_reversed && !selection.reversed {
30 // Head was at the start of the selection, and now is at the end. We need to move the start
31 // back by one if possible in order to compensate for this change.
32 *selection.start.column_mut() = selection.start.column().saturating_sub(1);
33 selection.start = map.clip_point(selection.start, Bias::Left);
34 } else if !was_reversed && selection.reversed {
35 // Head was at the end of the selection, and now is at the start. We need to move the end
36 // forward by one if possible in order to compensate for this change.
37 *selection.end.column_mut() = selection.end.column() + 1;
38 selection.end = map.clip_point(selection.end, Bias::Right);
39 }
40 });
41 });
42 });
43 });
44}
45
46pub fn change(_: &mut Workspace, _: &VisualChange, cx: &mut ViewContext<Workspace>) {
47 Vim::update(cx, |vim, cx| {
48 vim.update_active_editor(cx, |editor, cx| {
49 editor.set_clip_at_line_ends(false, cx);
50 // Compute edits and resulting anchor selections. If in line mode, adjust
51 // the anchor location and additional newline
52 let mut edits = Vec::new();
53 let mut new_selections = Vec::new();
54 let line_mode = editor.selections.line_mode;
55 editor.change_selections(None, cx, |s| {
56 s.move_with(|map, selection| {
57 if !selection.reversed {
58 // Head is at the end of the selection. Adjust the end position to
59 // to include the character under the cursor.
60 *selection.end.column_mut() = selection.end.column() + 1;
61 selection.end = map.clip_point(selection.end, Bias::Right);
62 }
63
64 if line_mode {
65 let range = selection.map(|p| p.to_point(map)).range();
66 let expanded_range = map.expand_to_line(range);
67 // If we are at the last line, the anchor needs to be after the newline so that
68 // it is on a line of its own. Otherwise, the anchor may be after the newline
69 let anchor = if expanded_range.end == map.buffer_snapshot.max_point() {
70 map.buffer_snapshot.anchor_after(expanded_range.end)
71 } else {
72 map.buffer_snapshot.anchor_before(expanded_range.start)
73 };
74
75 edits.push((expanded_range, "\n"));
76 new_selections.push(selection.map(|_| anchor.clone()));
77 } else {
78 let range = selection.map(|p| p.to_point(map)).range();
79 let anchor = map.buffer_snapshot.anchor_after(range.end);
80 edits.push((range, ""));
81 new_selections.push(selection.map(|_| anchor.clone()));
82 }
83 selection.goal = SelectionGoal::None;
84 });
85 });
86 copy_selections_content(editor, editor.selections.line_mode, cx);
87 editor.edit_with_autoindent(edits, cx);
88 editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
89 s.select_anchors(new_selections);
90 });
91 });
92 vim.switch_mode(Mode::Insert, false, cx);
93 });
94}
95
96pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext<Workspace>) {
97 Vim::update(cx, |vim, cx| {
98 vim.update_active_editor(cx, |editor, cx| {
99 editor.set_clip_at_line_ends(false, cx);
100 let mut original_columns: HashMap<_, _> = Default::default();
101 let line_mode = editor.selections.line_mode;
102 editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
103 s.move_with(|map, selection| {
104 if line_mode {
105 original_columns
106 .insert(selection.id, selection.head().to_point(map).column);
107 } else if !selection.reversed {
108 // Head is at the end of the selection. Adjust the end position to
109 // to include the character under the cursor.
110 *selection.end.column_mut() = selection.end.column() + 1;
111 selection.end = map.clip_point(selection.end, Bias::Right);
112 }
113 selection.goal = SelectionGoal::None;
114 });
115 });
116 copy_selections_content(editor, line_mode, cx);
117 editor.insert("", cx);
118
119 // Fixup cursor position after the deletion
120 editor.set_clip_at_line_ends(true, cx);
121 editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
122 s.move_with(|map, selection| {
123 let mut cursor = selection.head().to_point(map);
124
125 if let Some(column) = original_columns.get(&selection.id) {
126 cursor.column = *column
127 }
128 let cursor = map.clip_point(cursor.to_display_point(map), Bias::Left);
129 selection.collapse_to(cursor, selection.goal)
130 });
131 });
132 });
133 vim.switch_mode(Mode::Normal, false, cx);
134 });
135}
136
137pub fn yank(_: &mut Workspace, _: &VisualYank, cx: &mut ViewContext<Workspace>) {
138 Vim::update(cx, |vim, cx| {
139 vim.update_active_editor(cx, |editor, cx| {
140 editor.set_clip_at_line_ends(false, cx);
141 let line_mode = editor.selections.line_mode;
142 if !line_mode {
143 editor.change_selections(None, cx, |s| {
144 s.move_with(|map, selection| {
145 if !selection.reversed {
146 // Head is at the end of the selection. Adjust the end position to
147 // to include the character under the cursor.
148 *selection.end.column_mut() = selection.end.column() + 1;
149 selection.end = map.clip_point(selection.end, Bias::Right);
150 }
151 });
152 });
153 }
154 copy_selections_content(editor, line_mode, cx);
155 editor.change_selections(None, cx, |s| {
156 s.move_with(|_, selection| {
157 selection.collapse_to(selection.start, SelectionGoal::None)
158 });
159 });
160 });
161 vim.switch_mode(Mode::Normal, false, cx);
162 });
163}
164
165pub fn paste(_: &mut Workspace, _: &VisualPaste, cx: &mut ViewContext<Workspace>) {
166 Vim::update(cx, |vim, cx| {
167 vim.update_active_editor(cx, |editor, cx| {
168 editor.transact(cx, |editor, cx| {
169 if let Some(item) = cx.as_mut().read_from_clipboard() {
170 copy_selections_content(editor, editor.selections.line_mode, cx);
171 let mut clipboard_text = Cow::Borrowed(item.text());
172 if let Some(mut clipboard_selections) =
173 item.metadata::<Vec<ClipboardSelection>>()
174 {
175 let (display_map, selections) = editor.selections.all_adjusted_display(cx);
176 let all_selections_were_entire_line =
177 clipboard_selections.iter().all(|s| s.is_entire_line);
178 if clipboard_selections.len() != selections.len() {
179 let mut newline_separated_text = String::new();
180 let mut clipboard_selections =
181 clipboard_selections.drain(..).peekable();
182 let mut ix = 0;
183 while let Some(clipboard_selection) = clipboard_selections.next() {
184 newline_separated_text
185 .push_str(&clipboard_text[ix..ix + clipboard_selection.len]);
186 ix += clipboard_selection.len;
187 if clipboard_selections.peek().is_some() {
188 newline_separated_text.push('\n');
189 }
190 }
191 clipboard_text = Cow::Owned(newline_separated_text);
192 }
193
194 let mut new_selections = Vec::new();
195 editor.buffer().update(cx, |buffer, cx| {
196 let snapshot = buffer.snapshot(cx);
197 let mut start_offset = 0;
198 let mut edits = Vec::new();
199 for (ix, selection) in selections.iter().enumerate() {
200 let to_insert;
201 let linewise;
202 if let Some(clipboard_selection) = clipboard_selections.get(ix) {
203 let end_offset = start_offset + clipboard_selection.len;
204 to_insert = &clipboard_text[start_offset..end_offset];
205 linewise = clipboard_selection.is_entire_line;
206 start_offset = end_offset;
207 } else {
208 to_insert = clipboard_text.as_str();
209 linewise = all_selections_were_entire_line;
210 }
211
212 let mut selection = selection.clone();
213 if !selection.reversed {
214 let mut adjusted = selection.end;
215 // Head is at the end of the selection. Adjust the end position to
216 // to include the character under the cursor.
217 *adjusted.column_mut() = adjusted.column() + 1;
218 adjusted = display_map.clip_point(adjusted, Bias::Right);
219 // If the selection is empty, move both the start and end forward one
220 // character
221 if selection.is_empty() {
222 selection.start = adjusted;
223 selection.end = adjusted;
224 } else {
225 selection.end = adjusted;
226 }
227 }
228
229 let range = selection.map(|p| p.to_point(&display_map)).range();
230
231 let new_position = if linewise {
232 edits.push((range.start..range.start, "\n"));
233 let mut new_position = range.start;
234 new_position.column = 0;
235 new_position.row += 1;
236 new_position
237 } else {
238 range.start
239 };
240
241 new_selections.push(selection.map(|_| new_position));
242
243 if linewise && to_insert.ends_with('\n') {
244 edits.push((
245 range.clone(),
246 &to_insert[0..to_insert.len().saturating_sub(1)],
247 ))
248 } else {
249 edits.push((range.clone(), to_insert));
250 }
251
252 if linewise {
253 edits.push((range.end..range.end, "\n"));
254 }
255 }
256 drop(snapshot);
257 buffer.edit(edits, Some(AutoindentMode::EachLine), cx);
258 });
259
260 editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
261 s.select(new_selections)
262 });
263 } else {
264 editor.insert(&clipboard_text, cx);
265 }
266 }
267 });
268 });
269 vim.switch_mode(Mode::Normal, false, cx);
270 });
271}
272
273#[cfg(test)]
274mod test {
275 use indoc::indoc;
276
277 use crate::{state::Mode, vim_test_context::VimTestContext};
278
279 #[gpui::test]
280 async fn test_enter_visual_mode(cx: &mut gpui::TestAppContext) {
281 let cx = VimTestContext::new(cx, true).await;
282 let mut cx = cx
283 .binding(["v", "w", "j"])
284 .mode_after(Mode::Visual { line: false });
285 cx.assert(
286 indoc! {"
287 The ˇquick brown
288 fox jumps over
289 the lazy dog"},
290 indoc! {"
291 The «quick brown
292 fox jumps ˇ»over
293 the lazy dog"},
294 );
295 cx.assert(
296 indoc! {"
297 The quick brown
298 fox jumps over
299 the ˇlazy dog"},
300 indoc! {"
301 The quick brown
302 fox jumps over
303 the «lazy ˇ»dog"},
304 );
305 cx.assert(
306 indoc! {"
307 The quick brown
308 fox jumps ˇover
309 the lazy dog"},
310 indoc! {"
311 The quick brown
312 fox jumps «over
313 ˇ»the lazy dog"},
314 );
315 let mut cx = cx
316 .binding(["v", "b", "k"])
317 .mode_after(Mode::Visual { line: false });
318 cx.assert(
319 indoc! {"
320 The ˇquick brown
321 fox jumps over
322 the lazy dog"},
323 indoc! {"
324 «ˇThe q»uick brown
325 fox jumps over
326 the lazy dog"},
327 );
328 cx.assert(
329 indoc! {"
330 The quick brown
331 fox jumps over
332 the ˇlazy dog"},
333 indoc! {"
334 The quick brown
335 «ˇfox jumps over
336 the l»azy dog"},
337 );
338 cx.assert(
339 indoc! {"
340 The quick brown
341 fox jumps ˇover
342 the lazy dog"},
343 indoc! {"
344 The «ˇquick brown
345 fox jumps o»ver
346 the lazy dog"},
347 );
348 }
349
350 #[gpui::test]
351 async fn test_visual_delete(cx: &mut gpui::TestAppContext) {
352 let cx = VimTestContext::new(cx, true).await;
353 let mut cx = cx.binding(["v", "w", "x"]);
354 cx.assert("The quick ˇbrown", "The quickˇ ");
355 let mut cx = cx.binding(["v", "w", "j", "x"]);
356 cx.assert(
357 indoc! {"
358 The ˇquick brown
359 fox jumps over
360 the lazy dog"},
361 indoc! {"
362 The ˇver
363 the lazy dog"},
364 );
365 // Test pasting code copied on delete
366 cx.simulate_keystrokes(["j", "p"]);
367 cx.assert_editor_state(indoc! {"
368 The ver
369 the lˇquick brown
370 fox jumps oazy dog"});
371
372 cx.assert(
373 indoc! {"
374 The quick brown
375 fox jumps over
376 the ˇlazy dog"},
377 indoc! {"
378 The quick brown
379 fox jumps over
380 the ˇog"},
381 );
382 cx.assert(
383 indoc! {"
384 The quick brown
385 fox jumps ˇover
386 the lazy dog"},
387 indoc! {"
388 The quick brown
389 fox jumps ˇhe lazy dog"},
390 );
391 let mut cx = cx.binding(["v", "b", "k", "x"]);
392 cx.assert(
393 indoc! {"
394 The ˇquick brown
395 fox jumps over
396 the lazy dog"},
397 indoc! {"
398 ˇuick brown
399 fox jumps over
400 the lazy dog"},
401 );
402 cx.assert(
403 indoc! {"
404 The quick brown
405 fox jumps over
406 the ˇlazy dog"},
407 indoc! {"
408 The quick brown
409 ˇazy dog"},
410 );
411 cx.assert(
412 indoc! {"
413 The quick brown
414 fox jumps ˇover
415 the lazy dog"},
416 indoc! {"
417 The ˇver
418 the lazy dog"},
419 );
420 }
421
422 #[gpui::test]
423 async fn test_visual_line_delete(cx: &mut gpui::TestAppContext) {
424 let cx = VimTestContext::new(cx, true).await;
425 let mut cx = cx.binding(["shift-v", "x"]);
426 cx.assert(
427 indoc! {"
428 The quˇick brown
429 fox jumps over
430 the lazy dog"},
431 indoc! {"
432 fox juˇmps over
433 the lazy dog"},
434 );
435 // Test pasting code copied on delete
436 cx.simulate_keystroke("p");
437 cx.assert_editor_state(indoc! {"
438 fox jumps over
439 ˇThe quick brown
440 the lazy dog"});
441
442 cx.assert(
443 indoc! {"
444 The quick brown
445 fox juˇmps over
446 the lazy dog"},
447 indoc! {"
448 The quick brown
449 the laˇzy dog"},
450 );
451 cx.assert(
452 indoc! {"
453 The quick brown
454 fox jumps over
455 the laˇzy dog"},
456 indoc! {"
457 The quick brown
458 fox juˇmps over"},
459 );
460 let mut cx = cx.binding(["shift-v", "j", "x"]);
461 cx.assert(
462 indoc! {"
463 The quˇick brown
464 fox jumps over
465 the lazy dog"},
466 "the laˇzy dog",
467 );
468 // Test pasting code copied on delete
469 cx.simulate_keystroke("p");
470 cx.assert_editor_state(indoc! {"
471 the lazy dog
472 ˇThe quick brown
473 fox jumps over"});
474
475 cx.assert(
476 indoc! {"
477 The quick brown
478 fox juˇmps over
479 the lazy dog"},
480 "The quˇick brown",
481 );
482 cx.assert(
483 indoc! {"
484 The quick brown
485 fox jumps over
486 the laˇzy dog"},
487 indoc! {"
488 The quick brown
489 fox juˇmps over"},
490 );
491 }
492
493 #[gpui::test]
494 async fn test_visual_change(cx: &mut gpui::TestAppContext) {
495 let cx = VimTestContext::new(cx, true).await;
496 let mut cx = cx.binding(["v", "w", "c"]).mode_after(Mode::Insert);
497 cx.assert("The quick ˇbrown", "The quick ˇ");
498 let mut cx = cx.binding(["v", "w", "j", "c"]).mode_after(Mode::Insert);
499 cx.assert(
500 indoc! {"
501 The ˇquick brown
502 fox jumps over
503 the lazy dog"},
504 indoc! {"
505 The ˇver
506 the lazy dog"},
507 );
508 cx.assert(
509 indoc! {"
510 The quick brown
511 fox jumps over
512 the ˇlazy dog"},
513 indoc! {"
514 The quick brown
515 fox jumps over
516 the ˇog"},
517 );
518 cx.assert(
519 indoc! {"
520 The quick brown
521 fox jumps ˇover
522 the lazy dog"},
523 indoc! {"
524 The quick brown
525 fox jumps ˇhe lazy dog"},
526 );
527 let mut cx = cx.binding(["v", "b", "k", "c"]).mode_after(Mode::Insert);
528 cx.assert(
529 indoc! {"
530 The ˇquick brown
531 fox jumps over
532 the lazy dog"},
533 indoc! {"
534 ˇuick brown
535 fox jumps over
536 the lazy dog"},
537 );
538 cx.assert(
539 indoc! {"
540 The quick brown
541 fox jumps over
542 the ˇlazy dog"},
543 indoc! {"
544 The quick brown
545 ˇazy dog"},
546 );
547 cx.assert(
548 indoc! {"
549 The quick brown
550 fox jumps ˇover
551 the lazy dog"},
552 indoc! {"
553 The ˇver
554 the lazy dog"},
555 );
556 }
557
558 #[gpui::test]
559 async fn test_visual_line_change(cx: &mut gpui::TestAppContext) {
560 let cx = VimTestContext::new(cx, true).await;
561 let mut cx = cx.binding(["shift-v", "c"]).mode_after(Mode::Insert);
562 cx.assert(
563 indoc! {"
564 The quˇick brown
565 fox jumps over
566 the lazy dog"},
567 indoc! {"
568 ˇ
569 fox jumps over
570 the lazy dog"},
571 );
572 // Test pasting code copied on change
573 cx.simulate_keystrokes(["escape", "j", "p"]);
574 cx.assert_editor_state(indoc! {"
575
576 fox jumps over
577 ˇThe quick brown
578 the lazy dog"});
579
580 cx.assert(
581 indoc! {"
582 The quick brown
583 fox juˇmps over
584 the lazy dog"},
585 indoc! {"
586 The quick brown
587 ˇ
588 the lazy dog"},
589 );
590 cx.assert(
591 indoc! {"
592 The quick brown
593 fox jumps over
594 the laˇzy dog"},
595 indoc! {"
596 The quick brown
597 fox jumps over
598 ˇ"},
599 );
600 let mut cx = cx.binding(["shift-v", "j", "c"]).mode_after(Mode::Insert);
601 cx.assert(
602 indoc! {"
603 The quˇick brown
604 fox jumps over
605 the lazy dog"},
606 indoc! {"
607 ˇ
608 the lazy dog"},
609 );
610 // Test pasting code copied on delete
611 cx.simulate_keystrokes(["escape", "j", "p"]);
612 cx.assert_editor_state(indoc! {"
613
614 the lazy dog
615 ˇThe quick brown
616 fox jumps over"});
617 cx.assert(
618 indoc! {"
619 The quick brown
620 fox juˇmps over
621 the lazy dog"},
622 indoc! {"
623 The quick brown
624 ˇ"},
625 );
626 cx.assert(
627 indoc! {"
628 The quick brown
629 fox jumps over
630 the laˇzy dog"},
631 indoc! {"
632 The quick brown
633 fox jumps over
634 ˇ"},
635 );
636 }
637
638 #[gpui::test]
639 async fn test_visual_yank(cx: &mut gpui::TestAppContext) {
640 let cx = VimTestContext::new(cx, true).await;
641 let mut cx = cx.binding(["v", "w", "y"]);
642 cx.assert("The quick ˇbrown", "The quick ˇbrown");
643 cx.assert_clipboard_content(Some("brown"));
644 let mut cx = cx.binding(["v", "w", "j", "y"]);
645 cx.assert(
646 indoc! {"
647 The ˇquick brown
648 fox jumps over
649 the lazy dog"},
650 indoc! {"
651 The ˇquick brown
652 fox jumps over
653 the lazy dog"},
654 );
655 cx.assert_clipboard_content(Some(indoc! {"
656 quick brown
657 fox jumps o"}));
658 cx.assert(
659 indoc! {"
660 The quick brown
661 fox jumps over
662 the ˇlazy dog"},
663 indoc! {"
664 The quick brown
665 fox jumps over
666 the ˇlazy dog"},
667 );
668 cx.assert_clipboard_content(Some("lazy d"));
669 cx.assert(
670 indoc! {"
671 The quick brown
672 fox jumps ˇover
673 the lazy dog"},
674 indoc! {"
675 The quick brown
676 fox jumps ˇover
677 the lazy dog"},
678 );
679 cx.assert_clipboard_content(Some(indoc! {"
680 over
681 t"}));
682 let mut cx = cx.binding(["v", "b", "k", "y"]);
683 cx.assert(
684 indoc! {"
685 The ˇquick brown
686 fox jumps over
687 the lazy dog"},
688 indoc! {"
689 ˇThe quick brown
690 fox jumps over
691 the lazy dog"},
692 );
693 cx.assert_clipboard_content(Some("The q"));
694 cx.assert(
695 indoc! {"
696 The quick brown
697 fox jumps over
698 the ˇlazy dog"},
699 indoc! {"
700 The quick brown
701 ˇfox jumps over
702 the lazy dog"},
703 );
704 cx.assert_clipboard_content(Some(indoc! {"
705 fox jumps over
706 the l"}));
707 cx.assert(
708 indoc! {"
709 The quick brown
710 fox jumps ˇover
711 the lazy dog"},
712 indoc! {"
713 The ˇquick brown
714 fox jumps over
715 the lazy dog"},
716 );
717 cx.assert_clipboard_content(Some(indoc! {"
718 quick brown
719 fox jumps o"}));
720 }
721
722 #[gpui::test]
723 async fn test_visual_paste(cx: &mut gpui::TestAppContext) {
724 let mut cx = VimTestContext::new(cx, true).await;
725 cx.set_state(
726 indoc! {"
727 The quick brown
728 fox «jumpˇ»s over
729 the lazy dog"},
730 Mode::Visual { line: false },
731 );
732 cx.simulate_keystroke("y");
733 cx.set_state(
734 indoc! {"
735 The quick brown
736 fox jumpˇs over
737 the lazy dog"},
738 Mode::Normal,
739 );
740 cx.simulate_keystroke("p");
741 cx.assert_state(
742 indoc! {"
743 The quick brown
744 fox jumpsˇjumps over
745 the lazy dog"},
746 Mode::Normal,
747 );
748
749 cx.set_state(
750 indoc! {"
751 The quick brown
752 fox juˇmps over
753 the lazy dog"},
754 Mode::Visual { line: true },
755 );
756 cx.simulate_keystroke("d");
757 cx.assert_state(
758 indoc! {"
759 The quick brown
760 the laˇzy dog"},
761 Mode::Normal,
762 );
763 cx.set_state(
764 indoc! {"
765 The quick brown
766 the «lazˇ»y dog"},
767 Mode::Visual { line: false },
768 );
769 cx.simulate_keystroke("p");
770 cx.assert_state(
771 indoc! {"
772 The quick brown
773 the
774 ˇfox jumps over
775 dog"},
776 Mode::Normal,
777 );
778 }
779}