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