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