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