1mod change;
2mod convert;
3mod delete;
4mod increment;
5pub(crate) mod mark;
6pub(crate) mod 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 Vim,
19 indent::IndentDirection,
20 motion::{self, Motion, first_non_whitespace, next_line_end, right},
21 object::Object,
22 state::{Mark, Mode, Operator},
23 surrounds::SurroundsType,
24};
25use collections::BTreeSet;
26use convert::ConvertTarget;
27use editor::Editor;
28use editor::{Anchor, SelectionEffects};
29use editor::{Bias, ToPoint};
30use editor::{display_map::ToDisplayPoint, movement};
31use gpui::{Context, Window, actions};
32use language::{Point, SelectionGoal};
33use log::error;
34use multi_buffer::MultiBufferRow;
35
36actions!(
37 vim,
38 [
39 /// Inserts text after the cursor.
40 InsertAfter,
41 /// Inserts text before the cursor.
42 InsertBefore,
43 /// Inserts at the first non-whitespace character.
44 InsertFirstNonWhitespace,
45 /// Inserts at the end of the line.
46 InsertEndOfLine,
47 /// Inserts a new line above the current line.
48 InsertLineAbove,
49 /// Inserts a new line below the current line.
50 InsertLineBelow,
51 /// Inserts an empty line above without entering insert mode.
52 InsertEmptyLineAbove,
53 /// Inserts an empty line below without entering insert mode.
54 InsertEmptyLineBelow,
55 /// Inserts at the previous insert position.
56 InsertAtPrevious,
57 /// Joins the current line with the next line.
58 JoinLines,
59 /// Joins lines without adding whitespace.
60 JoinLinesNoWhitespace,
61 /// Deletes character to the left.
62 DeleteLeft,
63 /// Deletes character to the right.
64 DeleteRight,
65 /// Deletes using Helix-style behavior.
66 HelixDelete,
67 /// Collapse the current selection
68 HelixCollapseSelection,
69 /// Changes from cursor to end of line.
70 ChangeToEndOfLine,
71 /// Deletes from cursor to end of line.
72 DeleteToEndOfLine,
73 /// Yanks (copies) the selected text.
74 Yank,
75 /// Yanks the entire line.
76 YankLine,
77 /// Yanks from cursor to end of line.
78 YankToEndOfLine,
79 /// Toggles the case of selected text.
80 ChangeCase,
81 /// Converts selected text to uppercase.
82 ConvertToUpperCase,
83 /// Converts selected text to lowercase.
84 ConvertToLowerCase,
85 /// Applies ROT13 cipher to selected text.
86 ConvertToRot13,
87 /// Applies ROT47 cipher to selected text.
88 ConvertToRot47,
89 /// Toggles comments for selected lines.
90 ToggleComments,
91 /// Shows the current location in the file.
92 ShowLocation,
93 /// Undoes the last change.
94 Undo,
95 /// Redoes the last undone change.
96 Redo,
97 /// Undoes all changes to the most recently changed line.
98 UndoLastLine,
99 /// Go to tab page (with count support).
100 GoToTab,
101 /// Go to previous tab page (with count support).
102 GoToPreviousTab,
103 /// Goes to the previous reference to the symbol under the cursor.
104 GoToPreviousReference,
105 /// Goes to the next reference to the symbol under the cursor.
106 GoToNextReference,
107 ]
108);
109
110pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
111 Vim::action(editor, cx, Vim::insert_after);
112 Vim::action(editor, cx, Vim::insert_before);
113 Vim::action(editor, cx, Vim::insert_first_non_whitespace);
114 Vim::action(editor, cx, Vim::insert_end_of_line);
115 Vim::action(editor, cx, Vim::insert_line_above);
116 Vim::action(editor, cx, Vim::insert_line_below);
117 Vim::action(editor, cx, Vim::insert_empty_line_above);
118 Vim::action(editor, cx, Vim::insert_empty_line_below);
119 Vim::action(editor, cx, Vim::insert_at_previous);
120 Vim::action(editor, cx, Vim::change_case);
121 Vim::action(editor, cx, Vim::convert_to_upper_case);
122 Vim::action(editor, cx, Vim::convert_to_lower_case);
123 Vim::action(editor, cx, Vim::convert_to_rot13);
124 Vim::action(editor, cx, Vim::convert_to_rot47);
125 Vim::action(editor, cx, Vim::yank_line);
126 Vim::action(editor, cx, Vim::yank_to_end_of_line);
127 Vim::action(editor, cx, Vim::toggle_comments);
128 Vim::action(editor, cx, Vim::paste);
129 Vim::action(editor, cx, Vim::show_location);
130
131 Vim::action(editor, cx, |vim, _: &DeleteLeft, window, cx| {
132 vim.record_current_action(cx);
133 let times = Vim::take_count(cx);
134 let forced_motion = Vim::take_forced_motion(cx);
135 vim.delete_motion(Motion::Left, times, forced_motion, window, cx);
136 });
137 Vim::action(editor, cx, |vim, _: &DeleteRight, window, cx| {
138 vim.record_current_action(cx);
139 let times = Vim::take_count(cx);
140 let forced_motion = Vim::take_forced_motion(cx);
141 vim.delete_motion(Motion::Right, times, forced_motion, window, cx);
142 });
143
144 Vim::action(editor, cx, |vim, _: &HelixDelete, window, cx| {
145 vim.record_current_action(cx);
146 vim.update_editor(cx, |_, editor, cx| {
147 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
148 s.move_with(&mut |map, selection| {
149 if selection.is_empty() {
150 selection.end = movement::right(map, selection.end)
151 }
152 })
153 })
154 });
155 vim.visual_delete(false, window, cx);
156 vim.switch_mode(Mode::HelixNormal, true, window, cx);
157 });
158
159 Vim::action(editor, cx, |vim, _: &HelixCollapseSelection, window, cx| {
160 vim.update_editor(cx, |_, editor, cx| {
161 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
162 s.move_with(&mut |map, selection| {
163 let mut point = selection.head();
164 if !selection.reversed && !selection.is_empty() {
165 point = movement::left(map, selection.head());
166 }
167 selection.collapse_to(point, selection.goal)
168 });
169 });
170 });
171 });
172
173 Vim::action(editor, cx, |vim, _: &ChangeToEndOfLine, window, cx| {
174 vim.start_recording(cx);
175 let times = Vim::take_count(cx);
176 let forced_motion = Vim::take_forced_motion(cx);
177 vim.change_motion(
178 Motion::EndOfLine {
179 display_lines: false,
180 },
181 times,
182 forced_motion,
183 window,
184 cx,
185 );
186 });
187 Vim::action(editor, cx, |vim, _: &DeleteToEndOfLine, window, cx| {
188 vim.record_current_action(cx);
189 let times = Vim::take_count(cx);
190 let forced_motion = Vim::take_forced_motion(cx);
191 vim.delete_motion(
192 Motion::EndOfLine {
193 display_lines: false,
194 },
195 times,
196 forced_motion,
197 window,
198 cx,
199 );
200 });
201 Vim::action(editor, cx, |vim, _: &JoinLines, window, cx| {
202 vim.join_lines_impl(true, window, cx);
203 });
204
205 Vim::action(editor, cx, |vim, _: &JoinLinesNoWhitespace, window, cx| {
206 vim.join_lines_impl(false, window, cx);
207 });
208
209 Vim::action(editor, cx, |vim, _: &GoToPreviousReference, window, cx| {
210 let count = Vim::take_count(cx);
211 vim.update_editor(cx, |_, editor, cx| {
212 let task = editor.go_to_reference_before_or_after_position(
213 editor::Direction::Prev,
214 count.unwrap_or(1),
215 window,
216 cx,
217 );
218 if let Some(task) = task {
219 task.detach_and_log_err(cx);
220 };
221 });
222 });
223
224 Vim::action(editor, cx, |vim, _: &GoToNextReference, window, cx| {
225 let count = Vim::take_count(cx);
226 vim.update_editor(cx, |_, editor, cx| {
227 let task = editor.go_to_reference_before_or_after_position(
228 editor::Direction::Next,
229 count.unwrap_or(1),
230 window,
231 cx,
232 );
233 if let Some(task) = task {
234 task.detach_and_log_err(cx);
235 };
236 });
237 });
238
239 Vim::action(editor, cx, |vim, _: &Undo, window, cx| {
240 let times = Vim::take_count(cx);
241 Vim::take_forced_motion(cx);
242 vim.update_editor(cx, |_, editor, cx| {
243 for _ in 0..times.unwrap_or(1) {
244 editor.undo(&editor::actions::Undo, window, cx);
245 }
246 });
247 });
248 Vim::action(editor, cx, |vim, _: &Redo, window, cx| {
249 let times = Vim::take_count(cx);
250 Vim::take_forced_motion(cx);
251 vim.update_editor(cx, |_, editor, cx| {
252 for _ in 0..times.unwrap_or(1) {
253 editor.redo(&editor::actions::Redo, window, cx);
254 }
255 });
256 });
257 Vim::action(editor, cx, |vim, _: &UndoLastLine, window, cx| {
258 Vim::take_forced_motion(cx);
259 vim.update_editor(cx, |vim, editor, cx| {
260 let snapshot = editor.buffer().read(cx).snapshot(cx);
261 let Some(last_change) = editor.change_list.last_before_grouping() else {
262 return;
263 };
264
265 let anchors = last_change.to_vec();
266 let mut last_row = None;
267 let ranges: Vec<_> = anchors
268 .iter()
269 .filter_map(|anchor| {
270 let point = anchor.to_point(&snapshot);
271 if last_row == Some(point.row) {
272 return None;
273 }
274 last_row = Some(point.row);
275 let line_range = Point::new(point.row, 0)
276 ..Point::new(point.row, snapshot.line_len(MultiBufferRow(point.row)));
277 Some((
278 snapshot.anchor_before(line_range.start)
279 ..snapshot.anchor_after(line_range.end),
280 line_range,
281 ))
282 })
283 .collect();
284
285 let edits = editor.buffer().update(cx, |buffer, cx| {
286 let current_content = ranges
287 .iter()
288 .map(|(anchors, _)| {
289 buffer
290 .snapshot(cx)
291 .text_for_range(anchors.clone())
292 .collect::<String>()
293 })
294 .collect::<Vec<_>>();
295 let mut content_before_undo = current_content.clone();
296 let mut undo_count = 0;
297
298 loop {
299 let undone_tx = buffer.undo(cx);
300 undo_count += 1;
301 let mut content_after_undo = Vec::new();
302
303 let mut line_changed = false;
304 for ((anchors, _), text_before_undo) in
305 ranges.iter().zip(content_before_undo.iter())
306 {
307 let snapshot = buffer.snapshot(cx);
308 let text_after_undo =
309 snapshot.text_for_range(anchors.clone()).collect::<String>();
310
311 if &text_after_undo != text_before_undo {
312 line_changed = true;
313 }
314 content_after_undo.push(text_after_undo);
315 }
316
317 content_before_undo = content_after_undo;
318 if !line_changed {
319 break;
320 }
321 if undone_tx == vim.undo_last_line_tx {
322 break;
323 }
324 }
325
326 let edits = ranges
327 .into_iter()
328 .zip(content_before_undo.into_iter().zip(current_content))
329 .filter_map(|((_, mut points), (mut old_text, new_text))| {
330 if new_text == old_text {
331 return None;
332 }
333 let common_suffix_starts_at = old_text
334 .char_indices()
335 .rev()
336 .zip(new_text.chars().rev())
337 .find_map(
338 |((i, a), b)| {
339 if a != b { Some(i + a.len_utf8()) } else { None }
340 },
341 )
342 .unwrap_or(old_text.len());
343 points.end.column -= (old_text.len() - common_suffix_starts_at) as u32;
344 old_text = old_text.split_at(common_suffix_starts_at).0.to_string();
345 let common_prefix_len = old_text
346 .char_indices()
347 .zip(new_text.chars())
348 .find_map(|((i, a), b)| if a != b { Some(i) } else { None })
349 .unwrap_or(0);
350 points.start.column = common_prefix_len as u32;
351 old_text = old_text.split_at(common_prefix_len).1.to_string();
352
353 Some((points, old_text))
354 })
355 .collect::<Vec<_>>();
356
357 for _ in 0..undo_count {
358 buffer.redo(cx);
359 }
360 edits
361 });
362 vim.undo_last_line_tx = editor.transact(window, cx, |editor, window, cx| {
363 editor.change_list.invert_last_group();
364 editor.edit(edits, cx);
365 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
366 s.select_anchor_ranges(anchors.into_iter().map(|a| a..a));
367 })
368 });
369 });
370 });
371
372 repeat::register(editor, cx);
373 scroll::register(editor, cx);
374 search::register(editor, cx);
375 substitute::register(editor, cx);
376 increment::register(editor, cx);
377}
378
379impl Vim {
380 pub fn normal_motion(
381 &mut self,
382 motion: Motion,
383 operator: Option<Operator>,
384 times: Option<usize>,
385 forced_motion: bool,
386 window: &mut Window,
387 cx: &mut Context<Self>,
388 ) {
389 match operator {
390 None => self.move_cursor(motion, times, window, cx),
391 Some(Operator::Change) => self.change_motion(motion, times, forced_motion, window, cx),
392 Some(Operator::Delete) => self.delete_motion(motion, times, forced_motion, window, cx),
393 Some(Operator::Yank) => self.yank_motion(motion, times, forced_motion, window, cx),
394 Some(Operator::AddSurrounds { target: None }) => {}
395 Some(Operator::Indent) => self.indent_motion(
396 motion,
397 times,
398 forced_motion,
399 IndentDirection::In,
400 window,
401 cx,
402 ),
403 Some(Operator::Rewrap) => self.rewrap_motion(motion, times, forced_motion, window, cx),
404 Some(Operator::Outdent) => self.indent_motion(
405 motion,
406 times,
407 forced_motion,
408 IndentDirection::Out,
409 window,
410 cx,
411 ),
412 Some(Operator::AutoIndent) => self.indent_motion(
413 motion,
414 times,
415 forced_motion,
416 IndentDirection::Auto,
417 window,
418 cx,
419 ),
420 Some(Operator::ShellCommand) => {
421 self.shell_command_motion(motion, times, forced_motion, window, cx)
422 }
423 Some(Operator::Lowercase) => self.convert_motion(
424 motion,
425 times,
426 forced_motion,
427 ConvertTarget::LowerCase,
428 window,
429 cx,
430 ),
431 Some(Operator::Uppercase) => self.convert_motion(
432 motion,
433 times,
434 forced_motion,
435 ConvertTarget::UpperCase,
436 window,
437 cx,
438 ),
439 Some(Operator::OppositeCase) => self.convert_motion(
440 motion,
441 times,
442 forced_motion,
443 ConvertTarget::OppositeCase,
444 window,
445 cx,
446 ),
447 Some(Operator::Rot13) => self.convert_motion(
448 motion,
449 times,
450 forced_motion,
451 ConvertTarget::Rot13,
452 window,
453 cx,
454 ),
455 Some(Operator::Rot47) => self.convert_motion(
456 motion,
457 times,
458 forced_motion,
459 ConvertTarget::Rot47,
460 window,
461 cx,
462 ),
463 Some(Operator::ToggleComments) => {
464 self.toggle_comments_motion(motion, times, forced_motion, window, cx)
465 }
466 Some(Operator::ReplaceWithRegister) => {
467 self.replace_with_register_motion(motion, times, forced_motion, window, cx)
468 }
469 Some(Operator::Exchange) => {
470 self.exchange_motion(motion, times, forced_motion, window, cx)
471 }
472 Some(operator) => {
473 // Can't do anything for text objects, Ignoring
474 error!("Unexpected normal mode motion operator: {:?}", operator)
475 }
476 }
477 // Exit temporary normal mode (if active).
478 self.exit_temporary_normal(window, cx);
479 }
480
481 pub fn normal_object(
482 &mut self,
483 object: Object,
484 times: Option<usize>,
485 opening: bool,
486 window: &mut Window,
487 cx: &mut Context<Self>,
488 ) {
489 let mut waiting_operator: Option<Operator> = None;
490 match self.maybe_pop_operator() {
491 Some(Operator::Object { around }) => match self.maybe_pop_operator() {
492 Some(Operator::Change) => self.change_object(object, around, times, window, cx),
493 Some(Operator::Delete) => self.delete_object(object, around, times, window, cx),
494 Some(Operator::Yank) => self.yank_object(object, around, times, window, cx),
495 Some(Operator::Indent) => {
496 self.indent_object(object, around, IndentDirection::In, times, window, cx)
497 }
498 Some(Operator::Outdent) => {
499 self.indent_object(object, around, IndentDirection::Out, times, window, cx)
500 }
501 Some(Operator::AutoIndent) => {
502 self.indent_object(object, around, IndentDirection::Auto, times, window, cx)
503 }
504 Some(Operator::ShellCommand) => {
505 self.shell_command_object(object, around, window, cx);
506 }
507 Some(Operator::Rewrap) => self.rewrap_object(object, around, times, window, cx),
508 Some(Operator::Lowercase) => {
509 self.convert_object(object, around, ConvertTarget::LowerCase, times, window, cx)
510 }
511 Some(Operator::Uppercase) => {
512 self.convert_object(object, around, ConvertTarget::UpperCase, times, window, cx)
513 }
514 Some(Operator::OppositeCase) => self.convert_object(
515 object,
516 around,
517 ConvertTarget::OppositeCase,
518 times,
519 window,
520 cx,
521 ),
522 Some(Operator::Rot13) => {
523 self.convert_object(object, around, ConvertTarget::Rot13, times, window, cx)
524 }
525 Some(Operator::Rot47) => {
526 self.convert_object(object, around, ConvertTarget::Rot47, times, window, cx)
527 }
528 Some(Operator::AddSurrounds { target: None }) => {
529 waiting_operator = Some(Operator::AddSurrounds {
530 target: Some(SurroundsType::Object(object, around)),
531 });
532 }
533 Some(Operator::ToggleComments) => {
534 self.toggle_comments_object(object, around, times, window, cx)
535 }
536 Some(Operator::ReplaceWithRegister) => {
537 self.replace_with_register_object(object, around, window, cx)
538 }
539 Some(Operator::Exchange) => self.exchange_object(object, around, window, cx),
540 Some(Operator::HelixMatch) => {
541 self.select_current_object(object, around, window, cx)
542 }
543 _ => {
544 // Can't do anything for namespace operators. Ignoring
545 }
546 },
547 Some(Operator::HelixNext { around }) => {
548 self.select_next_object(object, around, window, cx);
549 }
550 Some(Operator::HelixPrevious { around }) => {
551 self.select_previous_object(object, around, window, cx);
552 }
553 Some(Operator::DeleteSurrounds) => {
554 waiting_operator = Some(Operator::DeleteSurrounds);
555 }
556 Some(Operator::ChangeSurrounds { target: None, .. }) => {
557 if self.check_and_move_to_valid_bracket_pair(object, window, cx) {
558 waiting_operator = Some(Operator::ChangeSurrounds {
559 target: Some(object),
560 opening,
561 });
562 }
563 }
564 _ => {
565 // Can't do anything with change/delete/yank/surrounds and text objects. Ignoring
566 }
567 }
568 self.clear_operator(window, cx);
569 if let Some(operator) = waiting_operator {
570 self.push_operator(operator, window, cx);
571 }
572 }
573
574 pub(crate) fn move_cursor(
575 &mut self,
576 motion: Motion,
577 times: Option<usize>,
578 window: &mut Window,
579 cx: &mut Context<Self>,
580 ) {
581 self.update_editor(cx, |vim, editor, cx| {
582 let text_layout_details = editor.text_layout_details(window, cx);
583
584 // If vim is in temporary mode and the motion being used is
585 // `EndOfLine` ($), we'll want to disable clipping at line ends so
586 // that the newline character can be selected so that, when moving
587 // back to visual mode, the cursor will be placed after the last
588 // character and not before it.
589 let clip_at_line_ends = editor.clip_at_line_ends(cx);
590 let should_disable_clip = matches!(motion, Motion::EndOfLine { .. }) && vim.temp_mode;
591
592 if should_disable_clip {
593 editor.set_clip_at_line_ends(false, cx)
594 };
595
596 editor.change_selections(
597 SelectionEffects::default().nav_history(motion.push_to_jump_list()),
598 window,
599 cx,
600 |s| {
601 s.move_cursors_with(&mut |map, cursor, goal| {
602 motion
603 .move_point(map, cursor, goal, times, &text_layout_details)
604 .unwrap_or((cursor, goal))
605 })
606 },
607 );
608
609 if should_disable_clip {
610 editor.set_clip_at_line_ends(clip_at_line_ends, cx);
611 };
612 });
613 }
614
615 fn insert_after(&mut self, _: &InsertAfter, window: &mut Window, cx: &mut Context<Self>) {
616 self.start_recording(cx);
617 self.switch_mode(Mode::Insert, false, window, cx);
618 self.update_editor(cx, |_, editor, cx| {
619 editor.change_selections(Default::default(), window, cx, |s| {
620 s.move_cursors_with(&mut |map, cursor, _| {
621 (right(map, cursor, 1), SelectionGoal::None)
622 });
623 });
624 });
625 }
626
627 fn insert_before(&mut self, _: &InsertBefore, window: &mut Window, cx: &mut Context<Self>) {
628 self.start_recording(cx);
629 if self.mode.is_visual() {
630 let current_mode = self.mode;
631 self.update_editor(cx, |_, editor, cx| {
632 editor.change_selections(Default::default(), window, cx, |s| {
633 s.move_with(&mut |map, selection| {
634 if current_mode == Mode::VisualLine {
635 let start_of_line = motion::start_of_line(map, false, selection.start);
636 selection.collapse_to(start_of_line, SelectionGoal::None)
637 } else {
638 selection.collapse_to(selection.start, SelectionGoal::None)
639 }
640 });
641 });
642 });
643 }
644 self.switch_mode(Mode::Insert, false, window, cx);
645 }
646
647 fn insert_first_non_whitespace(
648 &mut self,
649 _: &InsertFirstNonWhitespace,
650 window: &mut Window,
651 cx: &mut Context<Self>,
652 ) {
653 self.start_recording(cx);
654 self.switch_mode(Mode::Insert, false, window, cx);
655 self.update_editor(cx, |_, editor, cx| {
656 editor.change_selections(Default::default(), window, cx, |s| {
657 s.move_cursors_with(&mut |map, cursor, _| {
658 (
659 first_non_whitespace(map, false, cursor),
660 SelectionGoal::None,
661 )
662 });
663 });
664 });
665 }
666
667 fn insert_end_of_line(
668 &mut self,
669 _: &InsertEndOfLine,
670 window: &mut Window,
671 cx: &mut Context<Self>,
672 ) {
673 self.start_recording(cx);
674 self.switch_mode(Mode::Insert, false, window, cx);
675 self.update_editor(cx, |_, editor, cx| {
676 editor.change_selections(Default::default(), window, cx, |s| {
677 s.move_cursors_with(&mut |map, cursor, _| {
678 (next_line_end(map, cursor, 1), SelectionGoal::None)
679 });
680 });
681 });
682 }
683
684 fn insert_at_previous(
685 &mut self,
686 _: &InsertAtPrevious,
687 window: &mut Window,
688 cx: &mut Context<Self>,
689 ) {
690 self.start_recording(cx);
691 self.switch_mode(Mode::Insert, false, window, cx);
692 self.update_editor(cx, |vim, editor, cx| {
693 if let Some(Mark::Local(marks)) = vim.get_mark("^", editor, window, cx)
694 && !marks.is_empty()
695 {
696 editor.change_selections(Default::default(), window, cx, |s| {
697 s.select_anchor_ranges(marks.iter().map(|mark| *mark..*mark))
698 });
699 }
700 });
701 }
702
703 fn insert_line_above(
704 &mut self,
705 _: &InsertLineAbove,
706 window: &mut Window,
707 cx: &mut Context<Self>,
708 ) {
709 self.start_recording(cx);
710 self.switch_mode(Mode::Insert, false, window, cx);
711 self.update_editor(cx, |_, editor, cx| {
712 editor.transact(window, cx, |editor, window, cx| {
713 let selections = editor.selections.all::<Point>(&editor.display_snapshot(cx));
714 let snapshot = editor.buffer().read(cx).snapshot(cx);
715
716 let selection_start_rows: BTreeSet<u32> = selections
717 .into_iter()
718 .map(|selection| selection.start.row)
719 .collect();
720 let edits = selection_start_rows
721 .into_iter()
722 .map(|row| {
723 let indent = snapshot
724 .indent_and_comment_for_line(MultiBufferRow(row), cx)
725 .chars()
726 .collect::<String>();
727
728 let start_of_line = Point::new(row, 0);
729 (start_of_line..start_of_line, indent + "\n")
730 })
731 .collect::<Vec<_>>();
732 editor.edit_with_autoindent(edits, cx);
733 editor.change_selections(Default::default(), window, cx, |s| {
734 s.move_cursors_with(&mut |map, cursor, _| {
735 let previous_line = map.start_of_relative_buffer_row(cursor, -1);
736 let insert_point = motion::end_of_line(map, false, previous_line, 1);
737 (insert_point, SelectionGoal::None)
738 });
739 });
740 });
741 });
742 }
743
744 fn insert_line_below(
745 &mut self,
746 _: &InsertLineBelow,
747 window: &mut Window,
748 cx: &mut Context<Self>,
749 ) {
750 self.start_recording(cx);
751 self.switch_mode(Mode::Insert, false, window, cx);
752 self.update_editor(cx, |_, editor, cx| {
753 let text_layout_details = editor.text_layout_details(window, cx);
754 editor.transact(window, cx, |editor, window, cx| {
755 let selections = editor.selections.all::<Point>(&editor.display_snapshot(cx));
756 let snapshot = editor.buffer().read(cx).snapshot(cx);
757
758 let selection_end_rows: BTreeSet<u32> = selections
759 .into_iter()
760 .map(|selection| selection.end.row)
761 .collect();
762 let edits = selection_end_rows
763 .into_iter()
764 .map(|row| {
765 let indent = snapshot
766 .indent_and_comment_for_line(MultiBufferRow(row), cx)
767 .chars()
768 .collect::<String>();
769
770 let end_of_line = Point::new(row, snapshot.line_len(MultiBufferRow(row)));
771 (end_of_line..end_of_line, "\n".to_string() + &indent)
772 })
773 .collect::<Vec<_>>();
774 editor.change_selections(Default::default(), window, cx, |s| {
775 s.maybe_move_cursors_with(&mut |map, cursor, goal| {
776 Motion::CurrentLine.move_point(
777 map,
778 cursor,
779 goal,
780 None,
781 &text_layout_details,
782 )
783 });
784 });
785 editor.edit_with_autoindent(edits, cx);
786 });
787 });
788 }
789
790 fn insert_empty_line_above(
791 &mut self,
792 _: &InsertEmptyLineAbove,
793 window: &mut Window,
794 cx: &mut Context<Self>,
795 ) {
796 self.record_current_action(cx);
797 let count = Vim::take_count(cx).unwrap_or(1);
798 Vim::take_forced_motion(cx);
799 self.update_editor(cx, |_, editor, cx| {
800 editor.transact(window, cx, |editor, _, cx| {
801 let selections = editor.selections.all::<Point>(&editor.display_snapshot(cx));
802
803 let selection_start_rows: BTreeSet<u32> = selections
804 .into_iter()
805 .map(|selection| selection.start.row)
806 .collect();
807 let edits = selection_start_rows
808 .into_iter()
809 .map(|row| {
810 let start_of_line = Point::new(row, 0);
811 (start_of_line..start_of_line, "\n".repeat(count))
812 })
813 .collect::<Vec<_>>();
814 editor.edit(edits, cx);
815 });
816 });
817 }
818
819 fn insert_empty_line_below(
820 &mut self,
821 _: &InsertEmptyLineBelow,
822 window: &mut Window,
823 cx: &mut Context<Self>,
824 ) {
825 self.record_current_action(cx);
826 let count = Vim::take_count(cx).unwrap_or(1);
827 Vim::take_forced_motion(cx);
828 self.update_editor(cx, |_, editor, cx| {
829 editor.transact(window, cx, |editor, window, cx| {
830 let display_map = editor.display_snapshot(cx);
831 let selections = editor.selections.all::<Point>(&display_map);
832 let snapshot = editor.buffer().read(cx).snapshot(cx);
833 let display_selections = editor.selections.all_display(&display_map);
834 let original_positions = display_selections
835 .iter()
836 .map(|s| (s.id, s.head()))
837 .collect::<HashMap<_, _>>();
838
839 let selection_end_rows: BTreeSet<u32> = selections
840 .into_iter()
841 .map(|selection| selection.end.row)
842 .collect();
843 let edits = selection_end_rows
844 .into_iter()
845 .map(|row| {
846 let end_of_line = Point::new(row, snapshot.line_len(MultiBufferRow(row)));
847 (end_of_line..end_of_line, "\n".repeat(count))
848 })
849 .collect::<Vec<_>>();
850 editor.edit(edits, cx);
851
852 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
853 s.move_with(&mut |_, selection| {
854 if let Some(position) = original_positions.get(&selection.id) {
855 selection.collapse_to(*position, SelectionGoal::None);
856 }
857 });
858 });
859 });
860 });
861 }
862
863 fn join_lines_impl(
864 &mut self,
865 insert_whitespace: bool,
866 window: &mut Window,
867 cx: &mut Context<Self>,
868 ) {
869 self.record_current_action(cx);
870 let mut times = Vim::take_count(cx).unwrap_or(1);
871 Vim::take_forced_motion(cx);
872 if self.mode.is_visual() {
873 times = 1;
874 } else if times > 1 {
875 // 2J joins two lines together (same as J or 1J)
876 times -= 1;
877 }
878
879 self.update_editor(cx, |_, editor, cx| {
880 editor.transact(window, cx, |editor, window, cx| {
881 for _ in 0..times {
882 editor.join_lines_impl(insert_whitespace, window, cx)
883 }
884 })
885 });
886 if self.mode.is_visual() {
887 self.switch_mode(Mode::Normal, true, window, cx)
888 }
889 }
890
891 fn yank_line(&mut self, _: &YankLine, window: &mut Window, cx: &mut Context<Self>) {
892 let count = Vim::take_count(cx);
893 let forced_motion = Vim::take_forced_motion(cx);
894 self.yank_motion(
895 motion::Motion::CurrentLine,
896 count,
897 forced_motion,
898 window,
899 cx,
900 )
901 }
902
903 fn yank_to_end_of_line(
904 &mut self,
905 _: &YankToEndOfLine,
906 window: &mut Window,
907 cx: &mut Context<Self>,
908 ) {
909 let count = Vim::take_count(cx);
910 let forced_motion = Vim::take_forced_motion(cx);
911 self.yank_motion(
912 motion::Motion::EndOfLine {
913 display_lines: false,
914 },
915 count,
916 forced_motion,
917 window,
918 cx,
919 )
920 }
921
922 fn show_location(&mut self, _: &ShowLocation, _: &mut Window, cx: &mut Context<Self>) {
923 let count = Vim::take_count(cx);
924 Vim::take_forced_motion(cx);
925 self.update_editor(cx, |vim, editor, cx| {
926 let selection = editor.selections.newest_anchor();
927 let Some((buffer, point, _)) = editor
928 .buffer()
929 .read(cx)
930 .point_to_buffer_point(selection.head(), cx)
931 else {
932 return;
933 };
934 let filename = if let Some(file) = buffer.read(cx).file() {
935 if count.is_some() {
936 if let Some(local) = file.as_local() {
937 local.abs_path(cx).to_string_lossy().into_owned()
938 } else {
939 file.full_path(cx).to_string_lossy().into_owned()
940 }
941 } else {
942 file.path().display(file.path_style(cx)).into_owned()
943 }
944 } else {
945 "[No Name]".into()
946 };
947 let buffer = buffer.read(cx);
948 let lines = buffer.max_point().row + 1;
949 let current_line = point.row;
950 let percentage = current_line as f32 / lines as f32;
951 let modified = if buffer.is_dirty() { " [modified]" } else { "" };
952 vim.set_status_label(
953 format!(
954 "{}{} {} lines --{:.0}%--",
955 filename,
956 modified,
957 lines,
958 percentage * 100.0,
959 ),
960 cx,
961 );
962 });
963 }
964
965 fn toggle_comments(&mut self, _: &ToggleComments, window: &mut Window, cx: &mut Context<Self>) {
966 self.record_current_action(cx);
967 self.store_visual_marks(window, cx);
968 self.update_editor(cx, |vim, editor, cx| {
969 editor.transact(window, cx, |editor, window, cx| {
970 let original_positions = vim.save_selection_starts(editor, cx);
971 editor.toggle_comments(&Default::default(), window, cx);
972 vim.restore_selection_cursors(editor, window, cx, original_positions);
973 });
974 });
975 if self.mode.is_visual() {
976 self.switch_mode(Mode::Normal, true, window, cx)
977 }
978 }
979
980 pub(crate) fn normal_replace(
981 &mut self,
982 text: Arc<str>,
983 window: &mut Window,
984 cx: &mut Context<Self>,
985 ) {
986 // We need to use `text.chars().count()` instead of `text.len()` here as
987 // `len()` counts bytes, not characters.
988 let char_count = text.chars().count();
989 let count = Vim::take_count(cx).unwrap_or(char_count);
990 let is_return_char = text == "\n".into() || text == "\r".into();
991 let repeat_count = match (is_return_char, char_count) {
992 (true, _) => 0,
993 (_, 1) => count,
994 (_, _) => 1,
995 };
996
997 Vim::take_forced_motion(cx);
998 self.stop_recording(cx);
999 self.update_editor(cx, |_, editor, cx| {
1000 editor.transact(window, cx, |editor, window, cx| {
1001 editor.set_clip_at_line_ends(false, cx);
1002 let display_map = editor.display_snapshot(cx);
1003 let display_selections = editor.selections.all_display(&display_map);
1004
1005 let mut edits = Vec::with_capacity(display_selections.len());
1006 for selection in &display_selections {
1007 let mut range = selection.range();
1008 for _ in 0..count {
1009 let new_point = movement::saturating_right(&display_map, range.end);
1010 if range.end == new_point {
1011 return;
1012 }
1013 range.end = new_point;
1014 }
1015
1016 edits.push((
1017 range.start.to_offset(&display_map, Bias::Left)
1018 ..range.end.to_offset(&display_map, Bias::Left),
1019 text.repeat(repeat_count),
1020 ));
1021 }
1022
1023 editor.edit(edits, cx);
1024 if is_return_char {
1025 editor.newline(&editor::actions::Newline, window, cx);
1026 }
1027 editor.set_clip_at_line_ends(true, cx);
1028 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1029 s.move_with(&mut |map, selection| {
1030 let point = movement::saturating_left(map, selection.head());
1031 selection.collapse_to(point, SelectionGoal::None)
1032 });
1033 });
1034 });
1035 });
1036 self.pop_operator(window, cx);
1037 }
1038
1039 pub fn save_selection_starts(
1040 &self,
1041 editor: &Editor,
1042 cx: &mut Context<Editor>,
1043 ) -> HashMap<usize, Anchor> {
1044 let display_map = editor.display_snapshot(cx);
1045 let selections = editor.selections.all_display(&display_map);
1046 selections
1047 .iter()
1048 .map(|selection| {
1049 (
1050 selection.id,
1051 display_map.display_point_to_anchor(selection.start, Bias::Right),
1052 )
1053 })
1054 .collect::<HashMap<_, _>>()
1055 }
1056
1057 pub fn restore_selection_cursors(
1058 &self,
1059 editor: &mut Editor,
1060 window: &mut Window,
1061 cx: &mut Context<Editor>,
1062 mut positions: HashMap<usize, Anchor>,
1063 ) {
1064 editor.change_selections(Default::default(), window, cx, |s| {
1065 s.move_with(&mut |map, selection| {
1066 if let Some(anchor) = positions.remove(&selection.id) {
1067 selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None);
1068 }
1069 });
1070 });
1071 }
1072
1073 fn exit_temporary_normal(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1074 if self.temp_mode {
1075 self.switch_mode(Mode::Insert, true, window, cx);
1076 }
1077 }
1078}
1079
1080#[cfg(test)]
1081mod test {
1082 use gpui::{KeyBinding, TestAppContext, UpdateGlobal};
1083 use indoc::indoc;
1084 use settings::SettingsStore;
1085
1086 use crate::{
1087 motion,
1088 state::Mode::{self},
1089 test::{NeovimBackedTestContext, VimTestContext},
1090 };
1091
1092 #[gpui::test]
1093 async fn test_h(cx: &mut gpui::TestAppContext) {
1094 let mut cx = NeovimBackedTestContext::new(cx).await;
1095 cx.simulate_at_each_offset(
1096 "h",
1097 indoc! {"
1098 ˇThe qˇuick
1099 ˇbrown"
1100 },
1101 )
1102 .await
1103 .assert_matches();
1104 }
1105
1106 #[gpui::test]
1107 async fn test_backspace(cx: &mut gpui::TestAppContext) {
1108 let mut cx = NeovimBackedTestContext::new(cx).await;
1109 cx.simulate_at_each_offset(
1110 "backspace",
1111 indoc! {"
1112 ˇThe qˇuick
1113 ˇbrown"
1114 },
1115 )
1116 .await
1117 .assert_matches();
1118 }
1119
1120 #[gpui::test]
1121 async fn test_j(cx: &mut gpui::TestAppContext) {
1122 let mut cx = NeovimBackedTestContext::new(cx).await;
1123
1124 cx.set_shared_state(indoc! {"
1125 aaˇaa
1126 😃😃"
1127 })
1128 .await;
1129 cx.simulate_shared_keystrokes("j").await;
1130 cx.shared_state().await.assert_eq(indoc! {"
1131 aaaa
1132 😃ˇ😃"
1133 });
1134
1135 cx.simulate_at_each_offset(
1136 "j",
1137 indoc! {"
1138 ˇThe qˇuick broˇwn
1139 ˇfox jumps"
1140 },
1141 )
1142 .await
1143 .assert_matches();
1144 }
1145
1146 #[gpui::test]
1147 async fn test_enter(cx: &mut gpui::TestAppContext) {
1148 let mut cx = NeovimBackedTestContext::new(cx).await;
1149 cx.simulate_at_each_offset(
1150 "enter",
1151 indoc! {"
1152 ˇThe qˇuick broˇwn
1153 ˇfox jumps"
1154 },
1155 )
1156 .await
1157 .assert_matches();
1158 }
1159
1160 #[gpui::test]
1161 async fn test_k(cx: &mut gpui::TestAppContext) {
1162 let mut cx = NeovimBackedTestContext::new(cx).await;
1163 cx.simulate_at_each_offset(
1164 "k",
1165 indoc! {"
1166 ˇThe qˇuick
1167 ˇbrown fˇox jumˇps"
1168 },
1169 )
1170 .await
1171 .assert_matches();
1172 }
1173
1174 #[gpui::test]
1175 async fn test_l(cx: &mut gpui::TestAppContext) {
1176 let mut cx = NeovimBackedTestContext::new(cx).await;
1177 cx.simulate_at_each_offset(
1178 "l",
1179 indoc! {"
1180 ˇThe qˇuicˇk
1181 ˇbrowˇn"},
1182 )
1183 .await
1184 .assert_matches();
1185 }
1186
1187 #[gpui::test]
1188 async fn test_jump_to_line_boundaries(cx: &mut gpui::TestAppContext) {
1189 let mut cx = NeovimBackedTestContext::new(cx).await;
1190 cx.simulate_at_each_offset(
1191 "$",
1192 indoc! {"
1193 ˇThe qˇuicˇk
1194 ˇbrowˇn"},
1195 )
1196 .await
1197 .assert_matches();
1198 cx.simulate_at_each_offset(
1199 "0",
1200 indoc! {"
1201 ˇThe qˇuicˇk
1202 ˇbrowˇn"},
1203 )
1204 .await
1205 .assert_matches();
1206 }
1207
1208 #[gpui::test]
1209 async fn test_jump_to_end(cx: &mut gpui::TestAppContext) {
1210 let mut cx = NeovimBackedTestContext::new(cx).await;
1211
1212 cx.simulate_at_each_offset(
1213 "shift-g",
1214 indoc! {"
1215 The ˇquick
1216
1217 brown fox jumps
1218 overˇ the lazy doˇg"},
1219 )
1220 .await
1221 .assert_matches();
1222 cx.simulate(
1223 "shift-g",
1224 indoc! {"
1225 The quiˇck
1226
1227 brown"},
1228 )
1229 .await
1230 .assert_matches();
1231 cx.simulate(
1232 "shift-g",
1233 indoc! {"
1234 The quiˇck
1235
1236 "},
1237 )
1238 .await
1239 .assert_matches();
1240 }
1241
1242 #[gpui::test]
1243 async fn test_w(cx: &mut gpui::TestAppContext) {
1244 let mut cx = NeovimBackedTestContext::new(cx).await;
1245 cx.simulate_at_each_offset(
1246 "w",
1247 indoc! {"
1248 The ˇquickˇ-ˇbrown
1249 ˇ
1250 ˇ
1251 ˇfox_jumps ˇover
1252 ˇthˇe"},
1253 )
1254 .await
1255 .assert_matches();
1256 cx.simulate_at_each_offset(
1257 "shift-w",
1258 indoc! {"
1259 The ˇquickˇ-ˇbrown
1260 ˇ
1261 ˇ
1262 ˇfox_jumps ˇover
1263 ˇthˇe"},
1264 )
1265 .await
1266 .assert_matches();
1267 }
1268
1269 #[gpui::test]
1270 async fn test_end_of_word(cx: &mut gpui::TestAppContext) {
1271 let mut cx = NeovimBackedTestContext::new(cx).await;
1272 cx.simulate_at_each_offset(
1273 "e",
1274 indoc! {"
1275 Thˇe quicˇkˇ-browˇn
1276
1277
1278 fox_jumpˇs oveˇr
1279 thˇe"},
1280 )
1281 .await
1282 .assert_matches();
1283 cx.simulate_at_each_offset(
1284 "shift-e",
1285 indoc! {"
1286 Thˇe quicˇkˇ-browˇn
1287
1288
1289 fox_jumpˇs oveˇr
1290 thˇe"},
1291 )
1292 .await
1293 .assert_matches();
1294 }
1295
1296 #[gpui::test]
1297 async fn test_b(cx: &mut gpui::TestAppContext) {
1298 let mut cx = NeovimBackedTestContext::new(cx).await;
1299 cx.simulate_at_each_offset(
1300 "b",
1301 indoc! {"
1302 ˇThe ˇquickˇ-ˇbrown
1303 ˇ
1304 ˇ
1305 ˇfox_jumps ˇover
1306 ˇthe"},
1307 )
1308 .await
1309 .assert_matches();
1310 cx.simulate_at_each_offset(
1311 "shift-b",
1312 indoc! {"
1313 ˇThe ˇquickˇ-ˇbrown
1314 ˇ
1315 ˇ
1316 ˇfox_jumps ˇover
1317 ˇthe"},
1318 )
1319 .await
1320 .assert_matches();
1321 }
1322
1323 #[gpui::test]
1324 async fn test_gg(cx: &mut gpui::TestAppContext) {
1325 let mut cx = NeovimBackedTestContext::new(cx).await;
1326 cx.simulate_at_each_offset(
1327 "g g",
1328 indoc! {"
1329 The qˇuick
1330
1331 brown fox jumps
1332 over ˇthe laˇzy dog"},
1333 )
1334 .await
1335 .assert_matches();
1336 cx.simulate(
1337 "g g",
1338 indoc! {"
1339
1340
1341 brown fox jumps
1342 over the laˇzy dog"},
1343 )
1344 .await
1345 .assert_matches();
1346 cx.simulate(
1347 "2 g g",
1348 indoc! {"
1349 ˇ
1350
1351 brown fox jumps
1352 over the lazydog"},
1353 )
1354 .await
1355 .assert_matches();
1356 }
1357
1358 #[gpui::test]
1359 async fn test_end_of_document(cx: &mut gpui::TestAppContext) {
1360 let mut cx = NeovimBackedTestContext::new(cx).await;
1361 cx.simulate_at_each_offset(
1362 "shift-g",
1363 indoc! {"
1364 The qˇuick
1365
1366 brown fox jumps
1367 over ˇthe laˇzy dog"},
1368 )
1369 .await
1370 .assert_matches();
1371 cx.simulate(
1372 "shift-g",
1373 indoc! {"
1374
1375
1376 brown fox jumps
1377 over the laˇzy dog"},
1378 )
1379 .await
1380 .assert_matches();
1381 cx.simulate(
1382 "2 shift-g",
1383 indoc! {"
1384 ˇ
1385
1386 brown fox jumps
1387 over the lazydog"},
1388 )
1389 .await
1390 .assert_matches();
1391 }
1392
1393 #[gpui::test]
1394 async fn test_a(cx: &mut gpui::TestAppContext) {
1395 let mut cx = NeovimBackedTestContext::new(cx).await;
1396 cx.simulate_at_each_offset("a", "The qˇuicˇk")
1397 .await
1398 .assert_matches();
1399 }
1400
1401 #[gpui::test]
1402 async fn test_insert_end_of_line(cx: &mut gpui::TestAppContext) {
1403 let mut cx = NeovimBackedTestContext::new(cx).await;
1404 cx.simulate_at_each_offset(
1405 "shift-a",
1406 indoc! {"
1407 ˇ
1408 The qˇuick
1409 brown ˇfox "},
1410 )
1411 .await
1412 .assert_matches();
1413 }
1414
1415 #[gpui::test]
1416 async fn test_jump_to_first_non_whitespace(cx: &mut gpui::TestAppContext) {
1417 let mut cx = NeovimBackedTestContext::new(cx).await;
1418 cx.simulate("^", "The qˇuick").await.assert_matches();
1419 cx.simulate("^", " The qˇuick").await.assert_matches();
1420 cx.simulate("^", "ˇ").await.assert_matches();
1421 cx.simulate(
1422 "^",
1423 indoc! {"
1424 The qˇuick
1425 brown fox"},
1426 )
1427 .await
1428 .assert_matches();
1429 cx.simulate(
1430 "^",
1431 indoc! {"
1432 ˇ
1433 The quick"},
1434 )
1435 .await
1436 .assert_matches();
1437 // Indoc disallows trailing whitespace.
1438 cx.simulate("^", " ˇ \nThe quick").await.assert_matches();
1439 }
1440
1441 #[gpui::test]
1442 async fn test_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
1443 let mut cx = NeovimBackedTestContext::new(cx).await;
1444 cx.simulate("shift-i", "The qˇuick").await.assert_matches();
1445 cx.simulate("shift-i", " The qˇuick").await.assert_matches();
1446 cx.simulate("shift-i", "ˇ").await.assert_matches();
1447 cx.simulate(
1448 "shift-i",
1449 indoc! {"
1450 The qˇuick
1451 brown fox"},
1452 )
1453 .await
1454 .assert_matches();
1455 cx.simulate(
1456 "shift-i",
1457 indoc! {"
1458 ˇ
1459 The quick"},
1460 )
1461 .await
1462 .assert_matches();
1463 }
1464
1465 #[gpui::test]
1466 async fn test_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
1467 let mut cx = NeovimBackedTestContext::new(cx).await;
1468 cx.simulate(
1469 "shift-d",
1470 indoc! {"
1471 The qˇuick
1472 brown fox"},
1473 )
1474 .await
1475 .assert_matches();
1476 cx.simulate(
1477 "shift-d",
1478 indoc! {"
1479 The quick
1480 ˇ
1481 brown fox"},
1482 )
1483 .await
1484 .assert_matches();
1485 }
1486
1487 #[gpui::test]
1488 async fn test_x(cx: &mut gpui::TestAppContext) {
1489 let mut cx = NeovimBackedTestContext::new(cx).await;
1490 cx.simulate_at_each_offset("x", "ˇTeˇsˇt")
1491 .await
1492 .assert_matches();
1493 cx.simulate(
1494 "x",
1495 indoc! {"
1496 Tesˇt
1497 test"},
1498 )
1499 .await
1500 .assert_matches();
1501 }
1502
1503 #[gpui::test]
1504 async fn test_delete_left(cx: &mut gpui::TestAppContext) {
1505 let mut cx = NeovimBackedTestContext::new(cx).await;
1506 cx.simulate_at_each_offset("shift-x", "ˇTˇeˇsˇt")
1507 .await
1508 .assert_matches();
1509 cx.simulate(
1510 "shift-x",
1511 indoc! {"
1512 Test
1513 ˇtest"},
1514 )
1515 .await
1516 .assert_matches();
1517 }
1518
1519 #[gpui::test]
1520 async fn test_o(cx: &mut gpui::TestAppContext) {
1521 let mut cx = NeovimBackedTestContext::new(cx).await;
1522 cx.simulate("o", "ˇ").await.assert_matches();
1523 cx.simulate("o", "The ˇquick").await.assert_matches();
1524 cx.simulate_at_each_offset(
1525 "o",
1526 indoc! {"
1527 The qˇuick
1528 brown ˇfox
1529 jumps ˇover"},
1530 )
1531 .await
1532 .assert_matches();
1533 cx.simulate(
1534 "o",
1535 indoc! {"
1536 The quick
1537 ˇ
1538 brown fox"},
1539 )
1540 .await
1541 .assert_matches();
1542
1543 cx.assert_binding(
1544 "o",
1545 indoc! {"
1546 fn test() {
1547 println!(ˇ);
1548 }"},
1549 Mode::Normal,
1550 indoc! {"
1551 fn test() {
1552 println!();
1553 ˇ
1554 }"},
1555 Mode::Insert,
1556 );
1557
1558 cx.assert_binding(
1559 "o",
1560 indoc! {"
1561 fn test(ˇ) {
1562 println!();
1563 }"},
1564 Mode::Normal,
1565 indoc! {"
1566 fn test() {
1567 ˇ
1568 println!();
1569 }"},
1570 Mode::Insert,
1571 );
1572 }
1573
1574 #[gpui::test]
1575 async fn test_insert_line_above(cx: &mut gpui::TestAppContext) {
1576 let mut cx = NeovimBackedTestContext::new(cx).await;
1577 cx.simulate("shift-o", "ˇ").await.assert_matches();
1578 cx.simulate("shift-o", "The ˇquick").await.assert_matches();
1579 cx.simulate_at_each_offset(
1580 "shift-o",
1581 indoc! {"
1582 The qˇuick
1583 brown ˇfox
1584 jumps ˇover"},
1585 )
1586 .await
1587 .assert_matches();
1588 cx.simulate(
1589 "shift-o",
1590 indoc! {"
1591 The quick
1592 ˇ
1593 brown fox"},
1594 )
1595 .await
1596 .assert_matches();
1597
1598 // Our indentation is smarter than vims. So we don't match here
1599 cx.assert_binding(
1600 "shift-o",
1601 indoc! {"
1602 fn test() {
1603 println!(ˇ);
1604 }"},
1605 Mode::Normal,
1606 indoc! {"
1607 fn test() {
1608 ˇ
1609 println!();
1610 }"},
1611 Mode::Insert,
1612 );
1613 cx.assert_binding(
1614 "shift-o",
1615 indoc! {"
1616 fn test(ˇ) {
1617 println!();
1618 }"},
1619 Mode::Normal,
1620 indoc! {"
1621 ˇ
1622 fn test() {
1623 println!();
1624 }"},
1625 Mode::Insert,
1626 );
1627 }
1628
1629 #[gpui::test]
1630 async fn test_insert_empty_line(cx: &mut gpui::TestAppContext) {
1631 let mut cx = NeovimBackedTestContext::new(cx).await;
1632 cx.simulate("[ space", "ˇ").await.assert_matches();
1633 cx.simulate("[ space", "The ˇquick").await.assert_matches();
1634 cx.simulate_at_each_offset(
1635 "3 [ space",
1636 indoc! {"
1637 The qˇuick
1638 brown ˇfox
1639 jumps ˇover"},
1640 )
1641 .await
1642 .assert_matches();
1643 cx.simulate_at_each_offset(
1644 "[ space",
1645 indoc! {"
1646 The qˇuick
1647 brown ˇfox
1648 jumps ˇover"},
1649 )
1650 .await
1651 .assert_matches();
1652 cx.simulate(
1653 "[ space",
1654 indoc! {"
1655 The quick
1656 ˇ
1657 brown fox"},
1658 )
1659 .await
1660 .assert_matches();
1661
1662 cx.simulate("] space", "ˇ").await.assert_matches();
1663 cx.simulate("] space", "The ˇquick").await.assert_matches();
1664 cx.simulate_at_each_offset(
1665 "3 ] space",
1666 indoc! {"
1667 The qˇuick
1668 brown ˇfox
1669 jumps ˇover"},
1670 )
1671 .await
1672 .assert_matches();
1673 cx.simulate_at_each_offset(
1674 "] space",
1675 indoc! {"
1676 The qˇuick
1677 brown ˇfox
1678 jumps ˇover"},
1679 )
1680 .await
1681 .assert_matches();
1682 cx.simulate(
1683 "] space",
1684 indoc! {"
1685 The quick
1686 ˇ
1687 brown fox"},
1688 )
1689 .await
1690 .assert_matches();
1691 }
1692
1693 #[gpui::test]
1694 async fn test_dd(cx: &mut gpui::TestAppContext) {
1695 let mut cx = NeovimBackedTestContext::new(cx).await;
1696 cx.simulate("d d", "ˇ").await.assert_matches();
1697 cx.simulate("d d", "The ˇquick").await.assert_matches();
1698 cx.simulate_at_each_offset(
1699 "d d",
1700 indoc! {"
1701 The qˇuick
1702 brown ˇfox
1703 jumps ˇover"},
1704 )
1705 .await
1706 .assert_matches();
1707 cx.simulate(
1708 "d d",
1709 indoc! {"
1710 The quick
1711 ˇ
1712 brown fox"},
1713 )
1714 .await
1715 .assert_matches();
1716 }
1717
1718 #[gpui::test]
1719 async fn test_cc(cx: &mut gpui::TestAppContext) {
1720 let mut cx = NeovimBackedTestContext::new(cx).await;
1721 cx.simulate("c c", "ˇ").await.assert_matches();
1722 cx.simulate("c c", "The ˇquick").await.assert_matches();
1723 cx.simulate_at_each_offset(
1724 "c c",
1725 indoc! {"
1726 The quˇick
1727 brown ˇfox
1728 jumps ˇover"},
1729 )
1730 .await
1731 .assert_matches();
1732 cx.simulate(
1733 "c c",
1734 indoc! {"
1735 The quick
1736 ˇ
1737 brown fox"},
1738 )
1739 .await
1740 .assert_matches();
1741 }
1742
1743 #[gpui::test]
1744 async fn test_repeated_word(cx: &mut gpui::TestAppContext) {
1745 let mut cx = NeovimBackedTestContext::new(cx).await;
1746
1747 for count in 1..=5 {
1748 cx.simulate_at_each_offset(
1749 &format!("{count} w"),
1750 indoc! {"
1751 ˇThe quˇickˇ browˇn
1752 ˇ
1753 ˇfox ˇjumpsˇ-ˇoˇver
1754 ˇthe lazy dog
1755 "},
1756 )
1757 .await
1758 .assert_matches();
1759 }
1760 }
1761
1762 #[gpui::test]
1763 async fn test_h_through_unicode(cx: &mut gpui::TestAppContext) {
1764 let mut cx = NeovimBackedTestContext::new(cx).await;
1765 cx.simulate_at_each_offset("h", "Testˇ├ˇ──ˇ┐ˇTest")
1766 .await
1767 .assert_matches();
1768 }
1769
1770 #[gpui::test]
1771 async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
1772 let mut cx = NeovimBackedTestContext::new(cx).await;
1773
1774 for count in 1..=3 {
1775 let test_case = indoc! {"
1776 ˇaaaˇbˇ ˇbˇ ˇbˇbˇ aˇaaˇbaaa
1777 ˇ ˇbˇaaˇa ˇbˇbˇb
1778 ˇ
1779 ˇb
1780 "};
1781
1782 cx.simulate_at_each_offset(&format!("{count} f b"), test_case)
1783 .await
1784 .assert_matches();
1785
1786 cx.simulate_at_each_offset(&format!("{count} t b"), test_case)
1787 .await
1788 .assert_matches();
1789 }
1790 }
1791
1792 #[gpui::test]
1793 async fn test_capital_f_and_capital_t(cx: &mut gpui::TestAppContext) {
1794 let mut cx = NeovimBackedTestContext::new(cx).await;
1795 let test_case = indoc! {"
1796 ˇaaaˇbˇ ˇbˇ ˇbˇbˇ aˇaaˇbaaa
1797 ˇ ˇbˇaaˇa ˇbˇbˇb
1798 ˇ•••
1799 ˇb
1800 "
1801 };
1802
1803 for count in 1..=3 {
1804 cx.simulate_at_each_offset(&format!("{count} shift-f b"), test_case)
1805 .await
1806 .assert_matches();
1807
1808 cx.simulate_at_each_offset(&format!("{count} shift-t b"), test_case)
1809 .await
1810 .assert_matches();
1811 }
1812 }
1813
1814 #[gpui::test]
1815 async fn test_f_and_t_smartcase(cx: &mut gpui::TestAppContext) {
1816 let mut cx = VimTestContext::new(cx, true).await;
1817 cx.update_global(|store: &mut SettingsStore, cx| {
1818 store.update_user_settings(cx, |s| {
1819 s.vim.get_or_insert_default().use_smartcase_find = Some(true);
1820 });
1821 });
1822
1823 cx.assert_binding(
1824 "f p",
1825 indoc! {"ˇfmt.Println(\"Hello, World!\")"},
1826 Mode::Normal,
1827 indoc! {"fmt.ˇPrintln(\"Hello, World!\")"},
1828 Mode::Normal,
1829 );
1830
1831 cx.assert_binding(
1832 "shift-f p",
1833 indoc! {"fmt.Printlnˇ(\"Hello, World!\")"},
1834 Mode::Normal,
1835 indoc! {"fmt.ˇPrintln(\"Hello, World!\")"},
1836 Mode::Normal,
1837 );
1838
1839 cx.assert_binding(
1840 "t p",
1841 indoc! {"ˇfmt.Println(\"Hello, World!\")"},
1842 Mode::Normal,
1843 indoc! {"fmtˇ.Println(\"Hello, World!\")"},
1844 Mode::Normal,
1845 );
1846
1847 cx.assert_binding(
1848 "shift-t p",
1849 indoc! {"fmt.Printlnˇ(\"Hello, World!\")"},
1850 Mode::Normal,
1851 indoc! {"fmt.Pˇrintln(\"Hello, World!\")"},
1852 Mode::Normal,
1853 );
1854 }
1855
1856 #[gpui::test]
1857 async fn test_percent(cx: &mut TestAppContext) {
1858 let mut cx = NeovimBackedTestContext::new(cx).await;
1859 cx.simulate_at_each_offset("%", "ˇconsole.logˇ(ˇvaˇrˇ)ˇ;")
1860 .await
1861 .assert_matches();
1862 cx.simulate_at_each_offset("%", "ˇconsole.logˇ(ˇ'var', ˇ[ˇ1, ˇ2, 3ˇ]ˇ)ˇ;")
1863 .await
1864 .assert_matches();
1865 cx.simulate_at_each_offset("%", "let result = curried_funˇ(ˇ)ˇ(ˇ)ˇ;")
1866 .await
1867 .assert_matches();
1868 }
1869
1870 #[gpui::test]
1871 async fn test_percent_in_comment(cx: &mut TestAppContext) {
1872 let mut cx = NeovimBackedTestContext::new(cx).await;
1873 cx.simulate_at_each_offset("%", "// ˇconsole.logˇ(ˇvaˇrˇ)ˇ;")
1874 .await
1875 .assert_matches();
1876 cx.simulate_at_each_offset("%", "// ˇ{ ˇ{ˇ}ˇ }ˇ")
1877 .await
1878 .assert_matches();
1879 // Template-style brackets (like Liquid {% %} and {{ }})
1880 cx.simulate_at_each_offset("%", "ˇ{ˇ% block %ˇ}ˇ")
1881 .await
1882 .assert_matches();
1883 cx.simulate_at_each_offset("%", "ˇ{ˇ{ˇ var ˇ}ˇ}ˇ")
1884 .await
1885 .assert_matches();
1886 }
1887
1888 #[gpui::test]
1889 async fn test_end_of_line_with_neovim(cx: &mut gpui::TestAppContext) {
1890 let mut cx = NeovimBackedTestContext::new(cx).await;
1891
1892 // goes to current line end
1893 cx.set_shared_state(indoc! {"ˇaa\nbb\ncc"}).await;
1894 cx.simulate_shared_keystrokes("$").await;
1895 cx.shared_state().await.assert_eq("aˇa\nbb\ncc");
1896
1897 // goes to next line end
1898 cx.simulate_shared_keystrokes("2 $").await;
1899 cx.shared_state().await.assert_eq("aa\nbˇb\ncc");
1900
1901 // try to exceed the final line.
1902 cx.simulate_shared_keystrokes("4 $").await;
1903 cx.shared_state().await.assert_eq("aa\nbb\ncˇc");
1904 }
1905
1906 #[gpui::test]
1907 async fn test_subword_motions(cx: &mut gpui::TestAppContext) {
1908 let mut cx = VimTestContext::new(cx, true).await;
1909 cx.update(|_, cx| {
1910 cx.bind_keys(vec![
1911 KeyBinding::new(
1912 "w",
1913 motion::NextSubwordStart {
1914 ignore_punctuation: false,
1915 },
1916 Some("Editor && VimControl && !VimWaiting && !menu"),
1917 ),
1918 KeyBinding::new(
1919 "b",
1920 motion::PreviousSubwordStart {
1921 ignore_punctuation: false,
1922 },
1923 Some("Editor && VimControl && !VimWaiting && !menu"),
1924 ),
1925 KeyBinding::new(
1926 "e",
1927 motion::NextSubwordEnd {
1928 ignore_punctuation: false,
1929 },
1930 Some("Editor && VimControl && !VimWaiting && !menu"),
1931 ),
1932 KeyBinding::new(
1933 "g e",
1934 motion::PreviousSubwordEnd {
1935 ignore_punctuation: false,
1936 },
1937 Some("Editor && VimControl && !VimWaiting && !menu"),
1938 ),
1939 ]);
1940 });
1941
1942 cx.assert_binding_normal("w", indoc! {"ˇassert_binding"}, indoc! {"assert_ˇbinding"});
1943 // Special case: In 'cw', 'w' acts like 'e'
1944 cx.assert_binding(
1945 "c w",
1946 indoc! {"ˇassert_binding"},
1947 Mode::Normal,
1948 indoc! {"ˇ_binding"},
1949 Mode::Insert,
1950 );
1951
1952 cx.assert_binding_normal("e", indoc! {"ˇassert_binding"}, indoc! {"asserˇt_binding"});
1953
1954 // Subword end should stop at EOL
1955 cx.assert_binding_normal("e", indoc! {"foo_bˇar\nbaz"}, indoc! {"foo_baˇr\nbaz"});
1956
1957 // Already at subword end, should move to next subword on next line
1958 cx.assert_binding_normal(
1959 "e",
1960 indoc! {"foo_barˇ\nbaz_qux"},
1961 indoc! {"foo_bar\nbaˇz_qux"},
1962 );
1963
1964 // CamelCase at EOL
1965 cx.assert_binding_normal("e", indoc! {"fooˇBar\nbaz"}, indoc! {"fooBaˇr\nbaz"});
1966
1967 cx.assert_binding_normal("b", indoc! {"assert_ˇbinding"}, indoc! {"ˇassert_binding"});
1968
1969 cx.assert_binding_normal(
1970 "g e",
1971 indoc! {"assert_bindinˇg"},
1972 indoc! {"asserˇt_binding"},
1973 );
1974 }
1975
1976 #[gpui::test]
1977 async fn test_r(cx: &mut gpui::TestAppContext) {
1978 let mut cx = NeovimBackedTestContext::new(cx).await;
1979
1980 cx.set_shared_state("ˇhello\n").await;
1981 cx.simulate_shared_keystrokes("r -").await;
1982 cx.shared_state().await.assert_eq("ˇ-ello\n");
1983
1984 cx.set_shared_state("ˇhello\n").await;
1985 cx.simulate_shared_keystrokes("3 r -").await;
1986 cx.shared_state().await.assert_eq("--ˇ-lo\n");
1987
1988 cx.set_shared_state("ˇhello\n").await;
1989 cx.simulate_shared_keystrokes("r - 2 l .").await;
1990 cx.shared_state().await.assert_eq("-eˇ-lo\n");
1991
1992 cx.set_shared_state("ˇhello world\n").await;
1993 cx.simulate_shared_keystrokes("2 r - f w .").await;
1994 cx.shared_state().await.assert_eq("--llo -ˇ-rld\n");
1995
1996 cx.set_shared_state("ˇhello world\n").await;
1997 cx.simulate_shared_keystrokes("2 0 r - ").await;
1998 cx.shared_state().await.assert_eq("ˇhello world\n");
1999
2000 cx.set_shared_state(" helloˇ world\n").await;
2001 cx.simulate_shared_keystrokes("r enter").await;
2002 cx.shared_state().await.assert_eq(" hello\n ˇ world\n");
2003
2004 cx.set_shared_state(" helloˇ world\n").await;
2005 cx.simulate_shared_keystrokes("2 r enter").await;
2006 cx.shared_state().await.assert_eq(" hello\n ˇ orld\n");
2007 }
2008
2009 #[gpui::test]
2010 async fn test_gq(cx: &mut gpui::TestAppContext) {
2011 let mut cx = NeovimBackedTestContext::new(cx).await;
2012 cx.set_neovim_option("textwidth=5").await;
2013
2014 cx.update(|_, cx| {
2015 SettingsStore::update_global(cx, |settings, cx| {
2016 settings.update_user_settings(cx, |settings| {
2017 settings
2018 .project
2019 .all_languages
2020 .defaults
2021 .preferred_line_length = Some(5);
2022 });
2023 })
2024 });
2025
2026 cx.set_shared_state("ˇth th th th th th\n").await;
2027 cx.simulate_shared_keystrokes("g q q").await;
2028 cx.shared_state().await.assert_eq("th th\nth th\nˇth th\n");
2029
2030 cx.set_shared_state("ˇth th th th th th\nth th th th th th\n")
2031 .await;
2032 cx.simulate_shared_keystrokes("v j g q").await;
2033 cx.shared_state()
2034 .await
2035 .assert_eq("th th\nth th\nth th\nth th\nth th\nˇth th\n");
2036 }
2037
2038 #[gpui::test]
2039 async fn test_o_comment(cx: &mut gpui::TestAppContext) {
2040 let mut cx = NeovimBackedTestContext::new(cx).await;
2041 cx.set_neovim_option("filetype=rust").await;
2042
2043 cx.set_shared_state("// helloˇ\n").await;
2044 cx.simulate_shared_keystrokes("o").await;
2045 cx.shared_state().await.assert_eq("// hello\n// ˇ\n");
2046 cx.simulate_shared_keystrokes("x escape shift-o").await;
2047 cx.shared_state().await.assert_eq("// hello\n// ˇ\n// x\n");
2048 }
2049
2050 #[gpui::test]
2051 async fn test_yank_line_with_trailing_newline(cx: &mut gpui::TestAppContext) {
2052 let mut cx = NeovimBackedTestContext::new(cx).await;
2053 cx.set_shared_state("heˇllo\n").await;
2054 cx.simulate_shared_keystrokes("y y p").await;
2055 cx.shared_state().await.assert_eq("hello\nˇhello\n");
2056 }
2057
2058 #[gpui::test]
2059 async fn test_yank_line_without_trailing_newline(cx: &mut gpui::TestAppContext) {
2060 let mut cx = NeovimBackedTestContext::new(cx).await;
2061 cx.set_shared_state("heˇllo").await;
2062 cx.simulate_shared_keystrokes("y y p").await;
2063 cx.shared_state().await.assert_eq("hello\nˇhello");
2064 }
2065
2066 #[gpui::test]
2067 async fn test_yank_multiline_without_trailing_newline(cx: &mut gpui::TestAppContext) {
2068 let mut cx = NeovimBackedTestContext::new(cx).await;
2069 cx.set_shared_state("heˇllo\nhello").await;
2070 cx.simulate_shared_keystrokes("2 y y p").await;
2071 cx.shared_state()
2072 .await
2073 .assert_eq("hello\nˇhello\nhello\nhello");
2074 }
2075
2076 #[gpui::test]
2077 async fn test_dd_then_paste_without_trailing_newline(cx: &mut gpui::TestAppContext) {
2078 let mut cx = NeovimBackedTestContext::new(cx).await;
2079 cx.set_shared_state("heˇllo").await;
2080 cx.simulate_shared_keystrokes("d d").await;
2081 cx.shared_state().await.assert_eq("ˇ");
2082 cx.simulate_shared_keystrokes("p p").await;
2083 cx.shared_state().await.assert_eq("\nhello\nˇhello");
2084 }
2085
2086 #[gpui::test]
2087 async fn test_visual_mode_insert_before_after(cx: &mut gpui::TestAppContext) {
2088 let mut cx = NeovimBackedTestContext::new(cx).await;
2089
2090 cx.set_shared_state("heˇllo").await;
2091 cx.simulate_shared_keystrokes("v i w shift-i").await;
2092 cx.shared_state().await.assert_eq("ˇhello");
2093
2094 cx.set_shared_state(indoc! {"
2095 The quick brown
2096 fox ˇjumps over
2097 the lazy dog"})
2098 .await;
2099 cx.simulate_shared_keystrokes("shift-v shift-i").await;
2100 cx.shared_state().await.assert_eq(indoc! {"
2101 The quick brown
2102 ˇfox jumps over
2103 the lazy dog"});
2104
2105 cx.set_shared_state(indoc! {"
2106 The quick brown
2107 fox ˇjumps over
2108 the lazy dog"})
2109 .await;
2110 cx.simulate_shared_keystrokes("shift-v shift-a").await;
2111 cx.shared_state().await.assert_eq(indoc! {"
2112 The quick brown
2113 fox jˇumps over
2114 the lazy dog"});
2115 }
2116
2117 #[gpui::test]
2118 async fn test_jump_list(cx: &mut gpui::TestAppContext) {
2119 let mut cx = NeovimBackedTestContext::new(cx).await;
2120
2121 cx.set_shared_state(indoc! {"
2122 ˇfn a() { }
2123
2124
2125
2126
2127
2128 fn b() { }
2129
2130
2131
2132
2133
2134 fn b() { }"})
2135 .await;
2136 cx.simulate_shared_keystrokes("3 }").await;
2137 cx.shared_state().await.assert_matches();
2138 cx.simulate_shared_keystrokes("ctrl-o").await;
2139 cx.shared_state().await.assert_matches();
2140 cx.simulate_shared_keystrokes("ctrl-i").await;
2141 cx.shared_state().await.assert_matches();
2142 cx.simulate_shared_keystrokes("1 1 k").await;
2143 cx.shared_state().await.assert_matches();
2144 cx.simulate_shared_keystrokes("ctrl-o").await;
2145 cx.shared_state().await.assert_matches();
2146 }
2147
2148 #[gpui::test]
2149 async fn test_undo_last_line(cx: &mut gpui::TestAppContext) {
2150 let mut cx = NeovimBackedTestContext::new(cx).await;
2151
2152 cx.set_shared_state(indoc! {"
2153 ˇfn a() { }
2154 fn a() { }
2155 fn a() { }
2156 "})
2157 .await;
2158 // do a jump to reset vim's undo grouping
2159 cx.simulate_shared_keystrokes("shift-g").await;
2160 cx.shared_state().await.assert_matches();
2161 cx.simulate_shared_keystrokes("r a").await;
2162 cx.shared_state().await.assert_matches();
2163 cx.simulate_shared_keystrokes("shift-u").await;
2164 cx.shared_state().await.assert_matches();
2165 cx.simulate_shared_keystrokes("shift-u").await;
2166 cx.shared_state().await.assert_matches();
2167 cx.simulate_shared_keystrokes("g g shift-u").await;
2168 cx.shared_state().await.assert_matches();
2169 }
2170
2171 #[gpui::test]
2172 async fn test_undo_last_line_newline(cx: &mut gpui::TestAppContext) {
2173 let mut cx = NeovimBackedTestContext::new(cx).await;
2174
2175 cx.set_shared_state(indoc! {"
2176 ˇfn a() { }
2177 fn a() { }
2178 fn a() { }
2179 "})
2180 .await;
2181 // do a jump to reset vim's undo grouping
2182 cx.simulate_shared_keystrokes("shift-g k").await;
2183 cx.shared_state().await.assert_matches();
2184 cx.simulate_shared_keystrokes("o h e l l o escape").await;
2185 cx.shared_state().await.assert_matches();
2186 cx.simulate_shared_keystrokes("shift-u").await;
2187 cx.shared_state().await.assert_matches();
2188 cx.simulate_shared_keystrokes("shift-u").await;
2189 }
2190
2191 #[gpui::test]
2192 async fn test_undo_last_line_newline_many_changes(cx: &mut gpui::TestAppContext) {
2193 let mut cx = NeovimBackedTestContext::new(cx).await;
2194
2195 cx.set_shared_state(indoc! {"
2196 ˇfn a() { }
2197 fn a() { }
2198 fn a() { }
2199 "})
2200 .await;
2201 // do a jump to reset vim's undo grouping
2202 cx.simulate_shared_keystrokes("x shift-g k").await;
2203 cx.shared_state().await.assert_matches();
2204 cx.simulate_shared_keystrokes("x f a x f { x").await;
2205 cx.shared_state().await.assert_matches();
2206 cx.simulate_shared_keystrokes("shift-u").await;
2207 cx.shared_state().await.assert_matches();
2208 cx.simulate_shared_keystrokes("shift-u").await;
2209 cx.shared_state().await.assert_matches();
2210 cx.simulate_shared_keystrokes("shift-u").await;
2211 cx.shared_state().await.assert_matches();
2212 cx.simulate_shared_keystrokes("shift-u").await;
2213 cx.shared_state().await.assert_matches();
2214 }
2215
2216 #[gpui::test]
2217 async fn test_undo_last_line_multicursor(cx: &mut gpui::TestAppContext) {
2218 let mut cx = VimTestContext::new(cx, true).await;
2219
2220 cx.set_state(
2221 indoc! {"
2222 ˇone two ˇone
2223 two ˇone two
2224 "},
2225 Mode::Normal,
2226 );
2227 cx.simulate_keystrokes("3 r a");
2228 cx.assert_state(
2229 indoc! {"
2230 aaˇa two aaˇa
2231 two aaˇa two
2232 "},
2233 Mode::Normal,
2234 );
2235 cx.simulate_keystrokes("escape escape");
2236 cx.simulate_keystrokes("shift-u");
2237 cx.set_state(
2238 indoc! {"
2239 onˇe two onˇe
2240 two onˇe two
2241 "},
2242 Mode::Normal,
2243 );
2244 }
2245
2246 #[gpui::test]
2247 async fn test_go_to_tab_with_count(cx: &mut gpui::TestAppContext) {
2248 let mut cx = VimTestContext::new(cx, true).await;
2249
2250 // Open 4 tabs.
2251 cx.simulate_keystrokes(": tabnew");
2252 cx.simulate_keystrokes("enter");
2253 cx.simulate_keystrokes(": tabnew");
2254 cx.simulate_keystrokes("enter");
2255 cx.simulate_keystrokes(": tabnew");
2256 cx.simulate_keystrokes("enter");
2257 cx.workspace(|workspace, _, cx| {
2258 assert_eq!(workspace.items(cx).count(), 4);
2259 assert_eq!(workspace.active_pane().read(cx).active_item_index(), 3);
2260 });
2261
2262 cx.simulate_keystrokes("1 g t");
2263 cx.workspace(|workspace, _, cx| {
2264 assert_eq!(workspace.active_pane().read(cx).active_item_index(), 0);
2265 });
2266
2267 cx.simulate_keystrokes("3 g t");
2268 cx.workspace(|workspace, _, cx| {
2269 assert_eq!(workspace.active_pane().read(cx).active_item_index(), 2);
2270 });
2271
2272 cx.simulate_keystrokes("4 g t");
2273 cx.workspace(|workspace, _, cx| {
2274 assert_eq!(workspace.active_pane().read(cx).active_item_index(), 3);
2275 });
2276
2277 cx.simulate_keystrokes("1 g t");
2278 cx.simulate_keystrokes("g t");
2279 cx.workspace(|workspace, _, cx| {
2280 assert_eq!(workspace.active_pane().read(cx).active_item_index(), 1);
2281 });
2282 }
2283
2284 #[gpui::test]
2285 async fn test_go_to_previous_tab_with_count(cx: &mut gpui::TestAppContext) {
2286 let mut cx = VimTestContext::new(cx, true).await;
2287
2288 // Open 4 tabs.
2289 cx.simulate_keystrokes(": tabnew");
2290 cx.simulate_keystrokes("enter");
2291 cx.simulate_keystrokes(": tabnew");
2292 cx.simulate_keystrokes("enter");
2293 cx.simulate_keystrokes(": tabnew");
2294 cx.simulate_keystrokes("enter");
2295 cx.workspace(|workspace, _, cx| {
2296 assert_eq!(workspace.items(cx).count(), 4);
2297 assert_eq!(workspace.active_pane().read(cx).active_item_index(), 3);
2298 });
2299
2300 cx.simulate_keystrokes("2 g shift-t");
2301 cx.workspace(|workspace, _, cx| {
2302 assert_eq!(workspace.active_pane().read(cx).active_item_index(), 1);
2303 });
2304
2305 cx.simulate_keystrokes("g shift-t");
2306 cx.workspace(|workspace, _, cx| {
2307 assert_eq!(workspace.active_pane().read(cx).active_item_index(), 0);
2308 });
2309
2310 // Wraparound: gT from first tab should go to last.
2311 cx.simulate_keystrokes("g shift-t");
2312 cx.workspace(|workspace, _, cx| {
2313 assert_eq!(workspace.active_pane().read(cx).active_item_index(), 3);
2314 });
2315
2316 cx.simulate_keystrokes("6 g shift-t");
2317 cx.workspace(|workspace, _, cx| {
2318 assert_eq!(workspace.active_pane().read(cx).active_item_index(), 1);
2319 });
2320 }
2321
2322 #[gpui::test]
2323 async fn test_temporary_mode(cx: &mut gpui::TestAppContext) {
2324 let mut cx = NeovimBackedTestContext::new(cx).await;
2325
2326 // Test jumping to the end of the line ($).
2327 cx.set_shared_state(indoc! {"lorem ˇipsum"}).await;
2328 cx.simulate_shared_keystrokes("i").await;
2329 cx.shared_state().await.assert_matches();
2330 cx.simulate_shared_keystrokes("ctrl-o $").await;
2331 cx.shared_state().await.assert_eq(indoc! {"lorem ipsumˇ"});
2332
2333 // Test jumping to the next word.
2334 cx.set_shared_state(indoc! {"loremˇ ipsum dolor"}).await;
2335 cx.simulate_shared_keystrokes("a").await;
2336 cx.shared_state().await.assert_matches();
2337 cx.simulate_shared_keystrokes("a n d space ctrl-o w").await;
2338 cx.shared_state()
2339 .await
2340 .assert_eq(indoc! {"lorem and ipsum ˇdolor"});
2341
2342 // Test yanking to end of line ($).
2343 cx.set_shared_state(indoc! {"lorem ˇipsum dolor"}).await;
2344 cx.simulate_shared_keystrokes("i").await;
2345 cx.shared_state().await.assert_matches();
2346 cx.simulate_shared_keystrokes("a n d space ctrl-o y $")
2347 .await;
2348 cx.shared_state()
2349 .await
2350 .assert_eq(indoc! {"lorem and ˇipsum dolor"});
2351 }
2352}