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