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