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