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