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