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}