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