visual.rs

  1use editor::{Autoscroll, Bias};
  2use gpui::{actions, MutableAppContext, ViewContext};
  3use workspace::Workspace;
  4
  5use crate::{motion::Motion, state::Mode, Vim};
  6
  7actions!(
  8    vim,
  9    [
 10        VisualDelete,
 11        VisualChange,
 12        VisualLineDelete,
 13        VisualLineChange
 14    ]
 15);
 16
 17pub fn init(cx: &mut MutableAppContext) {
 18    cx.add_action(change);
 19    cx.add_action(change_line);
 20    cx.add_action(delete);
 21    cx.add_action(delete_line);
 22}
 23
 24pub fn visual_motion(motion: Motion, cx: &mut MutableAppContext) {
 25    Vim::update(cx, |vim, cx| {
 26        vim.update_active_editor(cx, |editor, cx| {
 27            editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
 28                s.move_with(|map, selection| {
 29                    let (new_head, goal) = motion.move_point(map, selection.head(), selection.goal);
 30                    let new_head = map.clip_at_line_end(new_head);
 31                    let was_reversed = selection.reversed;
 32                    selection.set_head(new_head, goal);
 33
 34                    if was_reversed && !selection.reversed {
 35                        // Head was at the start of the selection, and now is at the end. We need to move the start
 36                        // back by one if possible in order to compensate for this change.
 37                        *selection.start.column_mut() = selection.start.column().saturating_sub(1);
 38                        selection.start = map.clip_point(selection.start, Bias::Left);
 39                    } else if !was_reversed && selection.reversed {
 40                        // Head was at the end of the selection, and now is at the start. We need to move the end
 41                        // forward by one if possible in order to compensate for this change.
 42                        *selection.end.column_mut() = selection.end.column() + 1;
 43                        selection.end = map.clip_point(selection.end, Bias::Left);
 44                    }
 45                });
 46            });
 47        });
 48    });
 49}
 50
 51pub fn change(_: &mut Workspace, _: &VisualChange, cx: &mut ViewContext<Workspace>) {
 52    Vim::update(cx, |vim, cx| {
 53        vim.update_active_editor(cx, |editor, cx| {
 54            editor.set_clip_at_line_ends(false, cx);
 55            editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
 56                s.move_with(|map, selection| {
 57                    if !selection.reversed {
 58                        // Head was at the end of the selection, and now is at the start. We need to move the end
 59                        // forward by one if possible in order to compensate for this change.
 60                        *selection.end.column_mut() = selection.end.column() + 1;
 61                        selection.end = map.clip_point(selection.end, Bias::Left);
 62                    }
 63                });
 64            });
 65            editor.insert("", cx);
 66        });
 67        vim.switch_mode(Mode::Insert, cx);
 68    });
 69}
 70
 71pub fn change_line(_: &mut Workspace, _: &VisualChange, cx: &mut ViewContext<Workspace>) {
 72    Vim::update(cx, |vim, cx| {
 73        vim.update_active_editor(cx, |editor, cx| {
 74            editor.set_clip_at_line_ends(false, cx);
 75            editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
 76                s.move_with(|map, selection| {
 77                    selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
 78                    selection.end = map.next_line_boundary(selection.end.to_point(map)).1;
 79                });
 80            });
 81            editor.insert("", cx);
 82        });
 83        vim.switch_mode(Mode::Insert, cx);
 84    });
 85}
 86
 87pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext<Workspace>) {
 88    Vim::update(cx, |vim, cx| {
 89        vim.switch_mode(Mode::Normal, cx);
 90        vim.update_active_editor(cx, |editor, cx| {
 91            editor.set_clip_at_line_ends(false, cx);
 92            editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
 93                s.move_with(|map, selection| {
 94                    if !selection.reversed {
 95                        // Head was at the end of the selection, and now is at the start. We need to move the end
 96                        // forward by one if possible in order to compensate for this change.
 97                        *selection.end.column_mut() = selection.end.column() + 1;
 98                        selection.end = map.clip_point(selection.end, Bias::Left);
 99                    }
100                });
101            });
102            editor.insert("", cx);
103
104            // Fixup cursor position after the deletion
105            editor.set_clip_at_line_ends(true, cx);
106            editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
107                s.move_with(|map, selection| {
108                    let mut cursor = selection.head();
109                    cursor = map.clip_point(cursor, Bias::Left);
110                    selection.collapse_to(cursor, selection.goal)
111                });
112            });
113        });
114    });
115}
116
117pub fn delete_line(_: &mut Workspace, _: &VisualChange, cx: &mut ViewContext<Workspace>) {
118    Vim::update(cx, |vim, cx| {
119        vim.switch_mode(Mode::Normal, cx);
120        vim.update_active_editor(cx, |editor, cx| {
121            editor.set_clip_at_line_ends(false, cx);
122            editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
123                s.move_with(|map, selection| {
124                    selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
125
126                    if selection.end.row() < map.max_point().row() {
127                        *selection.end.row_mut() += 1;
128                        *selection.end.column_mut() = 0;
129                        // Don't reset the end here
130                        return;
131                    } else if selection.start.row() > 0 {
132                        *selection.start.row_mut() -= 1;
133                        *selection.start.column_mut() = map.line_len(selection.start.row());
134                    }
135
136                    selection.end = map.next_line_boundary(selection.end.to_point(map)).1;
137                });
138            });
139            editor.insert("", cx);
140
141            // Fixup cursor position after the deletion
142            editor.set_clip_at_line_ends(true, cx);
143            editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
144                s.move_with(|map, selection| {
145                    let mut cursor = selection.head();
146                    cursor = map.clip_point(cursor, Bias::Left);
147                    selection.collapse_to(cursor, selection.goal)
148                });
149            });
150        });
151    });
152}
153
154#[cfg(test)]
155mod test {
156    use indoc::indoc;
157
158    use crate::{state::Mode, vim_test_context::VimTestContext};
159
160    #[gpui::test]
161    async fn test_enter_visual_mode(cx: &mut gpui::TestAppContext) {
162        let cx = VimTestContext::new(cx, true).await;
163        let mut cx = cx.binding(["v", "w", "j"]).mode_after(Mode::Visual);
164        cx.assert(
165            indoc! {"
166                The |quick brown
167                fox jumps over
168                the lazy dog"},
169            indoc! {"
170                The [quick brown
171                fox jumps }over
172                the lazy dog"},
173        );
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        let mut cx = cx.binding(["v", "b", "k"]).mode_after(Mode::Visual);
195        cx.assert(
196            indoc! {"
197                The |quick brown
198                fox jumps over
199                the lazy dog"},
200            indoc! {"
201                {The q]uick brown
202                fox jumps over
203                the lazy dog"},
204        );
205        cx.assert(
206            indoc! {"
207                The quick brown
208                fox jumps over
209                the |lazy dog"},
210            indoc! {"
211                The quick brown
212                {fox jumps over
213                the l]azy 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 o]ver
223                the lazy dog"},
224        );
225    }
226
227    #[gpui::test]
228    async fn test_visual_delete(cx: &mut gpui::TestAppContext) {
229        let cx = VimTestContext::new(cx, true).await;
230        let mut cx = cx.binding(["v", "w", "x"]);
231        cx.assert("The quick |brown", "The quick| ");
232        let mut cx = cx.binding(["v", "w", "j", "x"]);
233        cx.assert(
234            indoc! {"
235                The |quick brown
236                fox jumps over
237                the lazy dog"},
238            indoc! {"
239                The |ver
240                the lazy dog"},
241        );
242        cx.assert(
243            indoc! {"
244                The quick brown
245                fox jumps over
246                the |lazy dog"},
247            indoc! {"
248                The quick brown
249                fox jumps over
250                the |og"},
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 |he lazy dog"},
260        );
261        let mut cx = cx.binding(["v", "b", "k", "x"]);
262        cx.assert(
263            indoc! {"
264                The |quick brown
265                fox jumps over
266                the lazy dog"},
267            indoc! {"
268                |uick brown
269                fox jumps over
270                the lazy dog"},
271        );
272        cx.assert(
273            indoc! {"
274                The quick brown
275                fox jumps over
276                the |lazy dog"},
277            indoc! {"
278                The quick brown
279                |azy dog"},
280        );
281        cx.assert(
282            indoc! {"
283                The quick brown
284                fox jumps |over
285                the lazy dog"},
286            indoc! {"
287                The |ver
288                the lazy dog"},
289        );
290    }
291
292    #[gpui::test]
293    async fn test_visual_change(cx: &mut gpui::TestAppContext) {
294        let cx = VimTestContext::new(cx, true).await;
295        let mut cx = cx.binding(["v", "w", "c"]).mode_after(Mode::Insert);
296        cx.assert("The quick |brown", "The quick |");
297        let mut cx = cx.binding(["v", "w", "j", "c"]).mode_after(Mode::Insert);
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        cx.assert(
308            indoc! {"
309                The quick brown
310                fox jumps over
311                the |lazy dog"},
312            indoc! {"
313                The quick brown
314                fox jumps over
315                the |og"},
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 |he lazy dog"},
325        );
326        let mut cx = cx.binding(["v", "b", "k", "c"]).mode_after(Mode::Insert);
327        cx.assert(
328            indoc! {"
329                The |quick brown
330                fox jumps over
331                the lazy dog"},
332            indoc! {"
333                |uick brown
334                fox jumps over
335                the lazy dog"},
336        );
337        cx.assert(
338            indoc! {"
339                The quick brown
340                fox jumps over
341                the |lazy dog"},
342            indoc! {"
343                The quick brown
344                |azy dog"},
345        );
346        cx.assert(
347            indoc! {"
348                The quick brown
349                fox jumps |over
350                the lazy dog"},
351            indoc! {"
352                The |ver
353                the lazy dog"},
354        );
355    }
356}