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