visual.rs

  1use collections::HashMap;
  2use editor::{Autoscroll, Bias};
  3use gpui::{actions, MutableAppContext, ViewContext};
  4use workspace::Workspace;
  5
  6use crate::{motion::Motion, state::Mode, 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::Left);
 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            editor.insert("", cx);
 67        });
 68        vim.switch_mode(Mode::Insert, cx);
 69    });
 70}
 71
 72pub fn change_line(_: &mut Workspace, _: &VisualLineChange, cx: &mut ViewContext<Workspace>) {
 73    Vim::update(cx, |vim, cx| {
 74        vim.update_active_editor(cx, |editor, cx| {
 75            editor.set_clip_at_line_ends(false, cx);
 76            editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
 77                s.move_with(|map, selection| {
 78                    selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
 79                    selection.end = map.next_line_boundary(selection.end.to_point(map)).1;
 80                });
 81            });
 82            editor.insert("", cx);
 83        });
 84        vim.switch_mode(Mode::Insert, cx);
 85    });
 86}
 87
 88pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext<Workspace>) {
 89    Vim::update(cx, |vim, cx| {
 90        vim.switch_mode(Mode::Normal, cx);
 91        vim.update_active_editor(cx, |editor, cx| {
 92            editor.set_clip_at_line_ends(false, cx);
 93            editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
 94                s.move_with(|map, selection| {
 95                    if !selection.reversed {
 96                        // Head was at the end of the selection, and now is at the start. We need to move the end
 97                        // forward by one if possible in order to compensate for this change.
 98                        *selection.end.column_mut() = selection.end.column() + 1;
 99                        selection.end = map.clip_point(selection.end, Bias::Left);
100                    }
101                });
102            });
103            editor.insert("", cx);
104
105            // Fixup cursor position after the deletion
106            editor.set_clip_at_line_ends(true, cx);
107            editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
108                s.move_with(|map, selection| {
109                    let mut cursor = selection.head();
110                    cursor = map.clip_point(cursor, Bias::Left);
111                    selection.collapse_to(cursor, selection.goal)
112                });
113            });
114        });
115    });
116}
117
118pub fn delete_line(_: &mut Workspace, _: &VisualLineDelete, cx: &mut ViewContext<Workspace>) {
119    Vim::update(cx, |vim, cx| {
120        vim.update_active_editor(cx, |editor, cx| {
121            editor.set_clip_at_line_ends(false, cx);
122            let mut original_columns: HashMap<_, _> = Default::default();
123            editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
124                s.move_with(|map, selection| {
125                    original_columns.insert(selection.id, selection.head().column());
126                    selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
127
128                    if selection.end.row() < map.max_point().row() {
129                        *selection.end.row_mut() += 1;
130                        *selection.end.column_mut() = 0;
131                        // Don't reset the end here
132                        return;
133                    } else if selection.start.row() > 0 {
134                        *selection.start.row_mut() -= 1;
135                        *selection.start.column_mut() = map.line_len(selection.start.row());
136                    }
137
138                    selection.end = map.next_line_boundary(selection.end.to_point(map)).1;
139                });
140            });
141            editor.insert("", cx);
142
143            // Fixup cursor position after the deletion
144            editor.set_clip_at_line_ends(true, cx);
145            editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
146                s.move_with(|map, selection| {
147                    let mut cursor = selection.head();
148                    if let Some(column) = original_columns.get(&selection.id) {
149                        *cursor.column_mut() = *column
150                    }
151                    cursor = map.clip_point(cursor, Bias::Left);
152                    selection.collapse_to(cursor, selection.goal)
153                });
154            });
155        });
156        vim.switch_mode(Mode::Normal, cx);
157    });
158}
159
160#[cfg(test)]
161mod test {
162    use indoc::indoc;
163
164    use crate::{state::Mode, vim_test_context::VimTestContext};
165
166    #[gpui::test]
167    async fn test_enter_visual_mode(cx: &mut gpui::TestAppContext) {
168        let cx = VimTestContext::new(cx, true).await;
169        let mut cx = cx.binding(["v", "w", "j"]).mode_after(Mode::Visual);
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.binding(["v", "b", "k"]).mode_after(Mode::Visual);
201        cx.assert(
202            indoc! {"
203                The |quick brown
204                fox jumps over
205                the lazy dog"},
206            indoc! {"
207                {The q]uick brown
208                fox jumps over
209                the lazy dog"},
210        );
211        cx.assert(
212            indoc! {"
213                The quick brown
214                fox jumps over
215                the |lazy dog"},
216            indoc! {"
217                The quick brown
218                {fox jumps over
219                the l]azy dog"},
220        );
221        cx.assert(
222            indoc! {"
223                The quick brown
224                fox jumps |over
225                the lazy dog"},
226            indoc! {"
227                The {quick brown
228                fox jumps o]ver
229                the lazy dog"},
230        );
231    }
232
233    #[gpui::test]
234    async fn test_visual_delete(cx: &mut gpui::TestAppContext) {
235        let cx = VimTestContext::new(cx, true).await;
236        let mut cx = cx.binding(["v", "w", "x"]);
237        cx.assert("The quick |brown", "The quick| ");
238        let mut cx = cx.binding(["v", "w", "j", "x"]);
239        cx.assert(
240            indoc! {"
241                The |quick brown
242                fox jumps over
243                the lazy dog"},
244            indoc! {"
245                The |ver
246                the lazy dog"},
247        );
248        cx.assert(
249            indoc! {"
250                The quick brown
251                fox jumps over
252                the |lazy dog"},
253            indoc! {"
254                The quick brown
255                fox jumps over
256                the |og"},
257        );
258        cx.assert(
259            indoc! {"
260                The quick brown
261                fox jumps |over
262                the lazy dog"},
263            indoc! {"
264                The quick brown
265                fox jumps |he lazy dog"},
266        );
267        let mut cx = cx.binding(["v", "b", "k", "x"]);
268        cx.assert(
269            indoc! {"
270                The |quick brown
271                fox jumps over
272                the lazy dog"},
273            indoc! {"
274                |uick brown
275                fox jumps over
276                the lazy dog"},
277        );
278        cx.assert(
279            indoc! {"
280                The quick brown
281                fox jumps over
282                the |lazy dog"},
283            indoc! {"
284                The quick brown
285                |azy dog"},
286        );
287        cx.assert(
288            indoc! {"
289                The quick brown
290                fox jumps |over
291                the lazy dog"},
292            indoc! {"
293                The |ver
294                the lazy dog"},
295        );
296    }
297
298    #[gpui::test]
299    async fn test_visual_change(cx: &mut gpui::TestAppContext) {
300        let cx = VimTestContext::new(cx, true).await;
301        let mut cx = cx.binding(["v", "w", "c"]).mode_after(Mode::Insert);
302        cx.assert("The quick |brown", "The quick |");
303        let mut cx = cx.binding(["v", "w", "j", "c"]).mode_after(Mode::Insert);
304        cx.assert(
305            indoc! {"
306                The |quick brown
307                fox jumps over
308                the lazy dog"},
309            indoc! {"
310                The |ver
311                the lazy dog"},
312        );
313        cx.assert(
314            indoc! {"
315                The quick brown
316                fox jumps over
317                the |lazy dog"},
318            indoc! {"
319                The quick brown
320                fox jumps over
321                the |og"},
322        );
323        cx.assert(
324            indoc! {"
325                The quick brown
326                fox jumps |over
327                the lazy dog"},
328            indoc! {"
329                The quick brown
330                fox jumps |he lazy dog"},
331        );
332        let mut cx = cx.binding(["v", "b", "k", "c"]).mode_after(Mode::Insert);
333        cx.assert(
334            indoc! {"
335                The |quick brown
336                fox jumps over
337                the lazy dog"},
338            indoc! {"
339                |uick brown
340                fox jumps over
341                the lazy dog"},
342        );
343        cx.assert(
344            indoc! {"
345                The quick brown
346                fox jumps over
347                the |lazy dog"},
348            indoc! {"
349                The quick brown
350                |azy dog"},
351        );
352        cx.assert(
353            indoc! {"
354                The quick brown
355                fox jumps |over
356                the lazy dog"},
357            indoc! {"
358                The |ver
359                the lazy dog"},
360        );
361    }
362}