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