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        cx.assert(
253            indoc! {"
254                The quick brown
255                fox jumps over
256                the |lazy dog"},
257            indoc! {"
258                The quick brown
259                fox jumps over
260                the |og"},
261        );
262        cx.assert(
263            indoc! {"
264                The quick brown
265                fox jumps |over
266                the lazy dog"},
267            indoc! {"
268                The quick brown
269                fox jumps |he lazy dog"},
270        );
271        let mut cx = cx.binding(["v", "b", "k", "x"]);
272        cx.assert(
273            indoc! {"
274                The |quick brown
275                fox jumps over
276                the lazy dog"},
277            indoc! {"
278                |uick brown
279                fox jumps over
280                the lazy dog"},
281        );
282        cx.assert(
283            indoc! {"
284                The quick brown
285                fox jumps over
286                the |lazy dog"},
287            indoc! {"
288                The quick brown
289                |azy dog"},
290        );
291        cx.assert(
292            indoc! {"
293                The quick brown
294                fox jumps |over
295                the lazy dog"},
296            indoc! {"
297                The |ver
298                the lazy dog"},
299        );
300    }
301
302    #[gpui::test]
303    async fn test_visual_change(cx: &mut gpui::TestAppContext) {
304        let cx = VimTestContext::new(cx, true).await;
305        let mut cx = cx.binding(["v", "w", "c"]).mode_after(Mode::Insert);
306        cx.assert("The quick |brown", "The quick |");
307        let mut cx = cx.binding(["v", "w", "j", "c"]).mode_after(Mode::Insert);
308        cx.assert(
309            indoc! {"
310                The |quick brown
311                fox jumps over
312                the lazy dog"},
313            indoc! {"
314                The |ver
315                the lazy dog"},
316        );
317        cx.assert(
318            indoc! {"
319                The quick brown
320                fox jumps over
321                the |lazy dog"},
322            indoc! {"
323                The quick brown
324                fox jumps over
325                the |og"},
326        );
327        cx.assert(
328            indoc! {"
329                The quick brown
330                fox jumps |over
331                the lazy dog"},
332            indoc! {"
333                The quick brown
334                fox jumps |he lazy dog"},
335        );
336        let mut cx = cx.binding(["v", "b", "k", "c"]).mode_after(Mode::Insert);
337        cx.assert(
338            indoc! {"
339                The |quick brown
340                fox jumps over
341                the lazy dog"},
342            indoc! {"
343                |uick brown
344                fox jumps over
345                the lazy dog"},
346        );
347        cx.assert(
348            indoc! {"
349                The quick brown
350                fox jumps over
351                the |lazy dog"},
352            indoc! {"
353                The quick brown
354                |azy dog"},
355        );
356        cx.assert(
357            indoc! {"
358                The quick brown
359                fox jumps |over
360                the lazy dog"},
361            indoc! {"
362                The |ver
363                the lazy dog"},
364        );
365    }
366}