visual.rs

  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}