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}