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