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