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 VisualChange,
13 VisualLineDelete,
14 VisualLineChange
15 ]
16);
17
18pub fn init(cx: &mut MutableAppContext) {
19 cx.add_action(change);
20 cx.add_action(change_line);
21 cx.add_action(delete);
22 cx.add_action(delete_line);
23}
24
25pub fn visual_motion(motion: Motion, cx: &mut MutableAppContext) {
26 Vim::update(cx, |vim, cx| {
27 vim.update_active_editor(cx, |editor, cx| {
28 editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
29 s.move_with(|map, selection| {
30 let (new_head, goal) = motion.move_point(map, selection.head(), selection.goal);
31 let new_head = map.clip_at_line_end(new_head);
32 let was_reversed = selection.reversed;
33 selection.set_head(new_head, goal);
34
35 if was_reversed && !selection.reversed {
36 // Head was at the start of the selection, and now is at the end. We need to move the start
37 // back by one if possible in order to compensate for this change.
38 *selection.start.column_mut() = selection.start.column().saturating_sub(1);
39 selection.start = map.clip_point(selection.start, Bias::Left);
40 } else if !was_reversed && selection.reversed {
41 // Head was at the end of the selection, and now is at the start. We need to move the end
42 // forward by one if possible in order to compensate for this change.
43 *selection.end.column_mut() = selection.end.column() + 1;
44 selection.end = map.clip_point(selection.end, Bias::Right);
45 }
46 });
47 });
48 });
49 });
50}
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 editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
57 s.move_with(|map, selection| {
58 if !selection.reversed {
59 // Head was at the end of the selection, and now is at the start. We need to move the end
60 // forward by one if possible in order to compensate for this change.
61 *selection.end.column_mut() = selection.end.column() + 1;
62 selection.end = map.clip_point(selection.end, Bias::Left);
63 }
64 });
65 });
66 copy_selections_content(editor, false, cx);
67 editor.insert("", cx);
68 });
69 vim.switch_mode(Mode::Insert, cx);
70 });
71}
72
73pub fn change_line(_: &mut Workspace, _: &VisualLineChange, cx: &mut ViewContext<Workspace>) {
74 Vim::update(cx, |vim, cx| {
75 vim.update_active_editor(cx, |editor, cx| {
76 editor.set_clip_at_line_ends(false, cx);
77 editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
78 s.move_with(|map, selection| {
79 selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
80 selection.end = map.next_line_boundary(selection.end.to_point(map)).1;
81 });
82 });
83 copy_selections_content(editor, true, cx);
84 editor.insert("", cx);
85 });
86 vim.switch_mode(Mode::Insert, cx);
87 });
88}
89
90pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext<Workspace>) {
91 Vim::update(cx, |vim, cx| {
92 vim.update_active_editor(cx, |editor, cx| {
93 editor.set_clip_at_line_ends(false, cx);
94 editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
95 s.move_with(|map, selection| {
96 if !selection.reversed {
97 // Head is at the end of the selection. Adjust the end position to
98 // to include the character under the cursor.
99 *selection.end.column_mut() = selection.end.column() + 1;
100 selection.end = map.clip_point(selection.end, Bias::Right);
101 }
102 });
103 });
104 copy_selections_content(editor, false, cx);
105 editor.insert("", cx);
106
107 // Fixup cursor position after the deletion
108 editor.set_clip_at_line_ends(true, cx);
109 editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
110 s.move_with(|map, selection| {
111 let mut cursor = selection.head();
112 cursor = map.clip_point(cursor, Bias::Left);
113 selection.collapse_to(cursor, selection.goal)
114 });
115 });
116 });
117 vim.switch_mode(Mode::Normal, cx);
118 });
119}
120
121pub fn delete_line(_: &mut Workspace, _: &VisualLineDelete, cx: &mut ViewContext<Workspace>) {
122 Vim::update(cx, |vim, cx| {
123 vim.update_active_editor(cx, |editor, cx| {
124 editor.set_clip_at_line_ends(false, cx);
125 let mut original_columns: HashMap<_, _> = Default::default();
126 editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
127 s.move_with(|map, selection| {
128 original_columns.insert(selection.id, selection.head().column());
129 selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
130
131 if selection.end.row() < map.max_point().row() {
132 *selection.end.row_mut() += 1;
133 *selection.end.column_mut() = 0;
134 // Don't reset the end here
135 return;
136 } else if selection.start.row() > 0 {
137 *selection.start.row_mut() -= 1;
138 *selection.start.column_mut() = map.line_len(selection.start.row());
139 }
140
141 selection.end = map.next_line_boundary(selection.end.to_point(map)).1;
142 });
143 });
144 copy_selections_content(editor, true, cx);
145 editor.insert("", cx);
146
147 // Fixup cursor position after the deletion
148 editor.set_clip_at_line_ends(true, cx);
149 editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
150 s.move_with(|map, selection| {
151 let mut cursor = selection.head();
152 if let Some(column) = original_columns.get(&selection.id) {
153 *cursor.column_mut() = *column
154 }
155 cursor = map.clip_point(cursor, Bias::Left);
156 selection.collapse_to(cursor, selection.goal)
157 });
158 });
159 });
160 vim.switch_mode(Mode::Normal, cx);
161 });
162}
163
164#[cfg(test)]
165mod test {
166 use indoc::indoc;
167
168 use crate::{state::Mode, vim_test_context::VimTestContext};
169
170 #[gpui::test]
171 async fn test_enter_visual_mode(cx: &mut gpui::TestAppContext) {
172 let cx = VimTestContext::new(cx, true).await;
173 let mut cx = cx.binding(["v", "w", "j"]).mode_after(Mode::Visual);
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.binding(["v", "b", "k"]).mode_after(Mode::Visual);
205 cx.assert(
206 indoc! {"
207 The |quick brown
208 fox jumps over
209 the lazy dog"},
210 indoc! {"
211 {The q]uick brown
212 fox jumps over
213 the lazy dog"},
214 );
215 cx.assert(
216 indoc! {"
217 The quick brown
218 fox jumps over
219 the |lazy dog"},
220 indoc! {"
221 The quick brown
222 {fox jumps over
223 the l]azy dog"},
224 );
225 cx.assert(
226 indoc! {"
227 The quick brown
228 fox jumps |over
229 the lazy dog"},
230 indoc! {"
231 The {quick brown
232 fox jumps o]ver
233 the lazy dog"},
234 );
235 }
236
237 #[gpui::test]
238 async fn test_visual_delete(cx: &mut gpui::TestAppContext) {
239 let cx = VimTestContext::new(cx, true).await;
240 let mut cx = cx.binding(["v", "w", "x"]);
241 cx.assert("The quick |brown", "The quick| ");
242 let mut cx = cx.binding(["v", "w", "j", "x"]);
243 cx.assert(
244 indoc! {"
245 The |quick brown
246 fox jumps over
247 the lazy dog"},
248 indoc! {"
249 The |ver
250 the lazy dog"},
251 );
252 // Test pasting code copied on delete
253 cx.simulate_keystrokes(["j", "p"]);
254 cx.assert_editor_state(indoc! {"
255 The ver
256 the lazy d|quick brown
257 fox jumps oog"});
258
259 cx.assert(
260 indoc! {"
261 The quick brown
262 fox jumps over
263 the |lazy dog"},
264 indoc! {"
265 The quick brown
266 fox jumps over
267 the |og"},
268 );
269 cx.assert(
270 indoc! {"
271 The quick brown
272 fox jumps |over
273 the lazy dog"},
274 indoc! {"
275 The quick brown
276 fox jumps |he lazy dog"},
277 );
278 let mut cx = cx.binding(["v", "b", "k", "x"]);
279 cx.assert(
280 indoc! {"
281 The |quick brown
282 fox jumps over
283 the lazy dog"},
284 indoc! {"
285 |uick brown
286 fox jumps over
287 the lazy dog"},
288 );
289 cx.assert(
290 indoc! {"
291 The quick brown
292 fox jumps over
293 the |lazy dog"},
294 indoc! {"
295 The quick brown
296 |azy dog"},
297 );
298 cx.assert(
299 indoc! {"
300 The quick brown
301 fox jumps |over
302 the lazy dog"},
303 indoc! {"
304 The |ver
305 the lazy dog"},
306 );
307 }
308
309 #[gpui::test]
310 async fn test_visual_line_delete(cx: &mut gpui::TestAppContext) {
311 let cx = VimTestContext::new(cx, true).await;
312 let mut cx = cx.binding(["shift-V", "x"]);
313 cx.assert(
314 indoc! {"
315 The qu|ick brown
316 fox jumps over
317 the lazy dog"},
318 indoc! {"
319 fox ju|mps over
320 the lazy dog"},
321 );
322 // Test pasting code copied on delete
323 cx.simulate_keystroke("p");
324 cx.assert_editor_state(indoc! {"
325 fox jumps over
326 |The quick brown
327 the lazy dog"});
328
329 cx.assert(
330 indoc! {"
331 The quick brown
332 fox ju|mps over
333 the lazy dog"},
334 indoc! {"
335 The quick brown
336 the la|zy dog"},
337 );
338 cx.assert(
339 indoc! {"
340 The quick brown
341 fox jumps over
342 the la|zy dog"},
343 indoc! {"
344 The quick brown
345 fox ju|mps over"},
346 );
347 let mut cx = cx.binding(["shift-V", "j", "x"]);
348 cx.assert(
349 indoc! {"
350 The qu|ick brown
351 fox jumps over
352 the lazy dog"},
353 "the la|zy dog",
354 );
355 // Test pasting code copied on delete
356 cx.simulate_keystroke("p");
357 cx.assert_editor_state(indoc! {"
358 the lazy dog
359 |The quick brown
360 fox jumps over"});
361
362 cx.assert(
363 indoc! {"
364 The quick brown
365 fox ju|mps over
366 the lazy dog"},
367 "The qu|ick brown",
368 );
369 cx.assert(
370 indoc! {"
371 The quick brown
372 fox jumps over
373 the la|zy dog"},
374 indoc! {"
375 The quick brown
376 fox ju|mps over"},
377 );
378 }
379
380 #[gpui::test]
381 async fn test_visual_change(cx: &mut gpui::TestAppContext) {
382 let cx = VimTestContext::new(cx, true).await;
383 let mut cx = cx.binding(["v", "w", "c"]).mode_after(Mode::Insert);
384 cx.assert("The quick |brown", "The quick |");
385 let mut cx = cx.binding(["v", "w", "j", "c"]).mode_after(Mode::Insert);
386 cx.assert(
387 indoc! {"
388 The |quick brown
389 fox jumps over
390 the lazy dog"},
391 indoc! {"
392 The |ver
393 the lazy dog"},
394 );
395 cx.assert(
396 indoc! {"
397 The quick brown
398 fox jumps over
399 the |lazy dog"},
400 indoc! {"
401 The quick brown
402 fox jumps over
403 the |og"},
404 );
405 cx.assert(
406 indoc! {"
407 The quick brown
408 fox jumps |over
409 the lazy dog"},
410 indoc! {"
411 The quick brown
412 fox jumps |he lazy dog"},
413 );
414 let mut cx = cx.binding(["v", "b", "k", "c"]).mode_after(Mode::Insert);
415 cx.assert(
416 indoc! {"
417 The |quick brown
418 fox jumps over
419 the lazy dog"},
420 indoc! {"
421 |uick brown
422 fox jumps over
423 the lazy dog"},
424 );
425 cx.assert(
426 indoc! {"
427 The quick brown
428 fox jumps over
429 the |lazy dog"},
430 indoc! {"
431 The quick brown
432 |azy dog"},
433 );
434 cx.assert(
435 indoc! {"
436 The quick brown
437 fox jumps |over
438 the lazy dog"},
439 indoc! {"
440 The |ver
441 the lazy dog"},
442 );
443 }
444
445 #[gpui::test]
446 async fn test_visual_line_change(cx: &mut gpui::TestAppContext) {
447 let cx = VimTestContext::new(cx, true).await;
448 let mut cx = cx.binding(["shift-V", "c"]).mode_after(Mode::Insert);
449 cx.assert(
450 indoc! {"
451 The qu|ick brown
452 fox jumps over
453 the lazy dog"},
454 indoc! {"
455 |
456 fox jumps over
457 the lazy dog"},
458 );
459 // Test pasting code copied on change
460 cx.simulate_keystrokes(["escape", "j", "p"]);
461 cx.assert_editor_state(indoc! {"
462
463 fox jumps over
464 |The quick brown
465 the lazy dog"});
466
467 cx.assert(
468 indoc! {"
469 The quick brown
470 fox ju|mps over
471 the lazy dog"},
472 indoc! {"
473 The quick brown
474 |
475 the lazy dog"},
476 );
477 cx.assert(
478 indoc! {"
479 The quick brown
480 fox jumps over
481 the la|zy dog"},
482 indoc! {"
483 The quick brown
484 fox jumps over
485 |"},
486 );
487 let mut cx = cx.binding(["shift-V", "j", "c"]).mode_after(Mode::Insert);
488 cx.assert(
489 indoc! {"
490 The qu|ick brown
491 fox jumps over
492 the lazy dog"},
493 indoc! {"
494 |
495 the lazy dog"},
496 );
497 // Test pasting code copied on delete
498 cx.simulate_keystrokes(["escape", "j", "p"]);
499 cx.assert_editor_state(indoc! {"
500
501 the lazy dog
502 |The quick brown
503 fox jumps over"});
504 cx.assert(
505 indoc! {"
506 The quick brown
507 fox ju|mps over
508 the lazy dog"},
509 indoc! {"
510 The quick brown
511 |"},
512 );
513 cx.assert(
514 indoc! {"
515 The quick brown
516 fox jumps over
517 the la|zy dog"},
518 indoc! {"
519 The quick brown
520 fox jumps over
521 |"},
522 );
523 }
524}