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