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