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