normal.rs

  1use crate::{
  2    motion::Motion,
  3    state::{Mode, Operator},
  4    Vim,
  5};
  6use editor::Bias;
  7use gpui::MutableAppContext;
  8use language::SelectionGoal;
  9
 10pub fn normal_motion(motion: Motion, cx: &mut MutableAppContext) {
 11    Vim::update(cx, |vim, cx| {
 12        match vim.state.operator_stack.pop() {
 13            None => move_cursor(vim, motion, cx),
 14            Some(Operator::Change) => change_over(vim, motion, cx),
 15            Some(Operator::Delete) => delete_over(vim, motion, cx),
 16            Some(Operator::Namespace(_)) => panic!(
 17                "Normal mode recieved motion with namespaced operator. Likely this means an invalid keymap was used"),
 18        }
 19        vim.clear_operator(cx);
 20    });
 21}
 22
 23fn move_cursor(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
 24    vim.update_active_editor(cx, |editor, cx| {
 25        editor.move_cursors(cx, |map, cursor, goal| motion.move_point(map, cursor, goal))
 26    });
 27}
 28
 29fn change_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
 30    vim.update_active_editor(cx, |editor, cx| {
 31        editor.transact(cx, |editor, cx| {
 32            // Don't clip at line ends during change operation
 33            editor.set_clip_at_line_ends(false, cx);
 34            editor.move_selections(cx, |map, selection| motion.expand_selection(map, selection));
 35            editor.set_clip_at_line_ends(true, cx);
 36            match motion {
 37                Motion::Up => editor.insert(&"\n", cx),
 38                Motion::Down => editor.insert(&"\n", cx),
 39                _ => editor.insert(&"", cx),
 40            }
 41
 42            if let Motion::Up = motion {
 43                // Position cursor on previous line after change
 44                editor.move_cursors(cx, |map, cursor, goal| {
 45                    Motion::Up.move_point(map, cursor, goal)
 46                });
 47            }
 48        });
 49    });
 50    vim.switch_mode(Mode::Insert, cx)
 51}
 52
 53fn delete_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
 54    vim.update_active_editor(cx, |editor, cx| {
 55        editor.transact(cx, |editor, cx| {
 56            // Don't clip at line ends during delete operation
 57            editor.set_clip_at_line_ends(false, cx);
 58            editor.move_selections(cx, |map, selection| motion.expand_selection(map, selection));
 59            match motion {
 60                Motion::Up => editor.insert(&"\n", cx),
 61                Motion::Down => editor.insert(&"\n", cx),
 62                _ => editor.insert(&"", cx),
 63            }
 64
 65            if let Motion::Up = motion {
 66                // Position cursor on previous line after change
 67                editor.move_cursors(cx, |map, cursor, goal| {
 68                    Motion::Up.move_point(map, cursor, goal)
 69                });
 70            }
 71            // Fixup cursor position after the deletion
 72            editor.set_clip_at_line_ends(true, cx);
 73            editor.move_selection_heads(cx, |map, head, _| {
 74                (map.clip_point(head, Bias::Left), SelectionGoal::None)
 75            });
 76        });
 77    });
 78}
 79
 80#[cfg(test)]
 81mod test {
 82    use indoc::indoc;
 83    use util::test::marked_text;
 84
 85    use crate::{
 86        state::{
 87            Mode::{self, *},
 88            Namespace, Operator,
 89        },
 90        vim_test_context::VimTestContext,
 91    };
 92
 93    #[gpui::test]
 94    async fn test_hjkl(cx: &mut gpui::TestAppContext) {
 95        let mut cx = VimTestContext::new(cx, true, "Test\nTestTest\nTest").await;
 96        cx.simulate_keystroke("l");
 97        cx.assert_editor_state(indoc! {"
 98            T|est
 99            TestTest
100            Test"});
101        cx.simulate_keystroke("h");
102        cx.assert_editor_state(indoc! {"
103            |Test
104            TestTest
105            Test"});
106        cx.simulate_keystroke("j");
107        cx.assert_editor_state(indoc! {"
108            Test
109            |TestTest
110            Test"});
111        cx.simulate_keystroke("k");
112        cx.assert_editor_state(indoc! {"
113            |Test
114            TestTest
115            Test"});
116        cx.simulate_keystroke("j");
117        cx.assert_editor_state(indoc! {"
118            Test
119            |TestTest
120            Test"});
121
122        // When moving left, cursor does not wrap to the previous line
123        cx.simulate_keystroke("h");
124        cx.assert_editor_state(indoc! {"
125            Test
126            |TestTest
127            Test"});
128
129        // When moving right, cursor does not reach the line end or wrap to the next line
130        for _ in 0..9 {
131            cx.simulate_keystroke("l");
132        }
133        cx.assert_editor_state(indoc! {"
134            Test
135            TestTes|t
136            Test"});
137
138        // Goal column respects the inability to reach the end of the line
139        cx.simulate_keystroke("k");
140        cx.assert_editor_state(indoc! {"
141            Tes|t
142            TestTest
143            Test"});
144        cx.simulate_keystroke("j");
145        cx.assert_editor_state(indoc! {"
146            Test
147            TestTes|t
148            Test"});
149    }
150
151    #[gpui::test]
152    async fn test_jump_to_line_boundaries(cx: &mut gpui::TestAppContext) {
153        let initial_content = indoc! {"
154            Test Test
155            
156            T"};
157        let mut cx = VimTestContext::new(cx, true, initial_content).await;
158
159        cx.simulate_keystroke("shift-$");
160        cx.assert_editor_state(indoc! {"
161            Test Tes|t
162            
163            T"});
164        cx.simulate_keystroke("0");
165        cx.assert_editor_state(indoc! {"
166            |Test Test
167            
168            T"});
169
170        cx.simulate_keystroke("j");
171        cx.simulate_keystroke("shift-$");
172        cx.assert_editor_state(indoc! {"
173            Test Test
174            |
175            T"});
176        cx.simulate_keystroke("0");
177        cx.assert_editor_state(indoc! {"
178            Test Test
179            |
180            T"});
181
182        cx.simulate_keystroke("j");
183        cx.simulate_keystroke("shift-$");
184        cx.assert_editor_state(indoc! {"
185            Test Test
186            
187            |T"});
188        cx.simulate_keystroke("0");
189        cx.assert_editor_state(indoc! {"
190            Test Test
191            
192            |T"});
193    }
194
195    #[gpui::test]
196    async fn test_jump_to_end(cx: &mut gpui::TestAppContext) {
197        let initial_content = indoc! {"
198            The quick
199            
200            brown fox jumps
201            over the lazy dog"};
202        let mut cx = VimTestContext::new(cx, true, initial_content).await;
203
204        cx.simulate_keystroke("shift-G");
205        cx.assert_editor_state(indoc! {"
206            The quick
207            
208            brown fox jumps
209            over the lazy do|g"});
210
211        // Repeat the action doesn't move
212        cx.simulate_keystroke("shift-G");
213        cx.assert_editor_state(indoc! {"
214            The quick
215            
216            brown fox jumps
217            over the lazy do|g"});
218    }
219
220    #[gpui::test]
221    async fn test_next_word_start(cx: &mut gpui::TestAppContext) {
222        let (initial_content, cursor_offsets) = marked_text(indoc! {"
223            The |quick|-|brown
224            |
225            |
226            |fox_jumps |over
227            |th||e"});
228        let mut cx = VimTestContext::new(cx, true, &initial_content).await;
229
230        for cursor_offset in cursor_offsets {
231            cx.simulate_keystroke("w");
232            cx.assert_newest_selection_head_offset(cursor_offset);
233        }
234
235        // Reset and test ignoring punctuation
236        cx.simulate_keystrokes(["g", "g"]);
237        let (_, cursor_offsets) = marked_text(indoc! {"
238            The |quick-brown
239            |
240            |
241            |fox_jumps |over
242            |th||e"});
243
244        for cursor_offset in cursor_offsets {
245            cx.simulate_keystroke("shift-W");
246            cx.assert_newest_selection_head_offset(cursor_offset);
247        }
248    }
249
250    #[gpui::test]
251    async fn test_next_word_end(cx: &mut gpui::TestAppContext) {
252        let (initial_content, cursor_offsets) = marked_text(indoc! {"
253            Th|e quic|k|-brow|n
254            
255            
256            fox_jump|s ove|r
257            th|e"});
258        let mut cx = VimTestContext::new(cx, true, &initial_content).await;
259
260        for cursor_offset in cursor_offsets {
261            cx.simulate_keystroke("e");
262            cx.assert_newest_selection_head_offset(cursor_offset);
263        }
264
265        // Reset and test ignoring punctuation
266        cx.simulate_keystrokes(["g", "g"]);
267        let (_, cursor_offsets) = marked_text(indoc! {"
268            Th|e quick-brow|n
269            
270            
271            fox_jump|s ove|r
272            th||e"});
273        for cursor_offset in cursor_offsets {
274            cx.simulate_keystroke("shift-E");
275            cx.assert_newest_selection_head_offset(cursor_offset);
276        }
277    }
278
279    #[gpui::test]
280    async fn test_previous_word_start(cx: &mut gpui::TestAppContext) {
281        let (initial_content, cursor_offsets) = marked_text(indoc! {"
282            ||The |quick|-|brown
283            |
284            |
285            |fox_jumps |over
286            |the"});
287        let mut cx = VimTestContext::new(cx, true, &initial_content).await;
288        cx.simulate_keystroke("shift-G");
289
290        for cursor_offset in cursor_offsets.into_iter().rev() {
291            cx.simulate_keystroke("b");
292            cx.assert_newest_selection_head_offset(cursor_offset);
293        }
294
295        // Reset and test ignoring punctuation
296        cx.simulate_keystroke("shift-G");
297        let (_, cursor_offsets) = marked_text(indoc! {"
298            ||The |quick-brown
299            |
300            |
301            |fox_jumps |over
302            |the"});
303        for cursor_offset in cursor_offsets.into_iter().rev() {
304            cx.simulate_keystroke("shift-B");
305            cx.assert_newest_selection_head_offset(cursor_offset);
306        }
307    }
308
309    #[gpui::test]
310    async fn test_g_prefix_and_abort(cx: &mut gpui::TestAppContext) {
311        let mut cx = VimTestContext::new(cx, true, "").await;
312
313        // Can abort with escape to get back to normal mode
314        cx.simulate_keystroke("g");
315        assert_eq!(cx.mode(), Normal);
316        assert_eq!(
317            cx.active_operator(),
318            Some(Operator::Namespace(Namespace::G))
319        );
320        cx.simulate_keystroke("escape");
321        assert_eq!(cx.mode(), Normal);
322        assert_eq!(cx.active_operator(), None);
323    }
324
325    #[gpui::test]
326    async fn test_move_to_start(cx: &mut gpui::TestAppContext) {
327        let initial_content = indoc! {"
328            The quick
329            
330            brown fox jumps
331            over the lazy dog"};
332        let mut cx = VimTestContext::new(cx, true, initial_content).await;
333
334        // Jump to the end to
335        cx.simulate_keystroke("shift-G");
336        cx.assert_editor_state(indoc! {"
337            The quick
338            
339            brown fox jumps
340            over the lazy do|g"});
341
342        // Jump to the start
343        cx.simulate_keystrokes(["g", "g"]);
344        cx.assert_editor_state(indoc! {"
345            |The quick
346            
347            brown fox jumps
348            over the lazy dog"});
349        assert_eq!(cx.mode(), Normal);
350        assert_eq!(cx.active_operator(), None);
351
352        // Repeat action doesn't change
353        cx.simulate_keystrokes(["g", "g"]);
354        cx.assert_editor_state(indoc! {"
355            |The quick
356            
357            brown fox jumps
358            over the lazy dog"});
359        assert_eq!(cx.mode(), Normal);
360        assert_eq!(cx.active_operator(), None);
361    }
362
363    #[gpui::test]
364    async fn test_change(cx: &mut gpui::TestAppContext) {
365        fn assert(motion: &str, initial_state: &str, state_after: &str, cx: &mut VimTestContext) {
366            cx.assert_binding(
367                ["c", motion],
368                initial_state,
369                Mode::Normal,
370                state_after,
371                Mode::Insert,
372            );
373        }
374        let cx = &mut VimTestContext::new(cx, true, "").await;
375        assert("h", "Te|st", "T|st", cx);
376        assert("l", "Te|st", "Te|t", cx);
377        assert("w", "|Test", "|", cx);
378        assert("w", "Te|st", "Te|", cx);
379        assert("w", "Te|st Test", "Te| Test", cx);
380        assert("e", "Te|st Test", "Te| Test", cx);
381        assert("b", "Te|st", "|st", cx);
382        assert("b", "Test Te|st", "Test |st", cx);
383        assert(
384            "w",
385            indoc! {"
386            The quick
387            brown |fox
388            jumps over"},
389            indoc! {"
390            The quick
391            brown |
392            jumps over"},
393            cx,
394        );
395        assert(
396            "shift-W",
397            indoc! {"
398            The quick
399            brown |fox-fox
400            jumps over"},
401            indoc! {"
402            The quick
403            brown |
404            jumps over"},
405            cx,
406        );
407        assert(
408            "k",
409            indoc! {"
410            The quick
411            brown |fox"},
412            indoc! {"
413            |
414            "},
415            cx,
416        );
417        assert(
418            "j",
419            indoc! {"
420            The q|uick
421            brown fox"},
422            indoc! {"
423            
424            |"},
425            cx,
426        );
427        assert(
428            "shift-$",
429            indoc! {"
430            The q|uick
431            brown fox"},
432            indoc! {"
433            The q|
434            brown fox"},
435            cx,
436        );
437        assert(
438            "0",
439            indoc! {"
440            The q|uick
441            brown fox"},
442            indoc! {"
443            |uick
444            brown fox"},
445            cx,
446        );
447    }
448
449    #[gpui::test]
450    async fn test_delete(cx: &mut gpui::TestAppContext) {
451        fn assert(motion: &str, initial_state: &str, state_after: &str, cx: &mut VimTestContext) {
452            cx.assert_binding(
453                ["d", motion],
454                initial_state,
455                Mode::Normal,
456                state_after,
457                Mode::Normal,
458            );
459        }
460        let cx = &mut VimTestContext::new(cx, true, "").await;
461        assert("h", "Te|st", "T|st", cx);
462        assert("l", "Te|st", "Te|t", cx);
463        assert("w", "|Test", "|", cx);
464        assert("w", "Te|st", "T|e", cx);
465        assert("w", "Te|st Test", "Te|Test", cx);
466        assert("e", "Te|st Test", "Te| Test", cx);
467        assert("b", "Te|st", "|st", cx);
468        assert("b", "Test Te|st", "Test |st", cx);
469        assert(
470            "w",
471            indoc! {"
472            The quick
473            brown |fox
474            jumps over"},
475            // Trailing space after cursor
476            indoc! {"
477            The quick
478            brown| 
479            jumps over"},
480            cx,
481        );
482        assert(
483            "shift-W",
484            indoc! {"
485            The quick
486            brown |fox-fox
487            jumps over"},
488            // Trailing space after cursor
489            indoc! {"
490            The quick
491            brown| 
492            jumps over"},
493            cx,
494        );
495        assert(
496            "k",
497            indoc! {"
498            The quick
499            brown |fox"},
500            indoc! {"
501            |
502            "},
503            cx,
504        );
505        assert(
506            "j",
507            indoc! {"
508            The q|uick
509            brown fox"},
510            indoc! {"
511            
512            |"},
513            cx,
514        );
515        assert(
516            "shift-$",
517            indoc! {"
518            The q|uick
519            brown fox"},
520            indoc! {"
521            The |q
522            brown fox"},
523            cx,
524        );
525        assert(
526            "0",
527            indoc! {"
528            The q|uick
529            brown fox"},
530            indoc! {"
531            |uick
532            brown fox"},
533            cx,
534        );
535    }
536}