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(_)) => {
 17                // Can't do anything for a namespace operator. Ignoring
 18            }
 19        }
 20        vim.clear_operator(cx);
 21    });
 22}
 23
 24fn move_cursor(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
 25    vim.update_active_editor(cx, |editor, cx| {
 26        editor.move_cursors(cx, |map, cursor, goal| {
 27            motion.move_point(map, cursor, goal, true)
 28        })
 29    });
 30}
 31
 32fn change_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
 33    vim.update_active_editor(cx, |editor, cx| {
 34        editor.transact(cx, |editor, cx| {
 35            // Don't clip at line ends during change operation
 36            editor.set_clip_at_line_ends(false, cx);
 37            editor.move_selections(cx, |map, selection| {
 38                let (head, goal) = motion.move_point(map, selection.head(), selection.goal, false);
 39                selection.set_head(head, goal);
 40
 41                if motion.line_wise() {
 42                    selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
 43                    selection.end = map.next_line_boundary(selection.end.to_point(map)).1;
 44                }
 45            });
 46            editor.set_clip_at_line_ends(true, cx);
 47            editor.insert(&"", cx);
 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            // Use goal column to preserve previous position
 57            editor.set_clip_at_line_ends(false, cx);
 58            editor.move_selections(cx, |map, selection| {
 59                let original_head = selection.head();
 60                let (head, _) = motion.move_point(map, selection.head(), selection.goal, false);
 61                // Set the goal column to the original position in order to fix it up
 62                // after the deletion
 63                selection.set_head(head, SelectionGoal::Column(original_head.column()));
 64
 65                if motion.line_wise() {
 66                    if selection.end.row() == map.max_point().row() {
 67                        // Delete previous line break since we are at the end of the document
 68                        if selection.start.row() > 0 {
 69                            *selection.start.row_mut() = selection.start.row().saturating_sub(1);
 70                            selection.start = map.clip_point(selection.start, Bias::Left);
 71                            selection.start =
 72                                map.next_line_boundary(selection.start.to_point(map)).1;
 73                        } else {
 74                            // Selection covers the whole document. Just delete to the start of the
 75                            // line.
 76                            selection.start =
 77                                map.prev_line_boundary(selection.start.to_point(map)).1;
 78                        }
 79                        selection.end = map.next_line_boundary(selection.end.to_point(map)).1;
 80                    } else {
 81                        // Delete next line break so that we leave the previous line alone
 82                        selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
 83                        *selection.end.column_mut() = 0;
 84                        *selection.end.row_mut() += 1;
 85                        selection.end = map.clip_point(selection.end, Bias::Left);
 86                    }
 87                }
 88            });
 89            editor.insert(&"", cx);
 90
 91            // Fixup cursor position after the deletion
 92            editor.set_clip_at_line_ends(true, cx);
 93            editor.move_cursors(cx, |map, mut cursor, goal| {
 94                if motion.line_wise() {
 95                    if let SelectionGoal::Column(column) = goal {
 96                        *cursor.column_mut() = column
 97                    }
 98                }
 99                (map.clip_point(cursor, Bias::Left), SelectionGoal::None)
100            });
101        });
102    });
103}
104
105#[cfg(test)]
106mod test {
107    use indoc::indoc;
108    use util::test::marked_text;
109
110    use crate::{
111        state::{
112            Mode::{self, *},
113            Namespace, Operator,
114        },
115        vim_test_context::VimTestContext,
116    };
117
118    #[gpui::test]
119    async fn test_hjkl(cx: &mut gpui::TestAppContext) {
120        let mut cx = VimTestContext::new(cx, true, "Test\nTestTest\nTest").await;
121        cx.simulate_keystroke("l");
122        cx.assert_editor_state(indoc! {"
123            T|est
124            TestTest
125            Test"});
126        cx.simulate_keystroke("h");
127        cx.assert_editor_state(indoc! {"
128            |Test
129            TestTest
130            Test"});
131        cx.simulate_keystroke("j");
132        cx.assert_editor_state(indoc! {"
133            Test
134            |TestTest
135            Test"});
136        cx.simulate_keystroke("k");
137        cx.assert_editor_state(indoc! {"
138            |Test
139            TestTest
140            Test"});
141        cx.simulate_keystroke("j");
142        cx.assert_editor_state(indoc! {"
143            Test
144            |TestTest
145            Test"});
146
147        // When moving left, cursor does not wrap to the previous line
148        cx.simulate_keystroke("h");
149        cx.assert_editor_state(indoc! {"
150            Test
151            |TestTest
152            Test"});
153
154        // When moving right, cursor does not reach the line end or wrap to the next line
155        for _ in 0..9 {
156            cx.simulate_keystroke("l");
157        }
158        cx.assert_editor_state(indoc! {"
159            Test
160            TestTes|t
161            Test"});
162
163        // Goal column respects the inability to reach the end of the line
164        cx.simulate_keystroke("k");
165        cx.assert_editor_state(indoc! {"
166            Tes|t
167            TestTest
168            Test"});
169        cx.simulate_keystroke("j");
170        cx.assert_editor_state(indoc! {"
171            Test
172            TestTes|t
173            Test"});
174    }
175
176    #[gpui::test]
177    async fn test_jump_to_line_boundaries(cx: &mut gpui::TestAppContext) {
178        let initial_content = indoc! {"
179            Test Test
180            
181            T"};
182        let mut cx = VimTestContext::new(cx, true, initial_content).await;
183
184        cx.simulate_keystroke("shift-$");
185        cx.assert_editor_state(indoc! {"
186            Test Tes|t
187            
188            T"});
189        cx.simulate_keystroke("0");
190        cx.assert_editor_state(indoc! {"
191            |Test Test
192            
193            T"});
194
195        cx.simulate_keystroke("j");
196        cx.simulate_keystroke("shift-$");
197        cx.assert_editor_state(indoc! {"
198            Test Test
199            |
200            T"});
201        cx.simulate_keystroke("0");
202        cx.assert_editor_state(indoc! {"
203            Test Test
204            |
205            T"});
206
207        cx.simulate_keystroke("j");
208        cx.simulate_keystroke("shift-$");
209        cx.assert_editor_state(indoc! {"
210            Test Test
211            
212            |T"});
213        cx.simulate_keystroke("0");
214        cx.assert_editor_state(indoc! {"
215            Test Test
216            
217            |T"});
218    }
219
220    #[gpui::test]
221    async fn test_jump_to_end(cx: &mut gpui::TestAppContext) {
222        let mut cx = VimTestContext::new(cx, true, "").await;
223
224        cx.set_state(
225            indoc! {"
226            The |quick
227            
228            brown fox jumps
229            over the lazy dog"},
230            Mode::Normal,
231        );
232        cx.simulate_keystroke("shift-G");
233        cx.assert_editor_state(indoc! {"
234            The quick
235            
236            brown fox jumps
237            over| the lazy dog"});
238
239        // Repeat the action doesn't move
240        cx.simulate_keystroke("shift-G");
241        cx.assert_editor_state(indoc! {"
242            The quick
243            
244            brown fox jumps
245            over| the lazy dog"});
246    }
247
248    #[gpui::test]
249    async fn test_next_word_start(cx: &mut gpui::TestAppContext) {
250        let (initial_content, cursor_offsets) = marked_text(indoc! {"
251            The |quick|-|brown
252            |
253            |
254            |fox_jumps |over
255            |th||e"});
256        let mut cx = VimTestContext::new(cx, true, &initial_content).await;
257
258        for cursor_offset in cursor_offsets {
259            cx.simulate_keystroke("w");
260            cx.assert_newest_selection_head_offset(cursor_offset);
261        }
262
263        // Reset and test ignoring punctuation
264        cx.simulate_keystrokes(["g", "g", "0"]);
265        let (_, cursor_offsets) = marked_text(indoc! {"
266            The |quick-brown
267            |
268            |
269            |fox_jumps |over
270            |th||e"});
271
272        for cursor_offset in cursor_offsets {
273            cx.simulate_keystroke("shift-W");
274            cx.assert_newest_selection_head_offset(cursor_offset);
275        }
276    }
277
278    #[gpui::test]
279    async fn test_next_word_end(cx: &mut gpui::TestAppContext) {
280        let (initial_content, cursor_offsets) = marked_text(indoc! {"
281            Th|e quic|k|-brow|n
282            
283            
284            fox_jump|s ove|r
285            th|e"});
286        let mut cx = VimTestContext::new(cx, true, &initial_content).await;
287
288        for cursor_offset in cursor_offsets {
289            cx.simulate_keystroke("e");
290            cx.assert_newest_selection_head_offset(cursor_offset);
291        }
292
293        // Reset and test ignoring punctuation
294        cx.simulate_keystrokes(["g", "g", "0"]);
295        let (_, cursor_offsets) = marked_text(indoc! {"
296            Th|e quick-brow|n
297            
298            
299            fox_jump|s ove|r
300            th||e"});
301        for cursor_offset in cursor_offsets {
302            cx.simulate_keystroke("shift-E");
303            cx.assert_newest_selection_head_offset(cursor_offset);
304        }
305    }
306
307    #[gpui::test]
308    async fn test_previous_word_start(cx: &mut gpui::TestAppContext) {
309        let (initial_content, cursor_offsets) = marked_text(indoc! {"
310            ||The |quick|-|brown
311            |
312            |
313            |fox_jumps |over
314            |the"});
315        let mut cx = VimTestContext::new(cx, true, &initial_content).await;
316        cx.simulate_keystrokes(["shift-G", "shift-$"]);
317
318        for cursor_offset in cursor_offsets.into_iter().rev() {
319            cx.simulate_keystroke("b");
320            cx.assert_newest_selection_head_offset(cursor_offset);
321        }
322
323        // Reset and test ignoring punctuation
324        cx.simulate_keystrokes(["shift-G", "shift-$"]);
325        let (_, cursor_offsets) = marked_text(indoc! {"
326            ||The |quick-brown
327            |
328            |
329            |fox_jumps |over
330            |the"});
331        for cursor_offset in cursor_offsets.into_iter().rev() {
332            cx.simulate_keystroke("shift-B");
333            cx.assert_newest_selection_head_offset(cursor_offset);
334        }
335    }
336
337    #[gpui::test]
338    async fn test_g_prefix_and_abort(cx: &mut gpui::TestAppContext) {
339        let mut cx = VimTestContext::new(cx, true, "").await;
340
341        // Can abort with escape to get back to normal mode
342        cx.simulate_keystroke("g");
343        assert_eq!(cx.mode(), Normal);
344        assert_eq!(
345            cx.active_operator(),
346            Some(Operator::Namespace(Namespace::G))
347        );
348        cx.simulate_keystroke("escape");
349        assert_eq!(cx.mode(), Normal);
350        assert_eq!(cx.active_operator(), None);
351    }
352
353    #[gpui::test]
354    async fn test_move_to_start(cx: &mut gpui::TestAppContext) {
355        let mut cx = VimTestContext::new(cx, true, "").await;
356
357        cx.set_state(
358            indoc! {"
359            The q|uick
360            
361            brown fox jumps
362            over the lazy dog"},
363            Mode::Normal,
364        );
365
366        // Jump to the end to
367        cx.simulate_keystroke("shift-G");
368        cx.assert_editor_state(indoc! {"
369            The quick
370            
371            brown fox jumps
372            over |the lazy dog"});
373
374        // Jump to the start
375        cx.simulate_keystrokes(["g", "g"]);
376        cx.assert_editor_state(indoc! {"
377            The q|uick
378            
379            brown fox jumps
380            over the lazy dog"});
381        assert_eq!(cx.mode(), Normal);
382        assert_eq!(cx.active_operator(), None);
383
384        // Repeat action doesn't change
385        cx.simulate_keystrokes(["g", "g"]);
386        cx.assert_editor_state(indoc! {"
387            The q|uick
388            
389            brown fox jumps
390            over the lazy dog"});
391        assert_eq!(cx.mode(), Normal);
392        assert_eq!(cx.active_operator(), None);
393    }
394
395    #[gpui::test]
396    async fn test_change(cx: &mut gpui::TestAppContext) {
397        fn assert(motion: &str, initial_state: &str, state_after: &str, cx: &mut VimTestContext) {
398            cx.assert_binding(
399                ["c", motion],
400                initial_state,
401                Mode::Normal,
402                state_after,
403                Mode::Insert,
404            );
405        }
406        let cx = &mut VimTestContext::new(cx, true, "").await;
407        assert("h", "Te|st", "T|st", cx);
408        assert("l", "Te|st", "Te|t", cx);
409        assert("w", "|Test", "|", cx);
410        assert("w", "Te|st", "Te|", cx);
411        assert("w", "Te|st Test", "Te| Test", cx);
412        assert("e", "Te|st Test", "Te| Test", cx);
413        assert("b", "Te|st", "|st", cx);
414        assert("b", "Test Te|st", "Test |st", cx);
415        assert(
416            "w",
417            indoc! {"
418            The quick
419            brown |fox
420            jumps over"},
421            indoc! {"
422            The quick
423            brown |
424            jumps over"},
425            cx,
426        );
427        assert(
428            "shift-W",
429            indoc! {"
430            The quick
431            brown |fox-fox
432            jumps over"},
433            indoc! {"
434            The quick
435            brown |
436            jumps over"},
437            cx,
438        );
439        assert(
440            "k",
441            indoc! {"
442            The quick
443            brown |fox"},
444            indoc! {"
445            |"},
446            cx,
447        );
448        assert(
449            "j",
450            indoc! {"
451            The q|uick
452            brown fox"},
453            indoc! {"
454            |"},
455            cx,
456        );
457        assert(
458            "shift-$",
459            indoc! {"
460            The q|uick
461            brown fox"},
462            indoc! {"
463            The q|
464            brown fox"},
465            cx,
466        );
467        assert(
468            "0",
469            indoc! {"
470            The q|uick
471            brown fox"},
472            indoc! {"
473            |uick
474            brown fox"},
475            cx,
476        );
477    }
478
479    #[gpui::test]
480    async fn test_delete(cx: &mut gpui::TestAppContext) {
481        fn assert(motion: &str, initial_state: &str, state_after: &str, cx: &mut VimTestContext) {
482            cx.assert_binding(
483                ["d", motion],
484                initial_state,
485                Mode::Normal,
486                state_after,
487                Mode::Normal,
488            );
489        }
490        let cx = &mut VimTestContext::new(cx, true, "").await;
491        assert("h", "Te|st", "T|st", cx);
492        assert("l", "Te|st", "Te|t", cx);
493        assert("w", "|Test", "|", cx);
494        assert("w", "Te|st", "T|e", cx);
495        assert("w", "Te|st Test", "Te|Test", cx);
496        assert("e", "Te|st Test", "Te| Test", cx);
497        assert("b", "Te|st", "|st", cx);
498        assert("b", "Test Te|st", "Test |st", cx);
499        assert(
500            "w",
501            indoc! {"
502            The quick
503            brown |fox
504            jumps over"},
505            // Trailing space after cursor
506            indoc! {"
507            The quick
508            brown| 
509            jumps over"},
510            cx,
511        );
512        assert(
513            "shift-W",
514            indoc! {"
515            The quick
516            brown |fox-fox
517            jumps over"},
518            // Trailing space after cursor
519            indoc! {"
520            The quick
521            brown| 
522            jumps over"},
523            cx,
524        );
525        assert(
526            "shift-$",
527            indoc! {"
528            The q|uick
529            brown fox"},
530            indoc! {"
531            The |q
532            brown fox"},
533            cx,
534        );
535        assert(
536            "0",
537            indoc! {"
538            The q|uick
539            brown fox"},
540            indoc! {"
541            |uick
542            brown fox"},
543            cx,
544        );
545    }
546
547    #[gpui::test]
548    async fn test_linewise_delete(cx: &mut gpui::TestAppContext) {
549        fn assert(motion: &str, initial_state: &str, state_after: &str, cx: &mut VimTestContext) {
550            cx.assert_binding(
551                ["d", motion],
552                initial_state,
553                Mode::Normal,
554                state_after,
555                Mode::Normal,
556            );
557        }
558        let cx = &mut VimTestContext::new(cx, true, "").await;
559        assert(
560            "k",
561            indoc! {"
562            The quick
563            brown |fox
564            jumps over"},
565            indoc! {"
566            jumps |over"},
567            cx,
568        );
569        assert(
570            "k",
571            indoc! {"
572            The quick
573            brown fox
574            jumps |over"},
575            indoc! {"
576            The qu|ick"},
577            cx,
578        );
579        assert(
580            "j",
581            indoc! {"
582            The q|uick
583            brown fox
584            jumps over"},
585            indoc! {"
586            jumps| over"},
587            cx,
588        );
589        assert(
590            "j",
591            indoc! {"
592            The quick
593            brown| fox
594            jumps over"},
595            indoc! {"
596            The q|uick"},
597            cx,
598        );
599        assert(
600            "j",
601            indoc! {"
602            The quick
603            brown| fox
604            jumps over"},
605            indoc! {"
606            The q|uick"},
607            cx,
608        );
609        cx.assert_binding(
610            ["d", "g", "g"],
611            indoc! {"
612            The quick
613            brown| fox
614            jumps over
615            the lazy"},
616            Mode::Normal,
617            indoc! {"
618            jumps| over
619            the lazy"},
620            Mode::Normal,
621        );
622        cx.assert_binding(
623            ["d", "g", "g"],
624            indoc! {"
625            The quick
626            brown fox
627            jumps over
628            the l|azy"},
629            Mode::Normal,
630            "|",
631            Mode::Normal,
632        );
633        assert(
634            "shift-G",
635            indoc! {"
636            The quick
637            brown| fox
638            jumps over
639            the lazy"},
640            indoc! {"
641            The q|uick"},
642            cx,
643        );
644        cx.assert_binding(
645            ["d", "g", "g"],
646            indoc! {"
647            The q|uick
648            brown fox
649            jumps over
650            the lazy"},
651            Mode::Normal,
652            indoc! {"
653            brown| fox
654            jumps over
655            the lazy"},
656            Mode::Normal,
657        );
658    }
659
660    #[gpui::test]
661    async fn test_linewise_change(cx: &mut gpui::TestAppContext) {
662        fn assert(motion: &str, initial_state: &str, state_after: &str, cx: &mut VimTestContext) {
663            cx.assert_binding(
664                ["c", motion],
665                initial_state,
666                Mode::Normal,
667                state_after,
668                Mode::Insert,
669            );
670        }
671        let cx = &mut VimTestContext::new(cx, true, "").await;
672        assert(
673            "k",
674            indoc! {"
675            The quick
676            brown |fox
677            jumps over"},
678            indoc! {"
679            |
680            jumps over"},
681            cx,
682        );
683        assert(
684            "k",
685            indoc! {"
686            The quick
687            brown fox
688            jumps |over"},
689            indoc! {"
690            The quick
691            |"},
692            cx,
693        );
694        assert(
695            "j",
696            indoc! {"
697            The q|uick
698            brown fox
699            jumps over"},
700            indoc! {"
701            |
702            jumps over"},
703            cx,
704        );
705        assert(
706            "j",
707            indoc! {"
708            The quick
709            brown| fox
710            jumps over"},
711            indoc! {"
712            The quick
713            |"},
714            cx,
715        );
716        assert(
717            "j",
718            indoc! {"
719            The quick
720            brown| fox
721            jumps over"},
722            indoc! {"
723            The quick
724            |"},
725            cx,
726        );
727        assert(
728            "shift-G",
729            indoc! {"
730            The quick
731            brown| fox
732            jumps over
733            the lazy"},
734            indoc! {"
735            The quick
736            |"},
737            cx,
738        );
739        assert(
740            "shift-G",
741            indoc! {"
742            The quick
743            brown| fox
744            jumps over
745            the lazy"},
746            indoc! {"
747            The quick
748            |"},
749            cx,
750        );
751        assert(
752            "shift-G",
753            indoc! {"
754            The quick
755            brown fox
756            jumps over
757            the l|azy"},
758            indoc! {"
759            The quick
760            brown fox
761            jumps over
762            |"},
763            cx,
764        );
765        cx.assert_binding(
766            ["c", "g", "g"],
767            indoc! {"
768            The quick
769            brown| fox
770            jumps over
771            the lazy"},
772            Mode::Normal,
773            indoc! {"
774            |
775            jumps over
776            the lazy"},
777            Mode::Insert,
778        );
779        cx.assert_binding(
780            ["c", "g", "g"],
781            indoc! {"
782            The quick
783            brown fox
784            jumps over
785            the l|azy"},
786            Mode::Normal,
787            "|",
788            Mode::Insert,
789        );
790        cx.assert_binding(
791            ["c", "g", "g"],
792            indoc! {"
793            The q|uick
794            brown fox
795            jumps over
796            the lazy"},
797            Mode::Normal,
798            indoc! {"
799            |
800            brown fox
801            jumps over
802            the lazy"},
803            Mode::Insert,
804        );
805    }
806}