visual.rs

  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}