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