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