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::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}