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