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