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