normal.rs

  1mod g_prefix;
  2
  3use crate::VimState;
  4use editor::{char_kind, movement, Bias};
  5use gpui::{actions, impl_actions, MutableAppContext, ViewContext};
  6use language::SelectionGoal;
  7use serde::Deserialize;
  8use workspace::Workspace;
  9
 10#[derive(Clone, Deserialize)]
 11struct MoveToNextWordStart(pub bool);
 12
 13#[derive(Clone, Deserialize)]
 14struct MoveToNextWordEnd(pub bool);
 15
 16#[derive(Clone, Deserialize)]
 17struct MoveToPreviousWordStart(pub bool);
 18
 19impl_actions!(
 20    vim,
 21    [
 22        MoveToNextWordStart,
 23        MoveToNextWordEnd,
 24        MoveToPreviousWordStart,
 25    ]
 26);
 27
 28actions!(
 29    vim,
 30    [
 31        GPrefix,
 32        MoveLeft,
 33        MoveDown,
 34        MoveUp,
 35        MoveRight,
 36        MoveToStartOfLine,
 37        MoveToEndOfLine,
 38        MoveToEnd,
 39    ]
 40);
 41
 42pub fn init(cx: &mut MutableAppContext) {
 43    g_prefix::init(cx);
 44    cx.add_action(move_left);
 45    cx.add_action(move_down);
 46    cx.add_action(move_up);
 47    cx.add_action(move_right);
 48    cx.add_action(move_to_start_of_line);
 49    cx.add_action(move_to_end_of_line);
 50    cx.add_action(move_to_end);
 51    cx.add_action(move_to_next_word_start);
 52    cx.add_action(move_to_next_word_end);
 53    cx.add_action(move_to_previous_word_start);
 54}
 55
 56fn move_left(_: &mut Workspace, _: &MoveLeft, cx: &mut ViewContext<Workspace>) {
 57    VimState::update_global(cx, |state, cx| {
 58        state.update_active_editor(cx, |editor, cx| {
 59            editor.move_cursors(cx, |map, mut cursor, _| {
 60                *cursor.column_mut() = cursor.column().saturating_sub(1);
 61                (map.clip_point(cursor, Bias::Left), SelectionGoal::None)
 62            });
 63        });
 64    })
 65}
 66
 67fn move_down(_: &mut Workspace, _: &MoveDown, cx: &mut ViewContext<Workspace>) {
 68    VimState::update_global(cx, |state, cx| {
 69        state.update_active_editor(cx, |editor, cx| {
 70            editor.move_cursors(cx, movement::down);
 71        });
 72    });
 73}
 74
 75fn move_up(_: &mut Workspace, _: &MoveUp, cx: &mut ViewContext<Workspace>) {
 76    VimState::update_global(cx, |state, cx| {
 77        state.update_active_editor(cx, |editor, cx| {
 78            editor.move_cursors(cx, movement::up);
 79        });
 80    });
 81}
 82
 83fn move_right(_: &mut Workspace, _: &MoveRight, cx: &mut ViewContext<Workspace>) {
 84    VimState::update_global(cx, |state, cx| {
 85        state.update_active_editor(cx, |editor, cx| {
 86            editor.move_cursors(cx, |map, mut cursor, _| {
 87                *cursor.column_mut() += 1;
 88                (map.clip_point(cursor, Bias::Right), SelectionGoal::None)
 89            });
 90        });
 91    });
 92}
 93
 94fn move_to_start_of_line(
 95    _: &mut Workspace,
 96    _: &MoveToStartOfLine,
 97    cx: &mut ViewContext<Workspace>,
 98) {
 99    VimState::update_global(cx, |state, cx| {
100        state.update_active_editor(cx, |editor, cx| {
101            editor.move_cursors(cx, |map, cursor, _| {
102                (
103                    movement::line_beginning(map, cursor, false),
104                    SelectionGoal::None,
105                )
106            });
107        });
108    });
109}
110
111fn move_to_end_of_line(_: &mut Workspace, _: &MoveToEndOfLine, cx: &mut ViewContext<Workspace>) {
112    VimState::update_global(cx, |state, cx| {
113        state.update_active_editor(cx, |editor, cx| {
114            editor.move_cursors(cx, |map, cursor, _| {
115                (
116                    map.clip_point(movement::line_end(map, cursor, false), Bias::Left),
117                    SelectionGoal::None,
118                )
119            });
120        });
121    });
122}
123
124fn move_to_end(_: &mut Workspace, _: &MoveToEnd, cx: &mut ViewContext<Workspace>) {
125    VimState::update_global(cx, |state, cx| {
126        state.update_active_editor(cx, |editor, cx| {
127            editor.replace_selections_with(cx, |map| map.clip_point(map.max_point(), Bias::Left));
128        });
129    });
130}
131
132fn move_to_next_word_start(
133    _: &mut Workspace,
134    &MoveToNextWordStart(treat_punctuation_as_word): &MoveToNextWordStart,
135    cx: &mut ViewContext<Workspace>,
136) {
137    VimState::update_global(cx, |state, cx| {
138        state.update_active_editor(cx, |editor, cx| {
139            editor.move_cursors(cx, |map, mut cursor, _| {
140                let mut crossed_newline = false;
141                cursor = movement::find_boundary(map, cursor, |left, right| {
142                    let left_kind = char_kind(left).coerce_punctuation(treat_punctuation_as_word);
143                    let right_kind = char_kind(right).coerce_punctuation(treat_punctuation_as_word);
144                    let at_newline = right == '\n';
145
146                    let found = (left_kind != right_kind && !right.is_whitespace())
147                        || (at_newline && crossed_newline)
148                        || (at_newline && left == '\n'); // Prevents skipping repeated empty lines
149
150                    if at_newline {
151                        crossed_newline = true;
152                    }
153                    found
154                });
155                (cursor, SelectionGoal::None)
156            });
157        });
158    });
159}
160
161fn move_to_next_word_end(
162    _: &mut Workspace,
163    &MoveToNextWordEnd(treat_punctuation_as_word): &MoveToNextWordEnd,
164    cx: &mut ViewContext<Workspace>,
165) {
166    VimState::update_global(cx, |state, cx| {
167        state.update_active_editor(cx, |editor, cx| {
168            editor.move_cursors(cx, |map, mut cursor, _| {
169                *cursor.column_mut() += 1;
170                cursor = movement::find_boundary(map, cursor, |left, right| {
171                    let left_kind = char_kind(left).coerce_punctuation(treat_punctuation_as_word);
172                    let right_kind = char_kind(right).coerce_punctuation(treat_punctuation_as_word);
173
174                    left_kind != right_kind && !left.is_whitespace()
175                });
176                // find_boundary clips, so if the character after the next character is a newline or at the end of the document, we know
177                // we have backtraced already
178                if !map
179                    .chars_at(cursor)
180                    .skip(1)
181                    .next()
182                    .map(|c| c == '\n')
183                    .unwrap_or(true)
184                {
185                    *cursor.column_mut() = cursor.column().saturating_sub(1);
186                }
187                (map.clip_point(cursor, Bias::Left), SelectionGoal::None)
188            });
189        });
190    });
191}
192
193fn move_to_previous_word_start(
194    _: &mut Workspace,
195    &MoveToPreviousWordStart(treat_punctuation_as_word): &MoveToPreviousWordStart,
196    cx: &mut ViewContext<Workspace>,
197) {
198    VimState::update_global(cx, |state, cx| {
199        state.update_active_editor(cx, |editor, cx| {
200            editor.move_cursors(cx, |map, mut cursor, _| {
201                // This works even though find_preceding_boundary is called for every character in the line containing
202                // cursor because the newline is checked only once.
203                cursor = movement::find_preceding_boundary(map, cursor, |left, right| {
204                    let left_kind = char_kind(left).coerce_punctuation(treat_punctuation_as_word);
205                    let right_kind = char_kind(right).coerce_punctuation(treat_punctuation_as_word);
206
207                    (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
208                });
209                (cursor, SelectionGoal::None)
210            });
211        });
212    });
213}
214
215#[cfg(test)]
216mod test {
217    use indoc::indoc;
218    use util::test::marked_text;
219
220    use crate::vim_test_context::VimTestContext;
221
222    #[gpui::test]
223    async fn test_hjkl(cx: &mut gpui::TestAppContext) {
224        let mut cx = VimTestContext::new(cx, true, "Test\nTestTest\nTest").await;
225        cx.simulate_keystroke("l");
226        cx.assert_editor_state(indoc! {"
227            T|est
228            TestTest
229            Test"});
230        cx.simulate_keystroke("h");
231        cx.assert_editor_state(indoc! {"
232            |Test
233            TestTest
234            Test"});
235        cx.simulate_keystroke("j");
236        cx.assert_editor_state(indoc! {"
237            Test
238            |TestTest
239            Test"});
240        cx.simulate_keystroke("k");
241        cx.assert_editor_state(indoc! {"
242            |Test
243            TestTest
244            Test"});
245        cx.simulate_keystroke("j");
246        cx.assert_editor_state(indoc! {"
247            Test
248            |TestTest
249            Test"});
250
251        // When moving left, cursor does not wrap to the previous line
252        cx.simulate_keystroke("h");
253        cx.assert_editor_state(indoc! {"
254            Test
255            |TestTest
256            Test"});
257
258        // When moving right, cursor does not reach the line end or wrap to the next line
259        for _ in 0..9 {
260            cx.simulate_keystroke("l");
261        }
262        cx.assert_editor_state(indoc! {"
263            Test
264            TestTes|t
265            Test"});
266
267        // Goal column respects the inability to reach the end of the line
268        cx.simulate_keystroke("k");
269        cx.assert_editor_state(indoc! {"
270            Tes|t
271            TestTest
272            Test"});
273        cx.simulate_keystroke("j");
274        cx.assert_editor_state(indoc! {"
275            Test
276            TestTes|t
277            Test"});
278    }
279
280    #[gpui::test]
281    async fn test_jump_to_line_boundaries(cx: &mut gpui::TestAppContext) {
282        let initial_content = indoc! {"
283            Test Test
284            
285            T"};
286        let mut cx = VimTestContext::new(cx, true, initial_content).await;
287
288        cx.simulate_keystroke("shift-$");
289        cx.assert_editor_state(indoc! {"
290            Test Tes|t
291            
292            T"});
293        cx.simulate_keystroke("0");
294        cx.assert_editor_state(indoc! {"
295            |Test Test
296            
297            T"});
298
299        cx.simulate_keystroke("j");
300        cx.simulate_keystroke("shift-$");
301        cx.assert_editor_state(indoc! {"
302            Test Test
303            |
304            T"});
305        cx.simulate_keystroke("0");
306        cx.assert_editor_state(indoc! {"
307            Test Test
308            |
309            T"});
310
311        cx.simulate_keystroke("j");
312        cx.simulate_keystroke("shift-$");
313        cx.assert_editor_state(indoc! {"
314            Test Test
315            
316            |T"});
317        cx.simulate_keystroke("0");
318        cx.assert_editor_state(indoc! {"
319            Test Test
320            
321            |T"});
322    }
323
324    #[gpui::test]
325    async fn test_jump_to_end(cx: &mut gpui::TestAppContext) {
326        let initial_content = indoc! {"
327            The quick
328            
329            brown fox jumps
330            over the lazy dog"};
331        let mut cx = VimTestContext::new(cx, true, initial_content).await;
332
333        cx.simulate_keystroke("shift-G");
334        cx.assert_editor_state(indoc! {"
335            The quick
336            
337            brown fox jumps
338            over the lazy do|g"});
339
340        // Repeat the action doesn't move
341        cx.simulate_keystroke("shift-G");
342        cx.assert_editor_state(indoc! {"
343            The quick
344            
345            brown fox jumps
346            over the lazy do|g"});
347    }
348
349    #[gpui::test]
350    async fn test_next_word_start(cx: &mut gpui::TestAppContext) {
351        let (initial_content, cursor_offsets) = marked_text(indoc! {"
352            The |quick|-|brown
353            |
354            |
355            |fox_jumps |over
356            |th||e"});
357        let mut cx = VimTestContext::new(cx, true, &initial_content).await;
358
359        for cursor_offset in cursor_offsets {
360            cx.simulate_keystroke("w");
361            cx.assert_newest_selection_head_offset(cursor_offset);
362        }
363
364        // Reset and test ignoring punctuation
365        cx.simulate_keystrokes(&["g", "g"]);
366        let (_, cursor_offsets) = marked_text(indoc! {"
367            The |quick-brown
368            |
369            |
370            |fox_jumps |over
371            |th||e"});
372
373        for cursor_offset in cursor_offsets {
374            cx.simulate_keystroke("shift-W");
375            cx.assert_newest_selection_head_offset(cursor_offset);
376        }
377    }
378
379    #[gpui::test]
380    async fn test_next_word_end(cx: &mut gpui::TestAppContext) {
381        let (initial_content, cursor_offsets) = marked_text(indoc! {"
382            Th|e quic|k|-brow|n
383            
384            
385            fox_jump|s ove|r
386            th|e"});
387        let mut cx = VimTestContext::new(cx, true, &initial_content).await;
388
389        for cursor_offset in cursor_offsets {
390            cx.simulate_keystroke("e");
391            cx.assert_newest_selection_head_offset(cursor_offset);
392        }
393
394        // Reset and test ignoring punctuation
395        cx.simulate_keystrokes(&["g", "g"]);
396        let (_, cursor_offsets) = marked_text(indoc! {"
397            Th|e quick-brow|n
398            
399            
400            fox_jump|s ove|r
401            th||e"});
402        for cursor_offset in cursor_offsets {
403            cx.simulate_keystroke("shift-E");
404            cx.assert_newest_selection_head_offset(cursor_offset);
405        }
406    }
407
408    #[gpui::test]
409    async fn test_previous_word_start(cx: &mut gpui::TestAppContext) {
410        let (initial_content, cursor_offsets) = marked_text(indoc! {"
411            ||The |quick|-|brown
412            |
413            |
414            |fox_jumps |over
415            |the"});
416        let mut cx = VimTestContext::new(cx, true, &initial_content).await;
417        cx.simulate_keystroke("shift-G");
418
419        for cursor_offset in cursor_offsets.into_iter().rev() {
420            cx.simulate_keystroke("b");
421            cx.assert_newest_selection_head_offset(cursor_offset);
422        }
423
424        // Reset and test ignoring punctuation
425        cx.simulate_keystroke("shift-G");
426        let (_, cursor_offsets) = marked_text(indoc! {"
427            ||The |quick-brown
428            |
429            |
430            |fox_jumps |over
431            |the"});
432        for cursor_offset in cursor_offsets.into_iter().rev() {
433            cx.simulate_keystroke("shift-B");
434            cx.assert_newest_selection_head_offset(cursor_offset);
435        }
436    }
437}