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 JoinLines,
48 JoinLinesNoWhitespace,
49 DeleteLeft,
50 DeleteRight,
51 ChangeToEndOfLine,
52 DeleteToEndOfLine,
53 Yank,
54 YankLine,
55 ChangeCase,
56 ConvertToUpperCase,
57 ConvertToLowerCase,
58 ToggleComments,
59 Undo,
60 Redo,
61 ]
62);
63
64pub(crate) fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
65 Vim::action(editor, cx, Vim::insert_after);
66 Vim::action(editor, cx, Vim::insert_before);
67 Vim::action(editor, cx, Vim::insert_first_non_whitespace);
68 Vim::action(editor, cx, Vim::insert_end_of_line);
69 Vim::action(editor, cx, Vim::insert_line_above);
70 Vim::action(editor, cx, Vim::insert_line_below);
71 Vim::action(editor, cx, Vim::insert_at_previous);
72 Vim::action(editor, cx, Vim::change_case);
73 Vim::action(editor, cx, Vim::convert_to_upper_case);
74 Vim::action(editor, cx, Vim::convert_to_lower_case);
75 Vim::action(editor, cx, Vim::yank_line);
76 Vim::action(editor, cx, Vim::toggle_comments);
77 Vim::action(editor, cx, Vim::paste);
78
79 Vim::action(editor, cx, |vim, _: &DeleteLeft, cx| {
80 vim.record_current_action(cx);
81 let times = Vim::take_count(cx);
82 vim.delete_motion(Motion::Left, times, cx);
83 });
84 Vim::action(editor, cx, |vim, _: &DeleteRight, cx| {
85 vim.record_current_action(cx);
86 let times = Vim::take_count(cx);
87 vim.delete_motion(Motion::Right, times, cx);
88 });
89 Vim::action(editor, cx, |vim, _: &ChangeToEndOfLine, cx| {
90 vim.start_recording(cx);
91 let times = Vim::take_count(cx);
92 vim.change_motion(
93 Motion::EndOfLine {
94 display_lines: false,
95 },
96 times,
97 cx,
98 );
99 });
100 Vim::action(editor, cx, |vim, _: &DeleteToEndOfLine, cx| {
101 vim.record_current_action(cx);
102 let times = Vim::take_count(cx);
103 vim.delete_motion(
104 Motion::EndOfLine {
105 display_lines: false,
106 },
107 times,
108 cx,
109 );
110 });
111 Vim::action(editor, cx, |vim, _: &JoinLines, cx| {
112 vim.join_lines_impl(true, cx);
113 });
114
115 Vim::action(editor, cx, |vim, _: &JoinLinesNoWhitespace, cx| {
116 vim.join_lines_impl(false, cx);
117 });
118
119 Vim::action(editor, cx, |vim, _: &Undo, cx| {
120 let times = Vim::take_count(cx);
121 vim.update_editor(cx, |_, editor, cx| {
122 for _ in 0..times.unwrap_or(1) {
123 editor.undo(&editor::actions::Undo, cx);
124 }
125 });
126 });
127 Vim::action(editor, cx, |vim, _: &Redo, cx| {
128 let times = Vim::take_count(cx);
129 vim.update_editor(cx, |_, editor, cx| {
130 for _ in 0..times.unwrap_or(1) {
131 editor.redo(&editor::actions::Redo, cx);
132 }
133 });
134 });
135
136 repeat::register(editor, cx);
137 scroll::register(editor, cx);
138 search::register(editor, cx);
139 substitute::register(editor, cx);
140 increment::register(editor, cx);
141}
142
143impl Vim {
144 pub fn normal_motion(
145 &mut self,
146 motion: Motion,
147 operator: Option<Operator>,
148 times: Option<usize>,
149 cx: &mut ViewContext<Self>,
150 ) {
151 match operator {
152 None => self.move_cursor(motion, times, cx),
153 Some(Operator::Change) => self.change_motion(motion, times, cx),
154 Some(Operator::Delete) => self.delete_motion(motion, times, cx),
155 Some(Operator::Yank) => self.yank_motion(motion, times, cx),
156 Some(Operator::AddSurrounds { target: None }) => {}
157 Some(Operator::Indent) => self.indent_motion(motion, times, IndentDirection::In, cx),
158 Some(Operator::Rewrap) => self.rewrap_motion(motion, times, cx),
159 Some(Operator::Outdent) => self.indent_motion(motion, times, IndentDirection::Out, cx),
160 Some(Operator::AutoIndent) => {
161 self.indent_motion(motion, times, IndentDirection::Auto, cx)
162 }
163 Some(Operator::Lowercase) => {
164 self.change_case_motion(motion, times, CaseTarget::Lowercase, cx)
165 }
166 Some(Operator::Uppercase) => {
167 self.change_case_motion(motion, times, CaseTarget::Uppercase, cx)
168 }
169 Some(Operator::OppositeCase) => {
170 self.change_case_motion(motion, times, CaseTarget::OppositeCase, cx)
171 }
172 Some(Operator::ToggleComments) => self.toggle_comments_motion(motion, times, cx),
173 Some(operator) => {
174 // Can't do anything for text objects, Ignoring
175 error!("Unexpected normal mode motion operator: {:?}", operator)
176 }
177 }
178 // Exit temporary normal mode (if active).
179 self.exit_temporary_normal(cx);
180 }
181
182 pub fn normal_object(&mut self, object: Object, cx: &mut ViewContext<Self>) {
183 let mut waiting_operator: Option<Operator> = None;
184 match self.maybe_pop_operator() {
185 Some(Operator::Object { around }) => match self.maybe_pop_operator() {
186 Some(Operator::Change) => self.change_object(object, around, cx),
187 Some(Operator::Delete) => self.delete_object(object, around, cx),
188 Some(Operator::Yank) => self.yank_object(object, around, cx),
189 Some(Operator::Indent) => {
190 self.indent_object(object, around, IndentDirection::In, cx)
191 }
192 Some(Operator::Outdent) => {
193 self.indent_object(object, around, IndentDirection::Out, cx)
194 }
195 Some(Operator::AutoIndent) => {
196 self.indent_object(object, around, IndentDirection::Auto, cx)
197 }
198 Some(Operator::Rewrap) => self.rewrap_object(object, around, cx),
199 Some(Operator::Lowercase) => {
200 self.change_case_object(object, around, CaseTarget::Lowercase, cx)
201 }
202 Some(Operator::Uppercase) => {
203 self.change_case_object(object, around, CaseTarget::Uppercase, cx)
204 }
205 Some(Operator::OppositeCase) => {
206 self.change_case_object(object, around, CaseTarget::OppositeCase, cx)
207 }
208 Some(Operator::AddSurrounds { target: None }) => {
209 waiting_operator = Some(Operator::AddSurrounds {
210 target: Some(SurroundsType::Object(object, around)),
211 });
212 }
213 Some(Operator::ToggleComments) => self.toggle_comments_object(object, around, cx),
214 _ => {
215 // Can't do anything for namespace operators. Ignoring
216 }
217 },
218 Some(Operator::DeleteSurrounds) => {
219 waiting_operator = Some(Operator::DeleteSurrounds);
220 }
221 Some(Operator::ChangeSurrounds { target: None }) => {
222 if self.check_and_move_to_valid_bracket_pair(object, cx) {
223 waiting_operator = Some(Operator::ChangeSurrounds {
224 target: Some(object),
225 });
226 }
227 }
228 _ => {
229 // Can't do anything with change/delete/yank/surrounds and text objects. Ignoring
230 }
231 }
232 self.clear_operator(cx);
233 if let Some(operator) = waiting_operator {
234 self.push_operator(operator, cx);
235 }
236 }
237
238 pub(crate) fn move_cursor(
239 &mut self,
240 motion: Motion,
241 times: Option<usize>,
242 cx: &mut ViewContext<Self>,
243 ) {
244 self.update_editor(cx, |_, editor, cx| {
245 let text_layout_details = editor.text_layout_details(cx);
246 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
247 s.move_cursors_with(|map, cursor, goal| {
248 motion
249 .move_point(map, cursor, goal, times, &text_layout_details)
250 .unwrap_or((cursor, goal))
251 })
252 })
253 });
254 }
255
256 fn insert_after(&mut self, _: &InsertAfter, cx: &mut ViewContext<Self>) {
257 self.start_recording(cx);
258 self.switch_mode(Mode::Insert, false, cx);
259 self.update_editor(cx, |_, editor, cx| {
260 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
261 s.move_cursors_with(|map, cursor, _| (right(map, cursor, 1), SelectionGoal::None));
262 });
263 });
264 }
265
266 fn insert_before(&mut self, _: &InsertBefore, cx: &mut ViewContext<Self>) {
267 self.start_recording(cx);
268 self.switch_mode(Mode::Insert, false, cx);
269 }
270
271 fn insert_first_non_whitespace(
272 &mut self,
273 _: &InsertFirstNonWhitespace,
274 cx: &mut ViewContext<Self>,
275 ) {
276 self.start_recording(cx);
277 self.switch_mode(Mode::Insert, false, cx);
278 self.update_editor(cx, |_, editor, cx| {
279 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
280 s.move_cursors_with(|map, cursor, _| {
281 (
282 first_non_whitespace(map, false, cursor),
283 SelectionGoal::None,
284 )
285 });
286 });
287 });
288 }
289
290 fn insert_end_of_line(&mut self, _: &InsertEndOfLine, cx: &mut ViewContext<Self>) {
291 self.start_recording(cx);
292 self.switch_mode(Mode::Insert, false, cx);
293 self.update_editor(cx, |_, editor, cx| {
294 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
295 s.move_cursors_with(|map, cursor, _| {
296 (next_line_end(map, cursor, 1), SelectionGoal::None)
297 });
298 });
299 });
300 }
301
302 fn insert_at_previous(&mut self, _: &InsertAtPrevious, cx: &mut ViewContext<Self>) {
303 self.start_recording(cx);
304 self.switch_mode(Mode::Insert, false, cx);
305 self.update_editor(cx, |vim, editor, cx| {
306 if let Some(marks) = vim.marks.get("^") {
307 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
308 s.select_anchor_ranges(marks.iter().map(|mark| *mark..*mark))
309 });
310 }
311 });
312 }
313
314 fn insert_line_above(&mut self, _: &InsertLineAbove, cx: &mut ViewContext<Self>) {
315 self.start_recording(cx);
316 self.switch_mode(Mode::Insert, false, cx);
317 self.update_editor(cx, |_, editor, cx| {
318 editor.transact(cx, |editor, cx| {
319 let selections = editor.selections.all::<Point>(cx);
320 let snapshot = editor.buffer().read(cx).snapshot(cx);
321
322 let selection_start_rows: BTreeSet<u32> = selections
323 .into_iter()
324 .map(|selection| selection.start.row)
325 .collect();
326 let edits = selection_start_rows
327 .into_iter()
328 .map(|row| {
329 let indent = snapshot
330 .indent_and_comment_for_line(MultiBufferRow(row), cx)
331 .chars()
332 .collect::<String>();
333
334 let start_of_line = Point::new(row, 0);
335 (start_of_line..start_of_line, indent + "\n")
336 })
337 .collect::<Vec<_>>();
338 editor.edit_with_autoindent(edits, cx);
339 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
340 s.move_cursors_with(|map, cursor, _| {
341 let previous_line = motion::start_of_relative_buffer_row(map, cursor, -1);
342 let insert_point = motion::end_of_line(map, false, previous_line, 1);
343 (insert_point, SelectionGoal::None)
344 });
345 });
346 });
347 });
348 }
349
350 fn insert_line_below(&mut self, _: &InsertLineBelow, cx: &mut ViewContext<Self>) {
351 self.start_recording(cx);
352 self.switch_mode(Mode::Insert, false, cx);
353 self.update_editor(cx, |_, editor, cx| {
354 let text_layout_details = editor.text_layout_details(cx);
355 editor.transact(cx, |editor, cx| {
356 let selections = editor.selections.all::<Point>(cx);
357 let snapshot = editor.buffer().read(cx).snapshot(cx);
358
359 let selection_end_rows: BTreeSet<u32> = selections
360 .into_iter()
361 .map(|selection| selection.end.row)
362 .collect();
363 let edits = selection_end_rows
364 .into_iter()
365 .map(|row| {
366 let indent = snapshot
367 .indent_and_comment_for_line(MultiBufferRow(row), cx)
368 .chars()
369 .collect::<String>();
370
371 let end_of_line = Point::new(row, snapshot.line_len(MultiBufferRow(row)));
372 (end_of_line..end_of_line, "\n".to_string() + &indent)
373 })
374 .collect::<Vec<_>>();
375 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
376 s.maybe_move_cursors_with(|map, cursor, goal| {
377 Motion::CurrentLine.move_point(
378 map,
379 cursor,
380 goal,
381 None,
382 &text_layout_details,
383 )
384 });
385 });
386 editor.edit_with_autoindent(edits, cx);
387 });
388 });
389 }
390
391 fn join_lines_impl(&mut self, insert_whitespace: bool, cx: &mut ViewContext<Self>) {
392 self.record_current_action(cx);
393 let mut times = Vim::take_count(cx).unwrap_or(1);
394 if self.mode.is_visual() {
395 times = 1;
396 } else if times > 1 {
397 // 2J joins two lines together (same as J or 1J)
398 times -= 1;
399 }
400
401 self.update_editor(cx, |_, editor, cx| {
402 editor.transact(cx, |editor, cx| {
403 for _ in 0..times {
404 editor.join_lines_impl(insert_whitespace, cx)
405 }
406 })
407 });
408 if self.mode.is_visual() {
409 self.switch_mode(Mode::Normal, true, cx)
410 }
411 }
412
413 fn yank_line(&mut self, _: &YankLine, cx: &mut ViewContext<Self>) {
414 let count = Vim::take_count(cx);
415 self.yank_motion(motion::Motion::CurrentLine, count, cx)
416 }
417
418 fn toggle_comments(&mut self, _: &ToggleComments, cx: &mut ViewContext<Self>) {
419 self.record_current_action(cx);
420 self.store_visual_marks(cx);
421 self.update_editor(cx, |vim, editor, cx| {
422 editor.transact(cx, |editor, cx| {
423 let original_positions = vim.save_selection_starts(editor, cx);
424 editor.toggle_comments(&Default::default(), cx);
425 vim.restore_selection_cursors(editor, cx, original_positions);
426 });
427 });
428 if self.mode.is_visual() {
429 self.switch_mode(Mode::Normal, true, cx)
430 }
431 }
432
433 pub(crate) fn normal_replace(&mut self, text: Arc<str>, cx: &mut ViewContext<Self>) {
434 let count = Vim::take_count(cx).unwrap_or(1);
435 self.stop_recording(cx);
436 self.update_editor(cx, |_, editor, cx| {
437 editor.transact(cx, |editor, cx| {
438 editor.set_clip_at_line_ends(false, cx);
439 let (map, display_selections) = editor.selections.all_display(cx);
440
441 let mut edits = Vec::new();
442 for selection in display_selections {
443 let mut range = selection.range();
444 for _ in 0..count {
445 let new_point = movement::saturating_right(&map, range.end);
446 if range.end == new_point {
447 return;
448 }
449 range.end = new_point;
450 }
451
452 edits.push((
453 range.start.to_offset(&map, Bias::Left)
454 ..range.end.to_offset(&map, Bias::Left),
455 text.repeat(count),
456 ))
457 }
458
459 editor.edit(edits, cx);
460 editor.set_clip_at_line_ends(true, cx);
461 editor.change_selections(None, cx, |s| {
462 s.move_with(|map, selection| {
463 let point = movement::saturating_left(map, selection.head());
464 selection.collapse_to(point, SelectionGoal::None)
465 });
466 });
467 });
468 });
469 self.pop_operator(cx);
470 }
471
472 pub fn save_selection_starts(
473 &self,
474 editor: &Editor,
475 cx: &mut ViewContext<Editor>,
476 ) -> HashMap<usize, Anchor> {
477 let (map, selections) = editor.selections.all_display(cx);
478 selections
479 .iter()
480 .map(|selection| {
481 (
482 selection.id,
483 map.display_point_to_anchor(selection.start, Bias::Right),
484 )
485 })
486 .collect::<HashMap<_, _>>()
487 }
488
489 pub fn restore_selection_cursors(
490 &self,
491 editor: &mut Editor,
492 cx: &mut ViewContext<Editor>,
493 mut positions: HashMap<usize, Anchor>,
494 ) {
495 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
496 s.move_with(|map, selection| {
497 if let Some(anchor) = positions.remove(&selection.id) {
498 selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None);
499 }
500 });
501 });
502 }
503
504 fn exit_temporary_normal(&mut self, cx: &mut ViewContext<Self>) {
505 if self.temp_mode {
506 self.switch_mode(Mode::Insert, true, cx);
507 }
508 }
509}
510#[cfg(test)]
511mod test {
512 use gpui::{KeyBinding, TestAppContext, UpdateGlobal};
513 use indoc::indoc;
514 use language::language_settings::AllLanguageSettings;
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
1422 #[gpui::test]
1423 async fn test_gq(cx: &mut gpui::TestAppContext) {
1424 let mut cx = NeovimBackedTestContext::new(cx).await;
1425 cx.set_neovim_option("textwidth=5").await;
1426
1427 cx.update(|cx| {
1428 SettingsStore::update_global(cx, |settings, cx| {
1429 settings.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1430 settings.defaults.preferred_line_length = Some(5);
1431 });
1432 })
1433 });
1434
1435 cx.set_shared_state("ˇth th th th th th\n").await;
1436 cx.simulate_shared_keystrokes("g q q").await;
1437 cx.shared_state().await.assert_eq("th th\nth th\nˇth th\n");
1438
1439 cx.set_shared_state("ˇth th th th th th\nth th th th th th\n")
1440 .await;
1441 cx.simulate_shared_keystrokes("v j g q").await;
1442 cx.shared_state()
1443 .await
1444 .assert_eq("th th\nth th\nth th\nth th\nth th\nˇth th\n");
1445 }
1446
1447 #[gpui::test]
1448 async fn test_o_comment(cx: &mut gpui::TestAppContext) {
1449 let mut cx = NeovimBackedTestContext::new(cx).await;
1450 cx.set_neovim_option("filetype=rust").await;
1451
1452 cx.set_shared_state("// helloˇ\n").await;
1453 cx.simulate_shared_keystrokes("o").await;
1454 cx.shared_state().await.assert_eq("// hello\n// ˇ\n");
1455 cx.simulate_shared_keystrokes("x escape shift-o").await;
1456 cx.shared_state().await.assert_eq("// hello\n// ˇ\n// x\n");
1457 }
1458}