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