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