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}