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