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