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