1mod case;
2mod change;
3mod delete;
4mod increment;
5mod paste;
6pub(crate) mod repeat;
7mod scroll;
8pub(crate) mod search;
9pub mod substitute;
10mod yank;
11
12use std::sync::Arc;
13
14use crate::{
15 motion::{self, first_non_whitespace, next_line_end, right, Motion},
16 object::Object,
17 state::{Mode, Operator},
18 Vim,
19};
20use collections::BTreeSet;
21use editor::scroll::Autoscroll;
22use editor::{Bias, DisplayPoint};
23use gpui::{actions, ViewContext, WindowContext};
24use language::SelectionGoal;
25use log::error;
26use workspace::Workspace;
27
28use self::{
29 case::{change_case, convert_to_lower_case, convert_to_upper_case},
30 change::{change_motion, change_object},
31 delete::{delete_motion, delete_object},
32 yank::{yank_motion, yank_object},
33};
34
35actions!(
36 vim,
37 [
38 InsertAfter,
39 InsertBefore,
40 InsertFirstNonWhitespace,
41 InsertEndOfLine,
42 InsertLineAbove,
43 InsertLineBelow,
44 DeleteLeft,
45 DeleteRight,
46 ChangeToEndOfLine,
47 DeleteToEndOfLine,
48 Yank,
49 YankLine,
50 ChangeCase,
51 ConvertToUpperCase,
52 ConvertToLowerCase,
53 JoinLines,
54 Indent,
55 Outdent,
56 ]
57);
58
59pub(crate) fn register(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
60 workspace.register_action(insert_after);
61 workspace.register_action(insert_before);
62 workspace.register_action(insert_first_non_whitespace);
63 workspace.register_action(insert_end_of_line);
64 workspace.register_action(insert_line_above);
65 workspace.register_action(insert_line_below);
66 workspace.register_action(change_case);
67 workspace.register_action(convert_to_upper_case);
68 workspace.register_action(convert_to_lower_case);
69 workspace.register_action(yank_line);
70
71 workspace.register_action(|_: &mut Workspace, _: &DeleteLeft, cx| {
72 Vim::update(cx, |vim, cx| {
73 vim.record_current_action(cx);
74 let times = vim.take_count(cx);
75 delete_motion(vim, Motion::Left, times, cx);
76 })
77 });
78 workspace.register_action(|_: &mut Workspace, _: &DeleteRight, cx| {
79 Vim::update(cx, |vim, cx| {
80 vim.record_current_action(cx);
81 let times = vim.take_count(cx);
82 delete_motion(vim, Motion::Right, times, cx);
83 })
84 });
85 workspace.register_action(|_: &mut Workspace, _: &ChangeToEndOfLine, cx| {
86 Vim::update(cx, |vim, cx| {
87 vim.start_recording(cx);
88 let times = vim.take_count(cx);
89 change_motion(
90 vim,
91 Motion::EndOfLine {
92 display_lines: false,
93 },
94 times,
95 cx,
96 );
97 })
98 });
99 workspace.register_action(|_: &mut Workspace, _: &DeleteToEndOfLine, cx| {
100 Vim::update(cx, |vim, cx| {
101 vim.record_current_action(cx);
102 let times = vim.take_count(cx);
103 delete_motion(
104 vim,
105 Motion::EndOfLine {
106 display_lines: false,
107 },
108 times,
109 cx,
110 );
111 })
112 });
113 workspace.register_action(|_: &mut Workspace, _: &JoinLines, cx| {
114 Vim::update(cx, |vim, cx| {
115 vim.record_current_action(cx);
116 let mut times = vim.take_count(cx).unwrap_or(1);
117 if vim.state().mode.is_visual() {
118 times = 1;
119 } else if times > 1 {
120 // 2J joins two lines together (same as J or 1J)
121 times -= 1;
122 }
123
124 vim.update_active_editor(cx, |_, editor, cx| {
125 editor.transact(cx, |editor, cx| {
126 for _ in 0..times {
127 editor.join_lines(&Default::default(), cx)
128 }
129 })
130 });
131 if vim.state().mode.is_visual() {
132 vim.switch_mode(Mode::Normal, false, cx)
133 }
134 });
135 });
136
137 workspace.register_action(|_: &mut Workspace, _: &Indent, cx| {
138 Vim::update(cx, |vim, cx| {
139 vim.record_current_action(cx);
140 vim.update_active_editor(cx, |_, editor, cx| {
141 editor.transact(cx, |editor, cx| editor.indent(&Default::default(), cx))
142 });
143 if vim.state().mode.is_visual() {
144 vim.switch_mode(Mode::Normal, false, cx)
145 }
146 });
147 });
148
149 workspace.register_action(|_: &mut Workspace, _: &Outdent, cx| {
150 Vim::update(cx, |vim, cx| {
151 vim.record_current_action(cx);
152 vim.update_active_editor(cx, |_, editor, cx| {
153 editor.transact(cx, |editor, cx| editor.outdent(&Default::default(), cx))
154 });
155 if vim.state().mode.is_visual() {
156 vim.switch_mode(Mode::Normal, false, cx)
157 }
158 });
159 });
160
161 paste::register(workspace, cx);
162 repeat::register(workspace, cx);
163 scroll::register(workspace, cx);
164 search::register(workspace, cx);
165 substitute::register(workspace, cx);
166 increment::register(workspace, cx);
167}
168
169pub fn normal_motion(
170 motion: Motion,
171 operator: Option<Operator>,
172 times: Option<usize>,
173 cx: &mut WindowContext,
174) {
175 Vim::update(cx, |vim, cx| {
176 match operator {
177 None => move_cursor(vim, motion, times, cx),
178 Some(Operator::Change) => change_motion(vim, motion, times, cx),
179 Some(Operator::Delete) => delete_motion(vim, motion, times, cx),
180 Some(Operator::Yank) => yank_motion(vim, motion, times, cx),
181 Some(operator) => {
182 // Can't do anything for text objects, Ignoring
183 error!("Unexpected normal mode motion operator: {:?}", operator)
184 }
185 }
186 });
187}
188
189pub fn normal_object(object: Object, cx: &mut WindowContext) {
190 Vim::update(cx, |vim, cx| {
191 match vim.maybe_pop_operator() {
192 Some(Operator::Object { around }) => match vim.maybe_pop_operator() {
193 Some(Operator::Change) => change_object(vim, object, around, cx),
194 Some(Operator::Delete) => delete_object(vim, object, around, cx),
195 Some(Operator::Yank) => yank_object(vim, object, around, cx),
196 _ => {
197 // Can't do anything for namespace operators. Ignoring
198 }
199 },
200 _ => {
201 // Can't do anything with change/delete/yank and text objects. Ignoring
202 }
203 }
204 vim.clear_operator(cx);
205 })
206}
207
208pub(crate) fn move_cursor(
209 vim: &mut Vim,
210 motion: Motion,
211 times: Option<usize>,
212 cx: &mut WindowContext,
213) {
214 vim.update_active_editor(cx, |_, editor, cx| {
215 let text_layout_details = editor.text_layout_details(cx);
216 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
217 s.move_cursors_with(|map, cursor, goal| {
218 motion
219 .move_point(map, cursor, goal, times, &text_layout_details)
220 .unwrap_or((cursor, goal))
221 })
222 })
223 });
224}
225
226fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext<Workspace>) {
227 Vim::update(cx, |vim, cx| {
228 vim.start_recording(cx);
229 vim.switch_mode(Mode::Insert, false, cx);
230 vim.update_active_editor(cx, |_, editor, cx| {
231 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
232 s.move_cursors_with(|map, cursor, _| (right(map, cursor, 1), SelectionGoal::None));
233 });
234 });
235 });
236}
237
238fn insert_before(_: &mut Workspace, _: &InsertBefore, cx: &mut ViewContext<Workspace>) {
239 Vim::update(cx, |vim, cx| {
240 vim.start_recording(cx);
241 vim.switch_mode(Mode::Insert, false, cx);
242 });
243}
244
245fn insert_first_non_whitespace(
246 _: &mut Workspace,
247 _: &InsertFirstNonWhitespace,
248 cx: &mut ViewContext<Workspace>,
249) {
250 Vim::update(cx, |vim, cx| {
251 vim.start_recording(cx);
252 vim.switch_mode(Mode::Insert, false, cx);
253 vim.update_active_editor(cx, |_, editor, cx| {
254 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
255 s.move_cursors_with(|map, cursor, _| {
256 (
257 first_non_whitespace(map, false, cursor),
258 SelectionGoal::None,
259 )
260 });
261 });
262 });
263 });
264}
265
266fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewContext<Workspace>) {
267 Vim::update(cx, |vim, cx| {
268 vim.start_recording(cx);
269 vim.switch_mode(Mode::Insert, false, cx);
270 vim.update_active_editor(cx, |_, editor, cx| {
271 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
272 s.move_cursors_with(|map, cursor, _| {
273 (next_line_end(map, cursor, 1), SelectionGoal::None)
274 });
275 });
276 });
277 });
278}
279
280fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContext<Workspace>) {
281 Vim::update(cx, |vim, cx| {
282 vim.start_recording(cx);
283 vim.switch_mode(Mode::Insert, false, cx);
284 vim.update_active_editor(cx, |_, editor, cx| {
285 editor.transact(cx, |editor, cx| {
286 let (map, old_selections) = editor.selections.all_display(cx);
287 let selection_start_rows: BTreeSet<u32> = old_selections
288 .into_iter()
289 .map(|selection| selection.start.row())
290 .collect();
291 let edits = selection_start_rows.into_iter().map(|row| {
292 let (indent, _) = map.line_indent(row);
293 let start_of_line =
294 motion::start_of_line(&map, false, DisplayPoint::new(row, 0))
295 .to_point(&map);
296 let mut new_text = " ".repeat(indent as usize);
297 new_text.push('\n');
298 (start_of_line..start_of_line, new_text)
299 });
300 editor.edit_with_autoindent(edits, cx);
301 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
302 s.move_cursors_with(|map, cursor, _| {
303 let previous_line = motion::start_of_relative_buffer_row(map, cursor, -1);
304 let insert_point = motion::end_of_line(map, false, previous_line, 1);
305 (insert_point, SelectionGoal::None)
306 });
307 });
308 });
309 });
310 });
311}
312
313fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContext<Workspace>) {
314 Vim::update(cx, |vim, cx| {
315 vim.start_recording(cx);
316 vim.switch_mode(Mode::Insert, false, cx);
317 vim.update_active_editor(cx, |_, editor, cx| {
318 let text_layout_details = editor.text_layout_details(cx);
319 editor.transact(cx, |editor, cx| {
320 let (map, old_selections) = editor.selections.all_display(cx);
321 let selection_end_rows: BTreeSet<u32> = old_selections
322 .into_iter()
323 .map(|selection| selection.end.row())
324 .collect();
325 let edits = selection_end_rows.into_iter().map(|row| {
326 let (indent, _) = map.line_indent(row);
327 let end_of_line =
328 motion::end_of_line(&map, false, DisplayPoint::new(row, 0), 1)
329 .to_point(&map);
330
331 let mut new_text = "\n".to_string();
332 new_text.push_str(&" ".repeat(indent as usize));
333 (end_of_line..end_of_line, new_text)
334 });
335 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
336 s.maybe_move_cursors_with(|map, cursor, goal| {
337 Motion::CurrentLine.move_point(
338 map,
339 cursor,
340 goal,
341 None,
342 &text_layout_details,
343 )
344 });
345 });
346 editor.edit_with_autoindent(edits, cx);
347 });
348 });
349 });
350}
351
352fn yank_line(_: &mut Workspace, _: &YankLine, cx: &mut ViewContext<Workspace>) {
353 Vim::update(cx, |vim, cx| {
354 let count = vim.take_count(cx);
355 yank_motion(vim, motion::Motion::CurrentLine, count, cx)
356 })
357}
358
359pub(crate) fn normal_replace(text: Arc<str>, cx: &mut WindowContext) {
360 Vim::update(cx, |vim, cx| {
361 vim.stop_recording();
362 vim.update_active_editor(cx, |_, editor, cx| {
363 editor.transact(cx, |editor, cx| {
364 editor.set_clip_at_line_ends(false, cx);
365 let (map, display_selections) = editor.selections.all_display(cx);
366 // Selections are biased right at the start. So we need to store
367 // anchors that are biased left so that we can restore the selections
368 // after the change
369 let stable_anchors = editor
370 .selections
371 .disjoint_anchors()
372 .into_iter()
373 .map(|selection| {
374 let start = selection.start.bias_left(&map.buffer_snapshot);
375 start..start
376 })
377 .collect::<Vec<_>>();
378
379 let edits = display_selections
380 .into_iter()
381 .map(|selection| {
382 let mut range = selection.range();
383 *range.end.column_mut() += 1;
384 range.end = map.clip_point(range.end, Bias::Right);
385
386 (
387 range.start.to_offset(&map, Bias::Left)
388 ..range.end.to_offset(&map, Bias::Left),
389 text.clone(),
390 )
391 })
392 .collect::<Vec<_>>();
393
394 editor.buffer().update(cx, |buffer, cx| {
395 buffer.edit(edits, None, cx);
396 });
397 editor.set_clip_at_line_ends(true, cx);
398 editor.change_selections(None, cx, |s| {
399 s.select_anchor_ranges(stable_anchors);
400 });
401 });
402 });
403 vim.pop_operator(cx)
404 });
405}
406
407#[cfg(test)]
408mod test {
409 use gpui::{KeyBinding, TestAppContext};
410 use indoc::indoc;
411 use settings::SettingsStore;
412
413 use crate::{
414 motion,
415 state::Mode::{self},
416 test::{NeovimBackedTestContext, VimTestContext},
417 VimSettings,
418 };
419
420 #[gpui::test]
421 async fn test_h(cx: &mut gpui::TestAppContext) {
422 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["h"]);
423 cx.assert_all(indoc! {"
424 ˇThe qˇuick
425 ˇbrown"
426 })
427 .await;
428 }
429
430 #[gpui::test]
431 async fn test_backspace(cx: &mut gpui::TestAppContext) {
432 let mut cx = NeovimBackedTestContext::new(cx)
433 .await
434 .binding(["backspace"]);
435 cx.assert_all(indoc! {"
436 ˇThe qˇuick
437 ˇbrown"
438 })
439 .await;
440 }
441
442 #[gpui::test]
443 async fn test_j(cx: &mut gpui::TestAppContext) {
444 let mut cx = NeovimBackedTestContext::new(cx).await;
445
446 cx.set_shared_state(indoc! {"
447 aaˇaa
448 😃😃"
449 })
450 .await;
451 cx.simulate_shared_keystrokes(["j"]).await;
452 cx.assert_shared_state(indoc! {"
453 aaaa
454 😃ˇ😃"
455 })
456 .await;
457
458 for marked_position in cx.each_marked_position(indoc! {"
459 ˇThe qˇuick broˇwn
460 ˇfox jumps"
461 }) {
462 cx.assert_neovim_compatible(&marked_position, ["j"]).await;
463 }
464 }
465
466 #[gpui::test]
467 async fn test_enter(cx: &mut gpui::TestAppContext) {
468 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["enter"]);
469 cx.assert_all(indoc! {"
470 ˇThe qˇuick broˇwn
471 ˇfox jumps"
472 })
473 .await;
474 }
475
476 #[gpui::test]
477 async fn test_k(cx: &mut gpui::TestAppContext) {
478 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["k"]);
479 cx.assert_all(indoc! {"
480 ˇThe qˇuick
481 ˇbrown fˇox jumˇps"
482 })
483 .await;
484 }
485
486 #[gpui::test]
487 async fn test_l(cx: &mut gpui::TestAppContext) {
488 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["l"]);
489 cx.assert_all(indoc! {"
490 ˇThe qˇuicˇk
491 ˇbrowˇn"})
492 .await;
493 }
494
495 #[gpui::test]
496 async fn test_jump_to_line_boundaries(cx: &mut gpui::TestAppContext) {
497 let mut cx = NeovimBackedTestContext::new(cx).await;
498 cx.assert_binding_matches_all(
499 ["$"],
500 indoc! {"
501 ˇThe qˇuicˇk
502 ˇbrowˇn"},
503 )
504 .await;
505 cx.assert_binding_matches_all(
506 ["0"],
507 indoc! {"
508 ˇThe qˇuicˇk
509 ˇbrowˇn"},
510 )
511 .await;
512 }
513
514 #[gpui::test]
515 async fn test_jump_to_end(cx: &mut gpui::TestAppContext) {
516 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-g"]);
517
518 cx.assert_all(indoc! {"
519 The ˇquick
520
521 brown fox jumps
522 overˇ the lazy doˇg"})
523 .await;
524 cx.assert(indoc! {"
525 The quiˇck
526
527 brown"})
528 .await;
529 cx.assert(indoc! {"
530 The quiˇck
531
532 "})
533 .await;
534 }
535
536 #[gpui::test]
537 async fn test_w(cx: &mut gpui::TestAppContext) {
538 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["w"]);
539 cx.assert_all(indoc! {"
540 The ˇquickˇ-ˇbrown
541 ˇ
542 ˇ
543 ˇfox_jumps ˇover
544 ˇthˇe"})
545 .await;
546 let mut cx = cx.binding(["shift-w"]);
547 cx.assert_all(indoc! {"
548 The ˇquickˇ-ˇbrown
549 ˇ
550 ˇ
551 ˇfox_jumps ˇover
552 ˇthˇe"})
553 .await;
554 }
555
556 #[gpui::test]
557 async fn test_end_of_word(cx: &mut gpui::TestAppContext) {
558 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["e"]);
559 cx.assert_all(indoc! {"
560 Thˇe quicˇkˇ-browˇn
561
562
563 fox_jumpˇs oveˇr
564 thˇe"})
565 .await;
566 let mut cx = cx.binding(["shift-e"]);
567 cx.assert_all(indoc! {"
568 Thˇe quicˇkˇ-browˇn
569
570
571 fox_jumpˇs oveˇr
572 thˇe"})
573 .await;
574 }
575
576 #[gpui::test]
577 async fn test_b(cx: &mut gpui::TestAppContext) {
578 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["b"]);
579 cx.assert_all(indoc! {"
580 ˇThe ˇquickˇ-ˇbrown
581 ˇ
582 ˇ
583 ˇfox_jumps ˇover
584 ˇthe"})
585 .await;
586 let mut cx = cx.binding(["shift-b"]);
587 cx.assert_all(indoc! {"
588 ˇThe ˇquickˇ-ˇbrown
589 ˇ
590 ˇ
591 ˇfox_jumps ˇover
592 ˇthe"})
593 .await;
594 }
595
596 #[gpui::test]
597 async fn test_gg(cx: &mut gpui::TestAppContext) {
598 let mut cx = NeovimBackedTestContext::new(cx).await;
599 cx.assert_binding_matches_all(
600 ["g", "g"],
601 indoc! {"
602 The qˇuick
603
604 brown fox jumps
605 over ˇthe laˇzy dog"},
606 )
607 .await;
608 cx.assert_binding_matches(
609 ["g", "g"],
610 indoc! {"
611
612
613 brown fox jumps
614 over the laˇzy dog"},
615 )
616 .await;
617 cx.assert_binding_matches(
618 ["2", "g", "g"],
619 indoc! {"
620 ˇ
621
622 brown fox jumps
623 over the lazydog"},
624 )
625 .await;
626 }
627
628 #[gpui::test]
629 async fn test_end_of_document(cx: &mut gpui::TestAppContext) {
630 let mut cx = NeovimBackedTestContext::new(cx).await;
631 cx.assert_binding_matches_all(
632 ["shift-g"],
633 indoc! {"
634 The qˇuick
635
636 brown fox jumps
637 over ˇthe laˇzy dog"},
638 )
639 .await;
640 cx.assert_binding_matches(
641 ["shift-g"],
642 indoc! {"
643
644
645 brown fox jumps
646 over the laˇzy dog"},
647 )
648 .await;
649 cx.assert_binding_matches(
650 ["2", "shift-g"],
651 indoc! {"
652 ˇ
653
654 brown fox jumps
655 over the lazydog"},
656 )
657 .await;
658 }
659
660 #[gpui::test]
661 async fn test_a(cx: &mut gpui::TestAppContext) {
662 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["a"]);
663 cx.assert_all("The qˇuicˇk").await;
664 }
665
666 #[gpui::test]
667 async fn test_insert_end_of_line(cx: &mut gpui::TestAppContext) {
668 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-a"]);
669 cx.assert_all(indoc! {"
670 ˇ
671 The qˇuick
672 brown ˇfox "})
673 .await;
674 }
675
676 #[gpui::test]
677 async fn test_jump_to_first_non_whitespace(cx: &mut gpui::TestAppContext) {
678 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["^"]);
679 cx.assert("The qˇuick").await;
680 cx.assert(" The qˇuick").await;
681 cx.assert("ˇ").await;
682 cx.assert(indoc! {"
683 The qˇuick
684 brown fox"})
685 .await;
686 cx.assert(indoc! {"
687 ˇ
688 The quick"})
689 .await;
690 // Indoc disallows trailing whitespace.
691 cx.assert(" ˇ \nThe quick").await;
692 }
693
694 #[gpui::test]
695 async fn test_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
696 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-i"]);
697 cx.assert("The qˇuick").await;
698 cx.assert(" The qˇuick").await;
699 cx.assert("ˇ").await;
700 cx.assert(indoc! {"
701 The qˇuick
702 brown fox"})
703 .await;
704 cx.assert(indoc! {"
705 ˇ
706 The quick"})
707 .await;
708 }
709
710 #[gpui::test]
711 async fn test_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
712 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-d"]);
713 cx.assert(indoc! {"
714 The qˇuick
715 brown fox"})
716 .await;
717 cx.assert(indoc! {"
718 The quick
719 ˇ
720 brown fox"})
721 .await;
722 }
723
724 #[gpui::test]
725 async fn test_x(cx: &mut gpui::TestAppContext) {
726 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["x"]);
727 cx.assert_all("ˇTeˇsˇt").await;
728 cx.assert(indoc! {"
729 Tesˇt
730 test"})
731 .await;
732 }
733
734 #[gpui::test]
735 async fn test_delete_left(cx: &mut gpui::TestAppContext) {
736 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-x"]);
737 cx.assert_all("ˇTˇeˇsˇt").await;
738 cx.assert(indoc! {"
739 Test
740 ˇtest"})
741 .await;
742 }
743
744 #[gpui::test]
745 async fn test_o(cx: &mut gpui::TestAppContext) {
746 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["o"]);
747 cx.assert("ˇ").await;
748 cx.assert("The ˇquick").await;
749 cx.assert_all(indoc! {"
750 The qˇuick
751 brown ˇfox
752 jumps ˇover"})
753 .await;
754 cx.assert(indoc! {"
755 The quick
756 ˇ
757 brown fox"})
758 .await;
759
760 cx.assert_manual(
761 indoc! {"
762 fn test() {
763 println!(ˇ);
764 }"},
765 Mode::Normal,
766 indoc! {"
767 fn test() {
768 println!();
769 ˇ
770 }"},
771 Mode::Insert,
772 );
773
774 cx.assert_manual(
775 indoc! {"
776 fn test(ˇ) {
777 println!();
778 }"},
779 Mode::Normal,
780 indoc! {"
781 fn test() {
782 ˇ
783 println!();
784 }"},
785 Mode::Insert,
786 );
787 }
788
789 #[gpui::test]
790 async fn test_insert_line_above(cx: &mut gpui::TestAppContext) {
791 let cx = NeovimBackedTestContext::new(cx).await;
792 let mut cx = cx.binding(["shift-o"]);
793 cx.assert("ˇ").await;
794 cx.assert("The ˇquick").await;
795 cx.assert_all(indoc! {"
796 The qˇuick
797 brown ˇfox
798 jumps ˇover"})
799 .await;
800 cx.assert(indoc! {"
801 The quick
802 ˇ
803 brown fox"})
804 .await;
805
806 // Our indentation is smarter than vims. So we don't match here
807 cx.assert_manual(
808 indoc! {"
809 fn test() {
810 println!(ˇ);
811 }"},
812 Mode::Normal,
813 indoc! {"
814 fn test() {
815 ˇ
816 println!();
817 }"},
818 Mode::Insert,
819 );
820 cx.assert_manual(
821 indoc! {"
822 fn test(ˇ) {
823 println!();
824 }"},
825 Mode::Normal,
826 indoc! {"
827 ˇ
828 fn test() {
829 println!();
830 }"},
831 Mode::Insert,
832 );
833 }
834
835 #[gpui::test]
836 async fn test_dd(cx: &mut gpui::TestAppContext) {
837 let mut cx = NeovimBackedTestContext::new(cx).await;
838 cx.assert_neovim_compatible("ˇ", ["d", "d"]).await;
839 cx.assert_neovim_compatible("The ˇquick", ["d", "d"]).await;
840 for marked_text in cx.each_marked_position(indoc! {"
841 The qˇuick
842 brown ˇfox
843 jumps ˇover"})
844 {
845 cx.assert_neovim_compatible(&marked_text, ["d", "d"]).await;
846 }
847 cx.assert_neovim_compatible(
848 indoc! {"
849 The quick
850 ˇ
851 brown fox"},
852 ["d", "d"],
853 )
854 .await;
855 }
856
857 #[gpui::test]
858 async fn test_cc(cx: &mut gpui::TestAppContext) {
859 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "c"]);
860 cx.assert("ˇ").await;
861 cx.assert("The ˇquick").await;
862 cx.assert_all(indoc! {"
863 The quˇick
864 brown ˇfox
865 jumps ˇover"})
866 .await;
867 cx.assert(indoc! {"
868 The quick
869 ˇ
870 brown fox"})
871 .await;
872 }
873
874 #[gpui::test]
875 async fn test_repeated_word(cx: &mut gpui::TestAppContext) {
876 let mut cx = NeovimBackedTestContext::new(cx).await;
877
878 for count in 1..=5 {
879 cx.assert_binding_matches_all(
880 [&count.to_string(), "w"],
881 indoc! {"
882 ˇThe quˇickˇ browˇn
883 ˇ
884 ˇfox ˇjumpsˇ-ˇoˇver
885 ˇthe lazy dog
886 "},
887 )
888 .await;
889 }
890 }
891
892 #[gpui::test]
893 async fn test_h_through_unicode(cx: &mut gpui::TestAppContext) {
894 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["h"]);
895 cx.assert_all("Testˇ├ˇ──ˇ┐ˇTest").await;
896 }
897
898 #[gpui::test]
899 async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
900 let mut cx = NeovimBackedTestContext::new(cx).await;
901
902 for count in 1..=3 {
903 let test_case = indoc! {"
904 ˇaaaˇbˇ ˇbˇ ˇbˇbˇ aˇaaˇbaaa
905 ˇ ˇbˇaaˇa ˇbˇbˇb
906 ˇ
907 ˇb
908 "};
909
910 cx.assert_binding_matches_all([&count.to_string(), "f", "b"], test_case)
911 .await;
912
913 cx.assert_binding_matches_all([&count.to_string(), "t", "b"], test_case)
914 .await;
915 }
916 }
917
918 #[gpui::test]
919 async fn test_capital_f_and_capital_t(cx: &mut gpui::TestAppContext) {
920 let mut cx = NeovimBackedTestContext::new(cx).await;
921 let test_case = indoc! {"
922 ˇaaaˇbˇ ˇbˇ ˇbˇbˇ aˇaaˇbaaa
923 ˇ ˇbˇaaˇa ˇbˇbˇb
924 ˇ•••
925 ˇb
926 "
927 };
928
929 for count in 1..=3 {
930 cx.assert_binding_matches_all([&count.to_string(), "shift-f", "b"], test_case)
931 .await;
932
933 cx.assert_binding_matches_all([&count.to_string(), "shift-t", "b"], test_case)
934 .await;
935 }
936 }
937
938 #[gpui::test]
939 async fn test_f_and_t_multiline(cx: &mut gpui::TestAppContext) {
940 let mut cx = VimTestContext::new(cx, true).await;
941 cx.update_global(|store: &mut SettingsStore, cx| {
942 store.update_user_settings::<VimSettings>(cx, |s| {
943 s.use_multiline_find = Some(true);
944 });
945 });
946
947 cx.assert_binding(
948 ["f", "l"],
949 indoc! {"
950 ˇfunction print() {
951 console.log('ok')
952 }
953 "},
954 Mode::Normal,
955 indoc! {"
956 function print() {
957 consoˇle.log('ok')
958 }
959 "},
960 Mode::Normal,
961 );
962
963 cx.assert_binding(
964 ["t", "l"],
965 indoc! {"
966 ˇfunction print() {
967 console.log('ok')
968 }
969 "},
970 Mode::Normal,
971 indoc! {"
972 function print() {
973 consˇole.log('ok')
974 }
975 "},
976 Mode::Normal,
977 );
978 }
979
980 #[gpui::test]
981 async fn test_capital_f_and_capital_t_multiline(cx: &mut gpui::TestAppContext) {
982 let mut cx = VimTestContext::new(cx, true).await;
983 cx.update_global(|store: &mut SettingsStore, cx| {
984 store.update_user_settings::<VimSettings>(cx, |s| {
985 s.use_multiline_find = Some(true);
986 });
987 });
988
989 cx.assert_binding(
990 ["shift-f", "p"],
991 indoc! {"
992 function print() {
993 console.ˇlog('ok')
994 }
995 "},
996 Mode::Normal,
997 indoc! {"
998 function ˇprint() {
999 console.log('ok')
1000 }
1001 "},
1002 Mode::Normal,
1003 );
1004
1005 cx.assert_binding(
1006 ["shift-t", "p"],
1007 indoc! {"
1008 function print() {
1009 console.ˇlog('ok')
1010 }
1011 "},
1012 Mode::Normal,
1013 indoc! {"
1014 function pˇrint() {
1015 console.log('ok')
1016 }
1017 "},
1018 Mode::Normal,
1019 );
1020 }
1021
1022 #[gpui::test]
1023 async fn test_f_and_t_smartcase(cx: &mut gpui::TestAppContext) {
1024 let mut cx = VimTestContext::new(cx, true).await;
1025 cx.update_global(|store: &mut SettingsStore, cx| {
1026 store.update_user_settings::<VimSettings>(cx, |s| {
1027 s.use_smartcase_find = Some(true);
1028 });
1029 });
1030
1031 cx.assert_binding(
1032 ["f", "p"],
1033 indoc! {"ˇfmt.Println(\"Hello, World!\")"},
1034 Mode::Normal,
1035 indoc! {"fmt.ˇPrintln(\"Hello, World!\")"},
1036 Mode::Normal,
1037 );
1038
1039 cx.assert_binding(
1040 ["shift-f", "p"],
1041 indoc! {"fmt.Printlnˇ(\"Hello, World!\")"},
1042 Mode::Normal,
1043 indoc! {"fmt.ˇPrintln(\"Hello, World!\")"},
1044 Mode::Normal,
1045 );
1046
1047 cx.assert_binding(
1048 ["t", "p"],
1049 indoc! {"ˇfmt.Println(\"Hello, World!\")"},
1050 Mode::Normal,
1051 indoc! {"fmtˇ.Println(\"Hello, World!\")"},
1052 Mode::Normal,
1053 );
1054
1055 cx.assert_binding(
1056 ["shift-t", "p"],
1057 indoc! {"fmt.Printlnˇ(\"Hello, World!\")"},
1058 Mode::Normal,
1059 indoc! {"fmt.Pˇrintln(\"Hello, World!\")"},
1060 Mode::Normal,
1061 );
1062 }
1063
1064 #[gpui::test]
1065 async fn test_percent(cx: &mut TestAppContext) {
1066 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["%"]);
1067 cx.assert_all("ˇconsole.logˇ(ˇvaˇrˇ)ˇ;").await;
1068 cx.assert_all("ˇconsole.logˇ(ˇ'var', ˇ[ˇ1, ˇ2, 3ˇ]ˇ)ˇ;")
1069 .await;
1070 cx.assert_all("let result = curried_funˇ(ˇ)ˇ(ˇ)ˇ;").await;
1071 }
1072
1073 #[gpui::test]
1074 async fn test_end_of_line_with_neovim(cx: &mut gpui::TestAppContext) {
1075 let mut cx = NeovimBackedTestContext::new(cx).await;
1076
1077 // goes to current line end
1078 cx.set_shared_state(indoc! {"ˇaa\nbb\ncc"}).await;
1079 cx.simulate_shared_keystrokes(["$"]).await;
1080 cx.assert_shared_state(indoc! {"aˇa\nbb\ncc"}).await;
1081
1082 // goes to next line end
1083 cx.simulate_shared_keystrokes(["2", "$"]).await;
1084 cx.assert_shared_state("aa\nbˇb\ncc").await;
1085
1086 // try to exceed the final line.
1087 cx.simulate_shared_keystrokes(["4", "$"]).await;
1088 cx.assert_shared_state("aa\nbb\ncˇc").await;
1089 }
1090
1091 #[gpui::test]
1092 async fn test_subword_motions(cx: &mut gpui::TestAppContext) {
1093 let mut cx = VimTestContext::new(cx, true).await;
1094 cx.update(|cx| {
1095 cx.bind_keys(vec![
1096 KeyBinding::new(
1097 "w",
1098 motion::NextSubwordStart {
1099 ignore_punctuation: false,
1100 },
1101 Some("Editor && VimControl && !VimWaiting && !menu"),
1102 ),
1103 KeyBinding::new(
1104 "b",
1105 motion::PreviousSubwordStart {
1106 ignore_punctuation: false,
1107 },
1108 Some("Editor && VimControl && !VimWaiting && !menu"),
1109 ),
1110 KeyBinding::new(
1111 "e",
1112 motion::NextSubwordEnd {
1113 ignore_punctuation: false,
1114 },
1115 Some("Editor && VimControl && !VimWaiting && !menu"),
1116 ),
1117 KeyBinding::new(
1118 "g e",
1119 motion::PreviousSubwordEnd {
1120 ignore_punctuation: false,
1121 },
1122 Some("Editor && VimControl && !VimWaiting && !menu"),
1123 ),
1124 ]);
1125 });
1126
1127 cx.assert_binding_normal(
1128 ["w"],
1129 indoc! {"ˇassert_binding"},
1130 indoc! {"assert_ˇbinding"},
1131 );
1132 // Special case: In 'cw', 'w' acts like 'e'
1133 cx.assert_binding(
1134 ["c", "w"],
1135 indoc! {"ˇassert_binding"},
1136 Mode::Normal,
1137 indoc! {"ˇ_binding"},
1138 Mode::Insert,
1139 );
1140
1141 cx.assert_binding_normal(
1142 ["e"],
1143 indoc! {"ˇassert_binding"},
1144 indoc! {"asserˇt_binding"},
1145 );
1146
1147 cx.assert_binding_normal(
1148 ["b"],
1149 indoc! {"assert_ˇbinding"},
1150 indoc! {"ˇassert_binding"},
1151 );
1152
1153 cx.assert_binding_normal(
1154 ["g", "e"],
1155 indoc! {"assert_bindinˇg"},
1156 indoc! {"asserˇt_binding"},
1157 );
1158 }
1159}