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