1use collections::HashMap;
2use editor::{Autoscroll, Bias};
3use gpui::{actions, MutableAppContext, ViewContext};
4use workspace::Workspace;
5
6use crate::{motion::Motion, state::Mode, utils::copy_selections_content, Vim};
7
8actions!(
9 vim,
10 [
11 VisualDelete,
12 VisualLineDelete,
13 VisualChange,
14 VisualLineChange,
15 VisualYank,
16 VisualLineYank,
17 ]
18);
19
20pub fn init(cx: &mut MutableAppContext) {
21 cx.add_action(change);
22 cx.add_action(change_line);
23 cx.add_action(delete);
24 cx.add_action(delete_line);
25 cx.add_action(yank);
26 cx.add_action(yank_line);
27}
28
29pub fn visual_motion(motion: Motion, cx: &mut MutableAppContext) {
30 Vim::update(cx, |vim, cx| {
31 vim.update_active_editor(cx, |editor, cx| {
32 editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
33 s.move_with(|map, selection| {
34 let (new_head, goal) = motion.move_point(map, selection.head(), selection.goal);
35 let new_head = map.clip_at_line_end(new_head);
36 let was_reversed = selection.reversed;
37 selection.set_head(new_head, goal);
38
39 if was_reversed && !selection.reversed {
40 // Head was at the start of the selection, and now is at the end. We need to move the start
41 // back by one if possible in order to compensate for this change.
42 *selection.start.column_mut() = selection.start.column().saturating_sub(1);
43 selection.start = map.clip_point(selection.start, Bias::Left);
44 } else if !was_reversed && selection.reversed {
45 // Head was at the end of the selection, and now is at the start. We need to move the end
46 // forward by one if possible in order to compensate for this change.
47 *selection.end.column_mut() = selection.end.column() + 1;
48 selection.end = map.clip_point(selection.end, Bias::Right);
49 }
50 });
51 });
52 });
53 });
54}
55
56pub fn change(_: &mut Workspace, _: &VisualChange, cx: &mut ViewContext<Workspace>) {
57 Vim::update(cx, |vim, cx| {
58 vim.update_active_editor(cx, |editor, cx| {
59 editor.set_clip_at_line_ends(false, cx);
60 editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
61 s.move_with(|map, selection| {
62 if !selection.reversed {
63 // Head is at the end of the selection. Adjust the end position to
64 // to include the character under the cursor.
65 *selection.end.column_mut() = selection.end.column() + 1;
66 selection.end = map.clip_point(selection.end, Bias::Left);
67 }
68 });
69 });
70 copy_selections_content(editor, false, cx);
71 editor.insert("", cx);
72 });
73 vim.switch_mode(Mode::Insert, cx);
74 });
75}
76
77pub fn change_line(_: &mut Workspace, _: &VisualLineChange, cx: &mut ViewContext<Workspace>) {
78 Vim::update(cx, |vim, cx| {
79 vim.update_active_editor(cx, |editor, cx| {
80 editor.set_clip_at_line_ends(false, cx);
81
82 let adjusted = editor.selections.all_adjusted(cx);
83 editor.change_selections(None, cx, |s| s.select(adjusted));
84 copy_selections_content(editor, true, cx);
85 editor.insert("", cx);
86 });
87 vim.switch_mode(Mode::Insert, cx);
88 });
89}
90
91pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext<Workspace>) {
92 Vim::update(cx, |vim, cx| {
93 vim.update_active_editor(cx, |editor, cx| {
94 editor.set_clip_at_line_ends(false, cx);
95 editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
96 s.move_with(|map, selection| {
97 if !selection.reversed {
98 // Head is at the end of the selection. Adjust the end position to
99 // to include the character under the cursor.
100 *selection.end.column_mut() = selection.end.column() + 1;
101 selection.end = map.clip_point(selection.end, Bias::Right);
102 }
103 });
104 });
105 copy_selections_content(editor, false, cx);
106 editor.insert("", cx);
107
108 // Fixup cursor position after the deletion
109 editor.set_clip_at_line_ends(true, cx);
110 editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
111 s.move_with(|map, selection| {
112 let mut cursor = selection.head();
113 cursor = map.clip_point(cursor, Bias::Left);
114 selection.collapse_to(cursor, selection.goal)
115 });
116 });
117 });
118 vim.switch_mode(Mode::Normal, cx);
119 });
120}
121
122pub fn delete_line(_: &mut Workspace, _: &VisualLineDelete, cx: &mut ViewContext<Workspace>) {
123 Vim::update(cx, |vim, cx| {
124 vim.update_active_editor(cx, |editor, cx| {
125 editor.set_clip_at_line_ends(false, cx);
126 let mut original_columns: HashMap<_, _> = Default::default();
127 editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
128 s.move_with(|map, selection| {
129 original_columns.insert(selection.id, selection.head().column());
130 selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
131
132 if selection.end.row() < map.max_point().row() {
133 *selection.end.row_mut() += 1;
134 *selection.end.column_mut() = 0;
135 selection.end = map.clip_point(selection.end, Bias::Right);
136 // Don't reset the end here
137 return;
138 } else if selection.start.row() > 0 {
139 *selection.start.row_mut() -= 1;
140 *selection.start.column_mut() = map.line_len(selection.start.row());
141 selection.start = map.clip_point(selection.start, Bias::Left);
142 }
143
144 selection.end = map.next_line_boundary(selection.end.to_point(map)).1;
145 });
146 });
147 copy_selections_content(editor, true, cx);
148 editor.insert("", cx);
149
150 // Fixup cursor position after the deletion
151 editor.set_clip_at_line_ends(true, cx);
152 editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
153 s.move_with(|map, selection| {
154 let mut cursor = selection.head();
155 if let Some(column) = original_columns.get(&selection.id) {
156 *cursor.column_mut() = *column
157 }
158 cursor = map.clip_point(cursor, Bias::Left);
159 selection.collapse_to(cursor, selection.goal)
160 });
161 });
162 });
163 vim.switch_mode(Mode::Normal, cx);
164 });
165}
166
167pub fn yank(_: &mut Workspace, _: &VisualYank, cx: &mut ViewContext<Workspace>) {
168 Vim::update(cx, |vim, cx| {
169 vim.update_active_editor(cx, |editor, cx| {
170 editor.set_clip_at_line_ends(false, cx);
171 editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
172 s.move_with(|map, selection| {
173 if !selection.reversed {
174 // Head is at the end of the selection. Adjust the end position to
175 // to include the character under the cursor.
176 *selection.end.column_mut() = selection.end.column() + 1;
177 selection.end = map.clip_point(selection.end, Bias::Left);
178 }
179 });
180 });
181 copy_selections_content(editor, false, cx);
182 });
183 vim.switch_mode(Mode::Normal, cx);
184 });
185}
186
187pub fn yank_line(_: &mut Workspace, _: &VisualLineYank, cx: &mut ViewContext<Workspace>) {
188 Vim::update(cx, |vim, cx| {
189 vim.update_active_editor(cx, |editor, cx| {
190 editor.set_clip_at_line_ends(false, cx);
191 let adjusted = editor.selections.all_adjusted(cx);
192 editor.change_selections(None, cx, |s| s.select(adjusted));
193 copy_selections_content(editor, true, cx);
194 });
195 vim.switch_mode(Mode::Normal, cx);
196 });
197}
198
199#[cfg(test)]
200mod test {
201 use indoc::indoc;
202
203 use crate::{state::Mode, vim_test_context::VimTestContext};
204
205 #[gpui::test]
206 async fn test_enter_visual_mode(cx: &mut gpui::TestAppContext) {
207 let cx = VimTestContext::new(cx, true).await;
208 let mut cx = cx.binding(["v", "w", "j"]).mode_after(Mode::Visual);
209 cx.assert(
210 indoc! {"
211 The |quick brown
212 fox jumps over
213 the lazy dog"},
214 indoc! {"
215 The [quick brown
216 fox jumps }over
217 the lazy dog"},
218 );
219 cx.assert(
220 indoc! {"
221 The quick brown
222 fox jumps over
223 the |lazy dog"},
224 indoc! {"
225 The quick brown
226 fox jumps over
227 the [lazy }dog"},
228 );
229 cx.assert(
230 indoc! {"
231 The quick brown
232 fox jumps |over
233 the lazy dog"},
234 indoc! {"
235 The quick brown
236 fox jumps [over
237 }the lazy dog"},
238 );
239 let mut cx = cx.binding(["v", "b", "k"]).mode_after(Mode::Visual);
240 cx.assert(
241 indoc! {"
242 The |quick brown
243 fox jumps over
244 the lazy dog"},
245 indoc! {"
246 {The q]uick brown
247 fox jumps over
248 the lazy dog"},
249 );
250 cx.assert(
251 indoc! {"
252 The quick brown
253 fox jumps over
254 the |lazy dog"},
255 indoc! {"
256 The quick brown
257 {fox jumps over
258 the l]azy dog"},
259 );
260 cx.assert(
261 indoc! {"
262 The quick brown
263 fox jumps |over
264 the lazy dog"},
265 indoc! {"
266 The {quick brown
267 fox jumps o]ver
268 the lazy dog"},
269 );
270 }
271
272 #[gpui::test]
273 async fn test_visual_delete(cx: &mut gpui::TestAppContext) {
274 let cx = VimTestContext::new(cx, true).await;
275 let mut cx = cx.binding(["v", "w", "x"]);
276 cx.assert("The quick |brown", "The quick| ");
277 let mut cx = cx.binding(["v", "w", "j", "x"]);
278 cx.assert(
279 indoc! {"
280 The |quick brown
281 fox jumps over
282 the lazy dog"},
283 indoc! {"
284 The |ver
285 the lazy dog"},
286 );
287 // Test pasting code copied on delete
288 cx.simulate_keystrokes(["j", "p"]);
289 cx.assert_editor_state(indoc! {"
290 The ver
291 the lazy d|quick brown
292 fox jumps oog"});
293
294 cx.assert(
295 indoc! {"
296 The quick brown
297 fox jumps over
298 the |lazy dog"},
299 indoc! {"
300 The quick brown
301 fox jumps over
302 the |og"},
303 );
304 cx.assert(
305 indoc! {"
306 The quick brown
307 fox jumps |over
308 the lazy dog"},
309 indoc! {"
310 The quick brown
311 fox jumps |he lazy dog"},
312 );
313 let mut cx = cx.binding(["v", "b", "k", "x"]);
314 cx.assert(
315 indoc! {"
316 The |quick brown
317 fox jumps over
318 the lazy dog"},
319 indoc! {"
320 |uick brown
321 fox jumps over
322 the lazy dog"},
323 );
324 cx.assert(
325 indoc! {"
326 The quick brown
327 fox jumps over
328 the |lazy dog"},
329 indoc! {"
330 The quick brown
331 |azy dog"},
332 );
333 cx.assert(
334 indoc! {"
335 The quick brown
336 fox jumps |over
337 the lazy dog"},
338 indoc! {"
339 The |ver
340 the lazy dog"},
341 );
342 }
343
344 #[gpui::test]
345 async fn test_visual_line_delete(cx: &mut gpui::TestAppContext) {
346 let cx = VimTestContext::new(cx, true).await;
347 let mut cx = cx.binding(["shift-V", "x"]);
348 cx.assert(
349 indoc! {"
350 The qu|ick brown
351 fox jumps over
352 the lazy dog"},
353 indoc! {"
354 fox ju|mps over
355 the lazy dog"},
356 );
357 // Test pasting code copied on delete
358 cx.simulate_keystroke("p");
359 cx.assert_editor_state(indoc! {"
360 fox jumps over
361 |The quick brown
362 the lazy dog"});
363
364 cx.assert(
365 indoc! {"
366 The quick brown
367 fox ju|mps over
368 the lazy dog"},
369 indoc! {"
370 The quick brown
371 the la|zy dog"},
372 );
373 cx.assert(
374 indoc! {"
375 The quick brown
376 fox jumps over
377 the la|zy dog"},
378 indoc! {"
379 The quick brown
380 fox ju|mps over"},
381 );
382 let mut cx = cx.binding(["shift-V", "j", "x"]);
383 cx.assert(
384 indoc! {"
385 The qu|ick brown
386 fox jumps over
387 the lazy dog"},
388 "the la|zy dog",
389 );
390 // Test pasting code copied on delete
391 cx.simulate_keystroke("p");
392 cx.assert_editor_state(indoc! {"
393 the lazy dog
394 |The quick brown
395 fox jumps over"});
396
397 cx.assert(
398 indoc! {"
399 The quick brown
400 fox ju|mps over
401 the lazy dog"},
402 "The qu|ick brown",
403 );
404 cx.assert(
405 indoc! {"
406 The quick brown
407 fox jumps over
408 the la|zy dog"},
409 indoc! {"
410 The quick brown
411 fox ju|mps over"},
412 );
413 }
414
415 #[gpui::test]
416 async fn test_visual_change(cx: &mut gpui::TestAppContext) {
417 let cx = VimTestContext::new(cx, true).await;
418 let mut cx = cx.binding(["v", "w", "c"]).mode_after(Mode::Insert);
419 cx.assert("The quick |brown", "The quick |");
420 let mut cx = cx.binding(["v", "w", "j", "c"]).mode_after(Mode::Insert);
421 cx.assert(
422 indoc! {"
423 The |quick brown
424 fox jumps over
425 the lazy dog"},
426 indoc! {"
427 The |ver
428 the lazy dog"},
429 );
430 cx.assert(
431 indoc! {"
432 The quick brown
433 fox jumps over
434 the |lazy dog"},
435 indoc! {"
436 The quick brown
437 fox jumps over
438 the |og"},
439 );
440 cx.assert(
441 indoc! {"
442 The quick brown
443 fox jumps |over
444 the lazy dog"},
445 indoc! {"
446 The quick brown
447 fox jumps |he lazy dog"},
448 );
449 let mut cx = cx.binding(["v", "b", "k", "c"]).mode_after(Mode::Insert);
450 cx.assert(
451 indoc! {"
452 The |quick brown
453 fox jumps over
454 the lazy dog"},
455 indoc! {"
456 |uick brown
457 fox jumps over
458 the lazy dog"},
459 );
460 cx.assert(
461 indoc! {"
462 The quick brown
463 fox jumps over
464 the |lazy dog"},
465 indoc! {"
466 The quick brown
467 |azy dog"},
468 );
469 cx.assert(
470 indoc! {"
471 The quick brown
472 fox jumps |over
473 the lazy dog"},
474 indoc! {"
475 The |ver
476 the lazy dog"},
477 );
478 }
479
480 #[gpui::test]
481 async fn test_visual_line_change(cx: &mut gpui::TestAppContext) {
482 let cx = VimTestContext::new(cx, true).await;
483 let mut cx = cx.binding(["shift-V", "c"]).mode_after(Mode::Insert);
484 cx.assert(
485 indoc! {"
486 The qu|ick brown
487 fox jumps over
488 the lazy dog"},
489 indoc! {"
490 |
491 fox jumps over
492 the lazy dog"},
493 );
494 // Test pasting code copied on change
495 cx.simulate_keystrokes(["escape", "j", "p"]);
496 cx.assert_editor_state(indoc! {"
497
498 fox jumps over
499 |The quick brown
500 the lazy dog"});
501
502 cx.assert(
503 indoc! {"
504 The quick brown
505 fox ju|mps over
506 the lazy dog"},
507 indoc! {"
508 The quick brown
509 |
510 the lazy dog"},
511 );
512 cx.assert(
513 indoc! {"
514 The quick brown
515 fox jumps over
516 the la|zy dog"},
517 indoc! {"
518 The quick brown
519 fox jumps over
520 |"},
521 );
522 let mut cx = cx.binding(["shift-V", "j", "c"]).mode_after(Mode::Insert);
523 cx.assert(
524 indoc! {"
525 The qu|ick brown
526 fox jumps over
527 the lazy dog"},
528 indoc! {"
529 |
530 the lazy dog"},
531 );
532 // Test pasting code copied on delete
533 cx.simulate_keystrokes(["escape", "j", "p"]);
534 cx.assert_editor_state(indoc! {"
535
536 the lazy dog
537 |The quick brown
538 fox jumps over"});
539 cx.assert(
540 indoc! {"
541 The quick brown
542 fox ju|mps over
543 the lazy dog"},
544 indoc! {"
545 The quick brown
546 |"},
547 );
548 cx.assert(
549 indoc! {"
550 The quick brown
551 fox jumps over
552 the la|zy dog"},
553 indoc! {"
554 The quick brown
555 fox jumps over
556 |"},
557 );
558 }
559
560 #[gpui::test]
561 async fn test_visual_yank(cx: &mut gpui::TestAppContext) {
562 let cx = VimTestContext::new(cx, true).await;
563 let mut cx = cx.binding(["v", "w", "y"]);
564 cx.assert("The quick |brown", "The quick |brown");
565 cx.assert_clipboard_content(Some("brown"));
566 let mut cx = cx.binding(["v", "w", "j", "y"]);
567 cx.assert(
568 indoc! {"
569 The |quick brown
570 fox jumps over
571 the lazy dog"},
572 indoc! {"
573 The |quick brown
574 fox jumps over
575 the lazy dog"},
576 );
577 cx.assert_clipboard_content(Some(indoc! {"
578 quick brown
579 fox jumps ov"}));
580 cx.assert(
581 indoc! {"
582 The quick brown
583 fox jumps over
584 the |lazy dog"},
585 indoc! {"
586 The quick brown
587 fox jumps over
588 the |lazy dog"},
589 );
590 cx.assert_clipboard_content(Some("lazy d"));
591 cx.assert(
592 indoc! {"
593 The quick brown
594 fox jumps |over
595 the lazy dog"},
596 indoc! {"
597 The quick brown
598 fox jumps |over
599 the lazy dog"},
600 );
601 cx.assert_clipboard_content(Some(indoc! {"
602 over
603 t"}));
604 let mut cx = cx.binding(["v", "b", "k", "y"]);
605 cx.assert(
606 indoc! {"
607 The |quick brown
608 fox jumps over
609 the lazy dog"},
610 indoc! {"
611 The |quick brown
612 fox jumps over
613 the lazy dog"},
614 );
615 cx.assert_clipboard_content(Some("The q"));
616 cx.assert(
617 indoc! {"
618 The quick brown
619 fox jumps over
620 the |lazy dog"},
621 indoc! {"
622 The quick brown
623 fox jumps over
624 the |lazy dog"},
625 );
626 cx.assert_clipboard_content(Some(indoc! {"
627 fox jumps over
628 the l"}));
629 cx.assert(
630 indoc! {"
631 The quick brown
632 fox jumps |over
633 the lazy dog"},
634 indoc! {"
635 The quick brown
636 fox jumps |over
637 the lazy dog"},
638 );
639 cx.assert_clipboard_content(Some(indoc! {"
640 quick brown
641 fox jumps o"}));
642 }
643}