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_with(&mut |map, selection| {
735 let previous_line = map.start_of_relative_buffer_row(selection.start, -1);
736 let insert_point = motion::end_of_line(map, false, previous_line, 1);
737 selection.collapse_to(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 editor.transact(window, cx, |editor, window, cx| {
754 let selections = editor.selections.all::<Point>(&editor.display_snapshot(cx));
755 let snapshot = editor.buffer().read(cx).snapshot(cx);
756
757 let selection_end_rows: BTreeSet<u32> = selections
758 .into_iter()
759 .map(|selection| {
760 if !selection.is_empty() && selection.end.column == 0 {
761 selection.end.row.saturating_sub(1)
762 } else {
763 selection.end.row
764 }
765 })
766 .collect();
767 let edits = selection_end_rows
768 .into_iter()
769 .map(|row| {
770 let indent = snapshot
771 .indent_and_comment_for_line(MultiBufferRow(row), cx)
772 .chars()
773 .collect::<String>();
774
775 let end_of_line = Point::new(row, snapshot.line_len(MultiBufferRow(row)));
776 (end_of_line..end_of_line, "\n".to_string() + &indent)
777 })
778 .collect::<Vec<_>>();
779 editor.change_selections(Default::default(), window, cx, |s| {
780 s.move_with(&mut |map, selection| {
781 let current_line = if !selection.is_empty() && selection.end.column() == 0 {
782 // If this is an insert after a selection to the end of the line, the
783 // cursor needs to be bumped back, because it'll be at the start of the
784 // *next* line.
785 map.start_of_relative_buffer_row(selection.end, -1)
786 } else {
787 selection.end
788 };
789 let insert_point = motion::end_of_line(map, false, current_line, 1);
790 selection.collapse_to(insert_point, SelectionGoal::None)
791 });
792 });
793 editor.edit_with_autoindent(edits, cx);
794 });
795 });
796 }
797
798 fn insert_empty_line_above(
799 &mut self,
800 _: &InsertEmptyLineAbove,
801 window: &mut Window,
802 cx: &mut Context<Self>,
803 ) {
804 self.record_current_action(cx);
805 let count = Vim::take_count(cx).unwrap_or(1);
806 Vim::take_forced_motion(cx);
807 self.update_editor(cx, |_, editor, cx| {
808 editor.transact(window, cx, |editor, _, cx| {
809 let selections = editor.selections.all::<Point>(&editor.display_snapshot(cx));
810
811 let selection_start_rows: BTreeSet<u32> = selections
812 .into_iter()
813 .map(|selection| selection.start.row)
814 .collect();
815 let edits = selection_start_rows
816 .into_iter()
817 .map(|row| {
818 let start_of_line = Point::new(row, 0);
819 (start_of_line..start_of_line, "\n".repeat(count))
820 })
821 .collect::<Vec<_>>();
822 editor.edit(edits, cx);
823 });
824 });
825 }
826
827 fn insert_empty_line_below(
828 &mut self,
829 _: &InsertEmptyLineBelow,
830 window: &mut Window,
831 cx: &mut Context<Self>,
832 ) {
833 self.record_current_action(cx);
834 let count = Vim::take_count(cx).unwrap_or(1);
835 Vim::take_forced_motion(cx);
836 self.update_editor(cx, |_, editor, cx| {
837 editor.transact(window, cx, |editor, window, cx| {
838 let display_map = editor.display_snapshot(cx);
839 let selections = editor.selections.all::<Point>(&display_map);
840 let snapshot = editor.buffer().read(cx).snapshot(cx);
841 let display_selections = editor.selections.all_display(&display_map);
842 let original_positions = display_selections
843 .iter()
844 .map(|s| (s.id, s.head()))
845 .collect::<HashMap<_, _>>();
846
847 let selection_end_rows: BTreeSet<u32> = selections
848 .into_iter()
849 .map(|selection| selection.end.row)
850 .collect();
851 let edits = selection_end_rows
852 .into_iter()
853 .map(|row| {
854 let end_of_line = Point::new(row, snapshot.line_len(MultiBufferRow(row)));
855 (end_of_line..end_of_line, "\n".repeat(count))
856 })
857 .collect::<Vec<_>>();
858 editor.edit(edits, cx);
859
860 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
861 s.move_with(&mut |_, selection| {
862 if let Some(position) = original_positions.get(&selection.id) {
863 selection.collapse_to(*position, SelectionGoal::None);
864 }
865 });
866 });
867 });
868 });
869 }
870
871 fn join_lines_impl(
872 &mut self,
873 insert_whitespace: bool,
874 window: &mut Window,
875 cx: &mut Context<Self>,
876 ) {
877 self.record_current_action(cx);
878 let mut times = Vim::take_count(cx).unwrap_or(1);
879 Vim::take_forced_motion(cx);
880 if self.mode.is_visual() {
881 times = 1;
882 } else if times > 1 {
883 // 2J joins two lines together (same as J or 1J)
884 times -= 1;
885 }
886
887 self.update_editor(cx, |_, editor, cx| {
888 editor.transact(window, cx, |editor, window, cx| {
889 for _ in 0..times {
890 editor.join_lines_impl(insert_whitespace, window, cx)
891 }
892 })
893 });
894 if self.mode.is_visual() {
895 self.switch_mode(Mode::Normal, true, window, cx)
896 }
897 }
898
899 fn yank_line(&mut self, _: &YankLine, window: &mut Window, cx: &mut Context<Self>) {
900 let count = Vim::take_count(cx);
901 let forced_motion = Vim::take_forced_motion(cx);
902 self.yank_motion(
903 motion::Motion::CurrentLine,
904 count,
905 forced_motion,
906 window,
907 cx,
908 )
909 }
910
911 fn yank_to_end_of_line(
912 &mut self,
913 _: &YankToEndOfLine,
914 window: &mut Window,
915 cx: &mut Context<Self>,
916 ) {
917 let count = Vim::take_count(cx);
918 let forced_motion = Vim::take_forced_motion(cx);
919 self.yank_motion(
920 motion::Motion::EndOfLine {
921 display_lines: false,
922 },
923 count,
924 forced_motion,
925 window,
926 cx,
927 )
928 }
929
930 fn show_location(&mut self, _: &ShowLocation, _: &mut Window, cx: &mut Context<Self>) {
931 let count = Vim::take_count(cx);
932 Vim::take_forced_motion(cx);
933 self.update_editor(cx, |vim, editor, cx| {
934 let selection = editor.selections.newest_anchor();
935 let Some((buffer, point)) = editor
936 .buffer()
937 .read(cx)
938 .point_to_buffer_point(selection.head(), cx)
939 else {
940 return;
941 };
942 let filename = if let Some(file) = buffer.read(cx).file() {
943 if count.is_some() {
944 if let Some(local) = file.as_local() {
945 local.abs_path(cx).to_string_lossy().into_owned()
946 } else {
947 file.full_path(cx).to_string_lossy().into_owned()
948 }
949 } else {
950 file.path().display(file.path_style(cx)).into_owned()
951 }
952 } else {
953 "[No Name]".into()
954 };
955 let buffer = buffer.read(cx);
956 let lines = buffer.max_point().row + 1;
957 let current_line = point.row;
958 let percentage = current_line as f32 / lines as f32;
959 let modified = if buffer.is_dirty() { " [modified]" } else { "" };
960 vim.set_status_label(
961 format!(
962 "{}{} {} lines --{:.0}%--",
963 filename,
964 modified,
965 lines,
966 percentage * 100.0,
967 ),
968 cx,
969 );
970 });
971 }
972
973 fn toggle_comments(&mut self, _: &ToggleComments, window: &mut Window, cx: &mut Context<Self>) {
974 self.record_current_action(cx);
975 self.store_visual_marks(window, cx);
976 self.update_editor(cx, |vim, editor, cx| {
977 editor.transact(window, cx, |editor, window, cx| {
978 let original_positions = vim.save_selection_starts(editor, cx);
979 editor.toggle_comments(&Default::default(), window, cx);
980 vim.restore_selection_cursors(editor, window, cx, original_positions);
981 });
982 });
983 if self.mode.is_visual() {
984 self.switch_mode(Mode::Normal, true, window, cx)
985 }
986 }
987
988 pub(crate) fn normal_replace(
989 &mut self,
990 text: Arc<str>,
991 window: &mut Window,
992 cx: &mut Context<Self>,
993 ) {
994 // We need to use `text.chars().count()` instead of `text.len()` here as
995 // `len()` counts bytes, not characters.
996 let char_count = text.chars().count();
997 let count = Vim::take_count(cx).unwrap_or(char_count);
998 let is_return_char = text == "\n".into() || text == "\r".into();
999 let repeat_count = match (is_return_char, char_count) {
1000 (true, _) => 0,
1001 (_, 1) => count,
1002 (_, _) => 1,
1003 };
1004
1005 Vim::take_forced_motion(cx);
1006 self.stop_recording(cx);
1007 self.update_editor(cx, |_, editor, cx| {
1008 editor.transact(window, cx, |editor, window, cx| {
1009 editor.set_clip_at_line_ends(false, cx);
1010 let display_map = editor.display_snapshot(cx);
1011 let display_selections = editor.selections.all_display(&display_map);
1012
1013 let mut edits = Vec::with_capacity(display_selections.len());
1014 for selection in &display_selections {
1015 let mut range = selection.range();
1016 for _ in 0..count {
1017 let new_point = movement::saturating_right(&display_map, range.end);
1018 if range.end == new_point {
1019 return;
1020 }
1021 range.end = new_point;
1022 }
1023
1024 edits.push((
1025 range.start.to_offset(&display_map, Bias::Left)
1026 ..range.end.to_offset(&display_map, Bias::Left),
1027 text.repeat(repeat_count),
1028 ));
1029 }
1030
1031 editor.edit(edits, cx);
1032 if is_return_char {
1033 editor.newline(&editor::actions::Newline, window, cx);
1034 }
1035 editor.set_clip_at_line_ends(true, cx);
1036 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1037 s.move_with(&mut |map, selection| {
1038 let point = movement::saturating_left(map, selection.head());
1039 selection.collapse_to(point, SelectionGoal::None)
1040 });
1041 });
1042 });
1043 });
1044 self.pop_operator(window, cx);
1045 }
1046
1047 pub fn save_selection_starts(
1048 &self,
1049 editor: &Editor,
1050 cx: &mut Context<Editor>,
1051 ) -> HashMap<usize, Anchor> {
1052 let display_map = editor.display_snapshot(cx);
1053 let selections = editor.selections.all_display(&display_map);
1054 selections
1055 .iter()
1056 .map(|selection| {
1057 (
1058 selection.id,
1059 display_map.display_point_to_anchor(selection.start, Bias::Right),
1060 )
1061 })
1062 .collect::<HashMap<_, _>>()
1063 }
1064
1065 pub fn restore_selection_cursors(
1066 &self,
1067 editor: &mut Editor,
1068 window: &mut Window,
1069 cx: &mut Context<Editor>,
1070 mut positions: HashMap<usize, Anchor>,
1071 ) {
1072 editor.change_selections(Default::default(), window, cx, |s| {
1073 s.move_with(&mut |map, selection| {
1074 if let Some(anchor) = positions.remove(&selection.id) {
1075 selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None);
1076 }
1077 });
1078 });
1079 }
1080
1081 fn exit_temporary_normal(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1082 if self.temp_mode {
1083 self.switch_mode(Mode::Insert, true, window, cx);
1084 }
1085 }
1086}
1087
1088#[cfg(test)]
1089mod test {
1090 use gpui::{KeyBinding, TestAppContext, UpdateGlobal};
1091 use indoc::indoc;
1092 use settings::SettingsStore;
1093
1094 use crate::{
1095 motion,
1096 state::Mode::{self},
1097 test::{NeovimBackedTestContext, VimTestContext},
1098 };
1099
1100 #[gpui::test]
1101 async fn test_h(cx: &mut gpui::TestAppContext) {
1102 let mut cx = NeovimBackedTestContext::new(cx).await;
1103 cx.simulate_at_each_offset(
1104 "h",
1105 indoc! {"
1106 ˇThe qˇuick
1107 ˇbrown"
1108 },
1109 )
1110 .await
1111 .assert_matches();
1112 }
1113
1114 #[gpui::test]
1115 async fn test_backspace(cx: &mut gpui::TestAppContext) {
1116 let mut cx = NeovimBackedTestContext::new(cx).await;
1117 cx.simulate_at_each_offset(
1118 "backspace",
1119 indoc! {"
1120 ˇThe qˇuick
1121 ˇbrown"
1122 },
1123 )
1124 .await
1125 .assert_matches();
1126 }
1127
1128 #[gpui::test]
1129 async fn test_j(cx: &mut gpui::TestAppContext) {
1130 let mut cx = NeovimBackedTestContext::new(cx).await;
1131
1132 cx.set_shared_state(indoc! {"
1133 aaˇaa
1134 😃😃"
1135 })
1136 .await;
1137 cx.simulate_shared_keystrokes("j").await;
1138 cx.shared_state().await.assert_eq(indoc! {"
1139 aaaa
1140 😃ˇ😃"
1141 });
1142
1143 cx.simulate_at_each_offset(
1144 "j",
1145 indoc! {"
1146 ˇThe qˇuick broˇwn
1147 ˇfox jumps"
1148 },
1149 )
1150 .await
1151 .assert_matches();
1152 }
1153
1154 #[gpui::test]
1155 async fn test_enter(cx: &mut gpui::TestAppContext) {
1156 let mut cx = NeovimBackedTestContext::new(cx).await;
1157 cx.simulate_at_each_offset(
1158 "enter",
1159 indoc! {"
1160 ˇThe qˇuick broˇwn
1161 ˇfox jumps"
1162 },
1163 )
1164 .await
1165 .assert_matches();
1166 }
1167
1168 #[gpui::test]
1169 async fn test_k(cx: &mut gpui::TestAppContext) {
1170 let mut cx = NeovimBackedTestContext::new(cx).await;
1171 cx.simulate_at_each_offset(
1172 "k",
1173 indoc! {"
1174 ˇThe qˇuick
1175 ˇbrown fˇox jumˇps"
1176 },
1177 )
1178 .await
1179 .assert_matches();
1180 }
1181
1182 #[gpui::test]
1183 async fn test_l(cx: &mut gpui::TestAppContext) {
1184 let mut cx = NeovimBackedTestContext::new(cx).await;
1185 cx.simulate_at_each_offset(
1186 "l",
1187 indoc! {"
1188 ˇThe qˇuicˇk
1189 ˇbrowˇn"},
1190 )
1191 .await
1192 .assert_matches();
1193 }
1194
1195 #[gpui::test]
1196 async fn test_jump_to_line_boundaries(cx: &mut gpui::TestAppContext) {
1197 let mut cx = NeovimBackedTestContext::new(cx).await;
1198 cx.simulate_at_each_offset(
1199 "$",
1200 indoc! {"
1201 ˇThe qˇuicˇk
1202 ˇbrowˇn"},
1203 )
1204 .await
1205 .assert_matches();
1206 cx.simulate_at_each_offset(
1207 "0",
1208 indoc! {"
1209 ˇThe qˇuicˇk
1210 ˇbrowˇn"},
1211 )
1212 .await
1213 .assert_matches();
1214 }
1215
1216 #[gpui::test]
1217 async fn test_jump_to_end(cx: &mut gpui::TestAppContext) {
1218 let mut cx = NeovimBackedTestContext::new(cx).await;
1219
1220 cx.simulate_at_each_offset(
1221 "shift-g",
1222 indoc! {"
1223 The ˇquick
1224
1225 brown fox jumps
1226 overˇ the lazy doˇg"},
1227 )
1228 .await
1229 .assert_matches();
1230 cx.simulate(
1231 "shift-g",
1232 indoc! {"
1233 The quiˇck
1234
1235 brown"},
1236 )
1237 .await
1238 .assert_matches();
1239 cx.simulate(
1240 "shift-g",
1241 indoc! {"
1242 The quiˇck
1243
1244 "},
1245 )
1246 .await
1247 .assert_matches();
1248 }
1249
1250 #[gpui::test]
1251 async fn test_w(cx: &mut gpui::TestAppContext) {
1252 let mut cx = NeovimBackedTestContext::new(cx).await;
1253 cx.simulate_at_each_offset(
1254 "w",
1255 indoc! {"
1256 The ˇquickˇ-ˇbrown
1257 ˇ
1258 ˇ
1259 ˇfox_jumps ˇover
1260 ˇthˇe"},
1261 )
1262 .await
1263 .assert_matches();
1264 cx.simulate_at_each_offset(
1265 "shift-w",
1266 indoc! {"
1267 The ˇquickˇ-ˇbrown
1268 ˇ
1269 ˇ
1270 ˇfox_jumps ˇover
1271 ˇthˇe"},
1272 )
1273 .await
1274 .assert_matches();
1275 }
1276
1277 #[gpui::test]
1278 async fn test_end_of_word(cx: &mut gpui::TestAppContext) {
1279 let mut cx = NeovimBackedTestContext::new(cx).await;
1280 cx.simulate_at_each_offset(
1281 "e",
1282 indoc! {"
1283 Thˇe quicˇkˇ-browˇn
1284
1285
1286 fox_jumpˇs oveˇr
1287 thˇe"},
1288 )
1289 .await
1290 .assert_matches();
1291 cx.simulate_at_each_offset(
1292 "shift-e",
1293 indoc! {"
1294 Thˇe quicˇkˇ-browˇn
1295
1296
1297 fox_jumpˇs oveˇr
1298 thˇe"},
1299 )
1300 .await
1301 .assert_matches();
1302 }
1303
1304 #[gpui::test]
1305 async fn test_b(cx: &mut gpui::TestAppContext) {
1306 let mut cx = NeovimBackedTestContext::new(cx).await;
1307 cx.simulate_at_each_offset(
1308 "b",
1309 indoc! {"
1310 ˇThe ˇquickˇ-ˇbrown
1311 ˇ
1312 ˇ
1313 ˇfox_jumps ˇover
1314 ˇthe"},
1315 )
1316 .await
1317 .assert_matches();
1318 cx.simulate_at_each_offset(
1319 "shift-b",
1320 indoc! {"
1321 ˇThe ˇquickˇ-ˇbrown
1322 ˇ
1323 ˇ
1324 ˇfox_jumps ˇover
1325 ˇthe"},
1326 )
1327 .await
1328 .assert_matches();
1329 }
1330
1331 #[gpui::test]
1332 async fn test_gg(cx: &mut gpui::TestAppContext) {
1333 let mut cx = NeovimBackedTestContext::new(cx).await;
1334 cx.simulate_at_each_offset(
1335 "g g",
1336 indoc! {"
1337 The qˇuick
1338
1339 brown fox jumps
1340 over ˇthe laˇzy dog"},
1341 )
1342 .await
1343 .assert_matches();
1344 cx.simulate(
1345 "g g",
1346 indoc! {"
1347
1348
1349 brown fox jumps
1350 over the laˇzy dog"},
1351 )
1352 .await
1353 .assert_matches();
1354 cx.simulate(
1355 "2 g g",
1356 indoc! {"
1357 ˇ
1358
1359 brown fox jumps
1360 over the lazydog"},
1361 )
1362 .await
1363 .assert_matches();
1364 }
1365
1366 #[gpui::test]
1367 async fn test_end_of_document(cx: &mut gpui::TestAppContext) {
1368 let mut cx = NeovimBackedTestContext::new(cx).await;
1369 cx.simulate_at_each_offset(
1370 "shift-g",
1371 indoc! {"
1372 The qˇuick
1373
1374 brown fox jumps
1375 over ˇthe laˇzy dog"},
1376 )
1377 .await
1378 .assert_matches();
1379 cx.simulate(
1380 "shift-g",
1381 indoc! {"
1382
1383
1384 brown fox jumps
1385 over the laˇzy dog"},
1386 )
1387 .await
1388 .assert_matches();
1389 cx.simulate(
1390 "2 shift-g",
1391 indoc! {"
1392 ˇ
1393
1394 brown fox jumps
1395 over the lazydog"},
1396 )
1397 .await
1398 .assert_matches();
1399 }
1400
1401 #[gpui::test]
1402 async fn test_a(cx: &mut gpui::TestAppContext) {
1403 let mut cx = NeovimBackedTestContext::new(cx).await;
1404 cx.simulate_at_each_offset("a", "The qˇuicˇk")
1405 .await
1406 .assert_matches();
1407 }
1408
1409 #[gpui::test]
1410 async fn test_insert_end_of_line(cx: &mut gpui::TestAppContext) {
1411 let mut cx = NeovimBackedTestContext::new(cx).await;
1412 cx.simulate_at_each_offset(
1413 "shift-a",
1414 indoc! {"
1415 ˇ
1416 The qˇuick
1417 brown ˇfox "},
1418 )
1419 .await
1420 .assert_matches();
1421 }
1422
1423 #[gpui::test]
1424 async fn test_jump_to_first_non_whitespace(cx: &mut gpui::TestAppContext) {
1425 let mut cx = NeovimBackedTestContext::new(cx).await;
1426 cx.simulate("^", "The qˇuick").await.assert_matches();
1427 cx.simulate("^", " The qˇuick").await.assert_matches();
1428 cx.simulate("^", "ˇ").await.assert_matches();
1429 cx.simulate(
1430 "^",
1431 indoc! {"
1432 The qˇuick
1433 brown fox"},
1434 )
1435 .await
1436 .assert_matches();
1437 cx.simulate(
1438 "^",
1439 indoc! {"
1440 ˇ
1441 The quick"},
1442 )
1443 .await
1444 .assert_matches();
1445 // Indoc disallows trailing whitespace.
1446 cx.simulate("^", " ˇ \nThe quick").await.assert_matches();
1447 }
1448
1449 #[gpui::test]
1450 async fn test_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
1451 let mut cx = NeovimBackedTestContext::new(cx).await;
1452 cx.simulate("shift-i", "The qˇuick").await.assert_matches();
1453 cx.simulate("shift-i", " The qˇuick").await.assert_matches();
1454 cx.simulate("shift-i", "ˇ").await.assert_matches();
1455 cx.simulate(
1456 "shift-i",
1457 indoc! {"
1458 The qˇuick
1459 brown fox"},
1460 )
1461 .await
1462 .assert_matches();
1463 cx.simulate(
1464 "shift-i",
1465 indoc! {"
1466 ˇ
1467 The quick"},
1468 )
1469 .await
1470 .assert_matches();
1471 }
1472
1473 #[gpui::test]
1474 async fn test_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
1475 let mut cx = NeovimBackedTestContext::new(cx).await;
1476 cx.simulate(
1477 "shift-d",
1478 indoc! {"
1479 The qˇuick
1480 brown fox"},
1481 )
1482 .await
1483 .assert_matches();
1484 cx.simulate(
1485 "shift-d",
1486 indoc! {"
1487 The quick
1488 ˇ
1489 brown fox"},
1490 )
1491 .await
1492 .assert_matches();
1493 }
1494
1495 #[gpui::test]
1496 async fn test_x(cx: &mut gpui::TestAppContext) {
1497 let mut cx = NeovimBackedTestContext::new(cx).await;
1498 cx.simulate_at_each_offset("x", "ˇTeˇsˇt")
1499 .await
1500 .assert_matches();
1501 cx.simulate(
1502 "x",
1503 indoc! {"
1504 Tesˇt
1505 test"},
1506 )
1507 .await
1508 .assert_matches();
1509 }
1510
1511 #[gpui::test]
1512 async fn test_delete_left(cx: &mut gpui::TestAppContext) {
1513 let mut cx = NeovimBackedTestContext::new(cx).await;
1514 cx.simulate_at_each_offset("shift-x", "ˇTˇeˇsˇt")
1515 .await
1516 .assert_matches();
1517 cx.simulate(
1518 "shift-x",
1519 indoc! {"
1520 Test
1521 ˇtest"},
1522 )
1523 .await
1524 .assert_matches();
1525 }
1526
1527 #[gpui::test]
1528 async fn test_o(cx: &mut gpui::TestAppContext) {
1529 let mut cx = NeovimBackedTestContext::new(cx).await;
1530 cx.simulate("o", "ˇ").await.assert_matches();
1531 cx.simulate("o", "The ˇquick").await.assert_matches();
1532 cx.simulate_at_each_offset(
1533 "o",
1534 indoc! {"
1535 The qˇuick
1536 brown ˇfox
1537 jumps ˇover"},
1538 )
1539 .await
1540 .assert_matches();
1541 cx.simulate(
1542 "o",
1543 indoc! {"
1544 The quick
1545 ˇ
1546 brown fox"},
1547 )
1548 .await
1549 .assert_matches();
1550
1551 cx.assert_binding(
1552 "o",
1553 indoc! {"
1554 fn test() {
1555 println!(ˇ);
1556 }"},
1557 Mode::Normal,
1558 indoc! {"
1559 fn test() {
1560 println!();
1561 ˇ
1562 }"},
1563 Mode::Insert,
1564 );
1565
1566 cx.assert_binding(
1567 "o",
1568 indoc! {"
1569 fn test(ˇ) {
1570 println!();
1571 }"},
1572 Mode::Normal,
1573 indoc! {"
1574 fn test() {
1575 ˇ
1576 println!();
1577 }"},
1578 Mode::Insert,
1579 );
1580 }
1581
1582 #[gpui::test]
1583 async fn test_insert_line_above(cx: &mut gpui::TestAppContext) {
1584 let mut cx = NeovimBackedTestContext::new(cx).await;
1585 cx.simulate("shift-o", "ˇ").await.assert_matches();
1586 cx.simulate("shift-o", "The ˇquick").await.assert_matches();
1587 cx.simulate_at_each_offset(
1588 "shift-o",
1589 indoc! {"
1590 The qˇuick
1591 brown ˇfox
1592 jumps ˇover"},
1593 )
1594 .await
1595 .assert_matches();
1596 cx.simulate(
1597 "shift-o",
1598 indoc! {"
1599 The quick
1600 ˇ
1601 brown fox"},
1602 )
1603 .await
1604 .assert_matches();
1605
1606 // Our indentation is smarter than vims. So we don't match here
1607 cx.assert_binding(
1608 "shift-o",
1609 indoc! {"
1610 fn test() {
1611 println!(ˇ);
1612 }"},
1613 Mode::Normal,
1614 indoc! {"
1615 fn test() {
1616 ˇ
1617 println!();
1618 }"},
1619 Mode::Insert,
1620 );
1621 cx.assert_binding(
1622 "shift-o",
1623 indoc! {"
1624 fn test(ˇ) {
1625 println!();
1626 }"},
1627 Mode::Normal,
1628 indoc! {"
1629 ˇ
1630 fn test() {
1631 println!();
1632 }"},
1633 Mode::Insert,
1634 );
1635 }
1636
1637 #[gpui::test]
1638 async fn test_insert_empty_line(cx: &mut gpui::TestAppContext) {
1639 let mut cx = NeovimBackedTestContext::new(cx).await;
1640 cx.simulate("[ space", "ˇ").await.assert_matches();
1641 cx.simulate("[ space", "The ˇquick").await.assert_matches();
1642 cx.simulate_at_each_offset(
1643 "3 [ space",
1644 indoc! {"
1645 The qˇuick
1646 brown ˇfox
1647 jumps ˇover"},
1648 )
1649 .await
1650 .assert_matches();
1651 cx.simulate_at_each_offset(
1652 "[ space",
1653 indoc! {"
1654 The qˇuick
1655 brown ˇfox
1656 jumps ˇover"},
1657 )
1658 .await
1659 .assert_matches();
1660 cx.simulate(
1661 "[ space",
1662 indoc! {"
1663 The quick
1664 ˇ
1665 brown fox"},
1666 )
1667 .await
1668 .assert_matches();
1669
1670 cx.simulate("] space", "ˇ").await.assert_matches();
1671 cx.simulate("] space", "The ˇquick").await.assert_matches();
1672 cx.simulate_at_each_offset(
1673 "3 ] space",
1674 indoc! {"
1675 The qˇuick
1676 brown ˇfox
1677 jumps ˇover"},
1678 )
1679 .await
1680 .assert_matches();
1681 cx.simulate_at_each_offset(
1682 "] space",
1683 indoc! {"
1684 The qˇuick
1685 brown ˇfox
1686 jumps ˇover"},
1687 )
1688 .await
1689 .assert_matches();
1690 cx.simulate(
1691 "] space",
1692 indoc! {"
1693 The quick
1694 ˇ
1695 brown fox"},
1696 )
1697 .await
1698 .assert_matches();
1699 }
1700
1701 #[gpui::test]
1702 async fn test_dd(cx: &mut gpui::TestAppContext) {
1703 let mut cx = NeovimBackedTestContext::new(cx).await;
1704 cx.simulate("d d", "ˇ").await.assert_matches();
1705 cx.simulate("d d", "The ˇquick").await.assert_matches();
1706 cx.simulate_at_each_offset(
1707 "d d",
1708 indoc! {"
1709 The qˇuick
1710 brown ˇfox
1711 jumps ˇover"},
1712 )
1713 .await
1714 .assert_matches();
1715 cx.simulate(
1716 "d d",
1717 indoc! {"
1718 The quick
1719 ˇ
1720 brown fox"},
1721 )
1722 .await
1723 .assert_matches();
1724 }
1725
1726 #[gpui::test]
1727 async fn test_cc(cx: &mut gpui::TestAppContext) {
1728 let mut cx = NeovimBackedTestContext::new(cx).await;
1729 cx.simulate("c c", "ˇ").await.assert_matches();
1730 cx.simulate("c c", "The ˇquick").await.assert_matches();
1731 cx.simulate_at_each_offset(
1732 "c c",
1733 indoc! {"
1734 The quˇick
1735 brown ˇfox
1736 jumps ˇover"},
1737 )
1738 .await
1739 .assert_matches();
1740 cx.simulate(
1741 "c c",
1742 indoc! {"
1743 The quick
1744 ˇ
1745 brown fox"},
1746 )
1747 .await
1748 .assert_matches();
1749 }
1750
1751 #[gpui::test]
1752 async fn test_repeated_word(cx: &mut gpui::TestAppContext) {
1753 let mut cx = NeovimBackedTestContext::new(cx).await;
1754
1755 for count in 1..=5 {
1756 cx.simulate_at_each_offset(
1757 &format!("{count} w"),
1758 indoc! {"
1759 ˇThe quˇickˇ browˇn
1760 ˇ
1761 ˇfox ˇjumpsˇ-ˇoˇver
1762 ˇthe lazy dog
1763 "},
1764 )
1765 .await
1766 .assert_matches();
1767 }
1768 }
1769
1770 #[gpui::test]
1771 async fn test_h_through_unicode(cx: &mut gpui::TestAppContext) {
1772 let mut cx = NeovimBackedTestContext::new(cx).await;
1773 cx.simulate_at_each_offset("h", "Testˇ├ˇ──ˇ┐ˇTest")
1774 .await
1775 .assert_matches();
1776 }
1777
1778 #[gpui::test]
1779 async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
1780 let mut cx = NeovimBackedTestContext::new(cx).await;
1781
1782 for count in 1..=3 {
1783 let test_case = indoc! {"
1784 ˇaaaˇbˇ ˇbˇ ˇbˇbˇ aˇaaˇbaaa
1785 ˇ ˇbˇaaˇa ˇbˇbˇb
1786 ˇ
1787 ˇb
1788 "};
1789
1790 cx.simulate_at_each_offset(&format!("{count} f b"), test_case)
1791 .await
1792 .assert_matches();
1793
1794 cx.simulate_at_each_offset(&format!("{count} t b"), test_case)
1795 .await
1796 .assert_matches();
1797 }
1798 }
1799
1800 #[gpui::test]
1801 async fn test_capital_f_and_capital_t(cx: &mut gpui::TestAppContext) {
1802 let mut cx = NeovimBackedTestContext::new(cx).await;
1803 let test_case = indoc! {"
1804 ˇaaaˇbˇ ˇbˇ ˇbˇbˇ aˇaaˇbaaa
1805 ˇ ˇbˇaaˇa ˇbˇbˇb
1806 ˇ•••
1807 ˇb
1808 "
1809 };
1810
1811 for count in 1..=3 {
1812 cx.simulate_at_each_offset(&format!("{count} shift-f b"), test_case)
1813 .await
1814 .assert_matches();
1815
1816 cx.simulate_at_each_offset(&format!("{count} shift-t b"), test_case)
1817 .await
1818 .assert_matches();
1819 }
1820 }
1821
1822 #[gpui::test]
1823 async fn test_f_and_t_smartcase(cx: &mut gpui::TestAppContext) {
1824 let mut cx = VimTestContext::new(cx, true).await;
1825 cx.update_global(|store: &mut SettingsStore, cx| {
1826 store.update_user_settings(cx, |s| {
1827 s.vim.get_or_insert_default().use_smartcase_find = Some(true);
1828 });
1829 });
1830
1831 cx.assert_binding(
1832 "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 "shift-f 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 "t p",
1849 indoc! {"ˇfmt.Println(\"Hello, World!\")"},
1850 Mode::Normal,
1851 indoc! {"fmtˇ.Println(\"Hello, World!\")"},
1852 Mode::Normal,
1853 );
1854
1855 cx.assert_binding(
1856 "shift-t p",
1857 indoc! {"fmt.Printlnˇ(\"Hello, World!\")"},
1858 Mode::Normal,
1859 indoc! {"fmt.Pˇrintln(\"Hello, World!\")"},
1860 Mode::Normal,
1861 );
1862 }
1863
1864 #[gpui::test]
1865 async fn test_percent(cx: &mut TestAppContext) {
1866 let mut cx = NeovimBackedTestContext::new(cx).await;
1867 cx.simulate_at_each_offset("%", "ˇconsole.logˇ(ˇvaˇrˇ)ˇ;")
1868 .await
1869 .assert_matches();
1870 cx.simulate_at_each_offset("%", "ˇconsole.logˇ(ˇ'var', ˇ[ˇ1, ˇ2, 3ˇ]ˇ)ˇ;")
1871 .await
1872 .assert_matches();
1873 cx.simulate_at_each_offset("%", "let result = curried_funˇ(ˇ)ˇ(ˇ)ˇ;")
1874 .await
1875 .assert_matches();
1876 }
1877
1878 #[gpui::test]
1879 async fn test_percent_in_comment(cx: &mut TestAppContext) {
1880 let mut cx = NeovimBackedTestContext::new(cx).await;
1881 cx.simulate_at_each_offset("%", "// ˇconsole.logˇ(ˇvaˇrˇ)ˇ;")
1882 .await
1883 .assert_matches();
1884 cx.simulate_at_each_offset("%", "// ˇ{ ˇ{ˇ}ˇ }ˇ")
1885 .await
1886 .assert_matches();
1887 // Template-style brackets (like Liquid {% %} and {{ }})
1888 cx.simulate_at_each_offset("%", "ˇ{ˇ% block %ˇ}ˇ")
1889 .await
1890 .assert_matches();
1891 cx.simulate_at_each_offset("%", "ˇ{ˇ{ˇ var ˇ}ˇ}ˇ")
1892 .await
1893 .assert_matches();
1894 }
1895
1896 #[gpui::test]
1897 async fn test_end_of_line_with_neovim(cx: &mut gpui::TestAppContext) {
1898 let mut cx = NeovimBackedTestContext::new(cx).await;
1899
1900 // goes to current line end
1901 cx.set_shared_state(indoc! {"ˇaa\nbb\ncc"}).await;
1902 cx.simulate_shared_keystrokes("$").await;
1903 cx.shared_state().await.assert_eq("aˇa\nbb\ncc");
1904
1905 // goes to next line end
1906 cx.simulate_shared_keystrokes("2 $").await;
1907 cx.shared_state().await.assert_eq("aa\nbˇb\ncc");
1908
1909 // try to exceed the final line.
1910 cx.simulate_shared_keystrokes("4 $").await;
1911 cx.shared_state().await.assert_eq("aa\nbb\ncˇc");
1912 }
1913
1914 #[gpui::test]
1915 async fn test_subword_motions(cx: &mut gpui::TestAppContext) {
1916 let mut cx = VimTestContext::new(cx, true).await;
1917 cx.update(|_, cx| {
1918 cx.bind_keys(vec![
1919 KeyBinding::new(
1920 "w",
1921 motion::NextSubwordStart {
1922 ignore_punctuation: false,
1923 },
1924 Some("Editor && VimControl && !VimWaiting && !menu"),
1925 ),
1926 KeyBinding::new(
1927 "b",
1928 motion::PreviousSubwordStart {
1929 ignore_punctuation: false,
1930 },
1931 Some("Editor && VimControl && !VimWaiting && !menu"),
1932 ),
1933 KeyBinding::new(
1934 "e",
1935 motion::NextSubwordEnd {
1936 ignore_punctuation: false,
1937 },
1938 Some("Editor && VimControl && !VimWaiting && !menu"),
1939 ),
1940 KeyBinding::new(
1941 "g e",
1942 motion::PreviousSubwordEnd {
1943 ignore_punctuation: false,
1944 },
1945 Some("Editor && VimControl && !VimWaiting && !menu"),
1946 ),
1947 ]);
1948 });
1949
1950 cx.assert_binding_normal("w", indoc! {"ˇassert_binding"}, indoc! {"assert_ˇbinding"});
1951 // Special case: In 'cw', 'w' acts like 'e'
1952 cx.assert_binding(
1953 "c w",
1954 indoc! {"ˇassert_binding"},
1955 Mode::Normal,
1956 indoc! {"ˇ_binding"},
1957 Mode::Insert,
1958 );
1959
1960 cx.assert_binding_normal("e", indoc! {"ˇassert_binding"}, indoc! {"asserˇt_binding"});
1961
1962 // Subword end should stop at EOL
1963 cx.assert_binding_normal("e", indoc! {"foo_bˇar\nbaz"}, indoc! {"foo_baˇr\nbaz"});
1964
1965 // Already at subword end, should move to next subword on next line
1966 cx.assert_binding_normal(
1967 "e",
1968 indoc! {"foo_barˇ\nbaz_qux"},
1969 indoc! {"foo_bar\nbaˇz_qux"},
1970 );
1971
1972 // CamelCase at EOL
1973 cx.assert_binding_normal("e", indoc! {"fooˇBar\nbaz"}, indoc! {"fooBaˇr\nbaz"});
1974
1975 cx.assert_binding_normal("b", indoc! {"assert_ˇbinding"}, indoc! {"ˇassert_binding"});
1976
1977 cx.assert_binding_normal(
1978 "g e",
1979 indoc! {"assert_bindinˇg"},
1980 indoc! {"asserˇt_binding"},
1981 );
1982 }
1983
1984 #[gpui::test]
1985 async fn test_r(cx: &mut gpui::TestAppContext) {
1986 let mut cx = NeovimBackedTestContext::new(cx).await;
1987
1988 cx.set_shared_state("ˇhello\n").await;
1989 cx.simulate_shared_keystrokes("r -").await;
1990 cx.shared_state().await.assert_eq("ˇ-ello\n");
1991
1992 cx.set_shared_state("ˇhello\n").await;
1993 cx.simulate_shared_keystrokes("3 r -").await;
1994 cx.shared_state().await.assert_eq("--ˇ-lo\n");
1995
1996 cx.set_shared_state("ˇhello\n").await;
1997 cx.simulate_shared_keystrokes("r - 2 l .").await;
1998 cx.shared_state().await.assert_eq("-eˇ-lo\n");
1999
2000 cx.set_shared_state("ˇhello world\n").await;
2001 cx.simulate_shared_keystrokes("2 r - f w .").await;
2002 cx.shared_state().await.assert_eq("--llo -ˇ-rld\n");
2003
2004 cx.set_shared_state("ˇhello world\n").await;
2005 cx.simulate_shared_keystrokes("2 0 r - ").await;
2006 cx.shared_state().await.assert_eq("ˇhello world\n");
2007
2008 cx.set_shared_state(" helloˇ world\n").await;
2009 cx.simulate_shared_keystrokes("r enter").await;
2010 cx.shared_state().await.assert_eq(" hello\n ˇ world\n");
2011
2012 cx.set_shared_state(" helloˇ world\n").await;
2013 cx.simulate_shared_keystrokes("2 r enter").await;
2014 cx.shared_state().await.assert_eq(" hello\n ˇ orld\n");
2015 }
2016
2017 #[gpui::test]
2018 async fn test_gq(cx: &mut gpui::TestAppContext) {
2019 let mut cx = NeovimBackedTestContext::new(cx).await;
2020 cx.set_neovim_option("textwidth=5").await;
2021
2022 cx.update(|_, cx| {
2023 SettingsStore::update_global(cx, |settings, cx| {
2024 settings.update_user_settings(cx, |settings| {
2025 settings
2026 .project
2027 .all_languages
2028 .defaults
2029 .preferred_line_length = Some(5);
2030 });
2031 })
2032 });
2033
2034 cx.set_shared_state("ˇth th th th th th\n").await;
2035 cx.simulate_shared_keystrokes("g q q").await;
2036 cx.shared_state().await.assert_eq("th th\nth th\nˇth th\n");
2037
2038 cx.set_shared_state("ˇth th th th th th\nth th th th th th\n")
2039 .await;
2040 cx.simulate_shared_keystrokes("v j g q").await;
2041 cx.shared_state()
2042 .await
2043 .assert_eq("th th\nth th\nth th\nth th\nth th\nˇth th\n");
2044 }
2045
2046 #[gpui::test]
2047 async fn test_o_comment(cx: &mut gpui::TestAppContext) {
2048 let mut cx = NeovimBackedTestContext::new(cx).await;
2049 cx.set_neovim_option("filetype=rust").await;
2050
2051 cx.set_shared_state("// helloˇ\n").await;
2052 cx.simulate_shared_keystrokes("o").await;
2053 cx.shared_state().await.assert_eq("// hello\n// ˇ\n");
2054 cx.simulate_shared_keystrokes("x escape shift-o").await;
2055 cx.shared_state().await.assert_eq("// hello\n// ˇ\n// x\n");
2056 }
2057
2058 #[gpui::test]
2059 async fn test_yank_line_with_trailing_newline(cx: &mut gpui::TestAppContext) {
2060 let mut cx = NeovimBackedTestContext::new(cx).await;
2061 cx.set_shared_state("heˇllo\n").await;
2062 cx.simulate_shared_keystrokes("y y p").await;
2063 cx.shared_state().await.assert_eq("hello\nˇhello\n");
2064 }
2065
2066 #[gpui::test]
2067 async fn test_yank_line_without_trailing_newline(cx: &mut gpui::TestAppContext) {
2068 let mut cx = NeovimBackedTestContext::new(cx).await;
2069 cx.set_shared_state("heˇllo").await;
2070 cx.simulate_shared_keystrokes("y y p").await;
2071 cx.shared_state().await.assert_eq("hello\nˇhello");
2072 }
2073
2074 #[gpui::test]
2075 async fn test_yank_multiline_without_trailing_newline(cx: &mut gpui::TestAppContext) {
2076 let mut cx = NeovimBackedTestContext::new(cx).await;
2077 cx.set_shared_state("heˇllo\nhello").await;
2078 cx.simulate_shared_keystrokes("2 y y p").await;
2079 cx.shared_state()
2080 .await
2081 .assert_eq("hello\nˇhello\nhello\nhello");
2082 }
2083
2084 #[gpui::test]
2085 async fn test_dd_then_paste_without_trailing_newline(cx: &mut gpui::TestAppContext) {
2086 let mut cx = NeovimBackedTestContext::new(cx).await;
2087 cx.set_shared_state("heˇllo").await;
2088 cx.simulate_shared_keystrokes("d d").await;
2089 cx.shared_state().await.assert_eq("ˇ");
2090 cx.simulate_shared_keystrokes("p p").await;
2091 cx.shared_state().await.assert_eq("\nhello\nˇhello");
2092 }
2093
2094 #[gpui::test]
2095 async fn test_visual_mode_insert_before_after(cx: &mut gpui::TestAppContext) {
2096 let mut cx = NeovimBackedTestContext::new(cx).await;
2097
2098 cx.set_shared_state("heˇllo").await;
2099 cx.simulate_shared_keystrokes("v i w shift-i").await;
2100 cx.shared_state().await.assert_eq("ˇhello");
2101
2102 cx.set_shared_state(indoc! {"
2103 The quick brown
2104 fox ˇjumps over
2105 the lazy dog"})
2106 .await;
2107 cx.simulate_shared_keystrokes("shift-v shift-i").await;
2108 cx.shared_state().await.assert_eq(indoc! {"
2109 The quick brown
2110 ˇfox jumps over
2111 the lazy dog"});
2112
2113 cx.set_shared_state(indoc! {"
2114 The quick brown
2115 fox ˇjumps over
2116 the lazy dog"})
2117 .await;
2118 cx.simulate_shared_keystrokes("shift-v shift-a").await;
2119 cx.shared_state().await.assert_eq(indoc! {"
2120 The quick brown
2121 fox jˇumps over
2122 the lazy dog"});
2123 }
2124
2125 #[gpui::test]
2126 async fn test_jump_list(cx: &mut gpui::TestAppContext) {
2127 let mut cx = NeovimBackedTestContext::new(cx).await;
2128
2129 cx.set_shared_state(indoc! {"
2130 ˇfn a() { }
2131
2132
2133
2134
2135
2136 fn b() { }
2137
2138
2139
2140
2141
2142 fn b() { }"})
2143 .await;
2144 cx.simulate_shared_keystrokes("3 }").await;
2145 cx.shared_state().await.assert_matches();
2146 cx.simulate_shared_keystrokes("ctrl-o").await;
2147 cx.shared_state().await.assert_matches();
2148 cx.simulate_shared_keystrokes("ctrl-i").await;
2149 cx.shared_state().await.assert_matches();
2150 cx.simulate_shared_keystrokes("1 1 k").await;
2151 cx.shared_state().await.assert_matches();
2152 cx.simulate_shared_keystrokes("ctrl-o").await;
2153 cx.shared_state().await.assert_matches();
2154 }
2155
2156 #[gpui::test]
2157 async fn test_undo_last_line(cx: &mut gpui::TestAppContext) {
2158 let mut cx = NeovimBackedTestContext::new(cx).await;
2159
2160 cx.set_shared_state(indoc! {"
2161 ˇfn a() { }
2162 fn a() { }
2163 fn a() { }
2164 "})
2165 .await;
2166 // do a jump to reset vim's undo grouping
2167 cx.simulate_shared_keystrokes("shift-g").await;
2168 cx.shared_state().await.assert_matches();
2169 cx.simulate_shared_keystrokes("r a").await;
2170 cx.shared_state().await.assert_matches();
2171 cx.simulate_shared_keystrokes("shift-u").await;
2172 cx.shared_state().await.assert_matches();
2173 cx.simulate_shared_keystrokes("shift-u").await;
2174 cx.shared_state().await.assert_matches();
2175 cx.simulate_shared_keystrokes("g g shift-u").await;
2176 cx.shared_state().await.assert_matches();
2177 }
2178
2179 #[gpui::test]
2180 async fn test_undo_last_line_newline(cx: &mut gpui::TestAppContext) {
2181 let mut cx = NeovimBackedTestContext::new(cx).await;
2182
2183 cx.set_shared_state(indoc! {"
2184 ˇfn a() { }
2185 fn a() { }
2186 fn a() { }
2187 "})
2188 .await;
2189 // do a jump to reset vim's undo grouping
2190 cx.simulate_shared_keystrokes("shift-g k").await;
2191 cx.shared_state().await.assert_matches();
2192 cx.simulate_shared_keystrokes("o h e l l o escape").await;
2193 cx.shared_state().await.assert_matches();
2194 cx.simulate_shared_keystrokes("shift-u").await;
2195 cx.shared_state().await.assert_matches();
2196 cx.simulate_shared_keystrokes("shift-u").await;
2197 }
2198
2199 #[gpui::test]
2200 async fn test_undo_last_line_newline_many_changes(cx: &mut gpui::TestAppContext) {
2201 let mut cx = NeovimBackedTestContext::new(cx).await;
2202
2203 cx.set_shared_state(indoc! {"
2204 ˇfn a() { }
2205 fn a() { }
2206 fn a() { }
2207 "})
2208 .await;
2209 // do a jump to reset vim's undo grouping
2210 cx.simulate_shared_keystrokes("x shift-g k").await;
2211 cx.shared_state().await.assert_matches();
2212 cx.simulate_shared_keystrokes("x f a x f { x").await;
2213 cx.shared_state().await.assert_matches();
2214 cx.simulate_shared_keystrokes("shift-u").await;
2215 cx.shared_state().await.assert_matches();
2216 cx.simulate_shared_keystrokes("shift-u").await;
2217 cx.shared_state().await.assert_matches();
2218 cx.simulate_shared_keystrokes("shift-u").await;
2219 cx.shared_state().await.assert_matches();
2220 cx.simulate_shared_keystrokes("shift-u").await;
2221 cx.shared_state().await.assert_matches();
2222 }
2223
2224 #[gpui::test]
2225 async fn test_undo_last_line_multicursor(cx: &mut gpui::TestAppContext) {
2226 let mut cx = VimTestContext::new(cx, true).await;
2227
2228 cx.set_state(
2229 indoc! {"
2230 ˇone two ˇone
2231 two ˇone two
2232 "},
2233 Mode::Normal,
2234 );
2235 cx.simulate_keystrokes("3 r a");
2236 cx.assert_state(
2237 indoc! {"
2238 aaˇa two aaˇa
2239 two aaˇa two
2240 "},
2241 Mode::Normal,
2242 );
2243 cx.simulate_keystrokes("escape escape");
2244 cx.simulate_keystrokes("shift-u");
2245 cx.set_state(
2246 indoc! {"
2247 onˇe two onˇe
2248 two onˇe two
2249 "},
2250 Mode::Normal,
2251 );
2252 }
2253
2254 #[gpui::test]
2255 async fn test_go_to_tab_with_count(cx: &mut gpui::TestAppContext) {
2256 let mut cx = VimTestContext::new(cx, true).await;
2257
2258 // Open 4 tabs.
2259 cx.simulate_keystrokes(": tabnew");
2260 cx.simulate_keystrokes("enter");
2261 cx.simulate_keystrokes(": tabnew");
2262 cx.simulate_keystrokes("enter");
2263 cx.simulate_keystrokes(": tabnew");
2264 cx.simulate_keystrokes("enter");
2265 cx.workspace(|workspace, _, cx| {
2266 assert_eq!(workspace.items(cx).count(), 4);
2267 assert_eq!(workspace.active_pane().read(cx).active_item_index(), 3);
2268 });
2269
2270 cx.simulate_keystrokes("1 g t");
2271 cx.workspace(|workspace, _, cx| {
2272 assert_eq!(workspace.active_pane().read(cx).active_item_index(), 0);
2273 });
2274
2275 cx.simulate_keystrokes("3 g t");
2276 cx.workspace(|workspace, _, cx| {
2277 assert_eq!(workspace.active_pane().read(cx).active_item_index(), 2);
2278 });
2279
2280 cx.simulate_keystrokes("4 g t");
2281 cx.workspace(|workspace, _, cx| {
2282 assert_eq!(workspace.active_pane().read(cx).active_item_index(), 3);
2283 });
2284
2285 cx.simulate_keystrokes("1 g t");
2286 cx.simulate_keystrokes("g t");
2287 cx.workspace(|workspace, _, cx| {
2288 assert_eq!(workspace.active_pane().read(cx).active_item_index(), 1);
2289 });
2290 }
2291
2292 #[gpui::test]
2293 async fn test_go_to_previous_tab_with_count(cx: &mut gpui::TestAppContext) {
2294 let mut cx = VimTestContext::new(cx, true).await;
2295
2296 // Open 4 tabs.
2297 cx.simulate_keystrokes(": tabnew");
2298 cx.simulate_keystrokes("enter");
2299 cx.simulate_keystrokes(": tabnew");
2300 cx.simulate_keystrokes("enter");
2301 cx.simulate_keystrokes(": tabnew");
2302 cx.simulate_keystrokes("enter");
2303 cx.workspace(|workspace, _, cx| {
2304 assert_eq!(workspace.items(cx).count(), 4);
2305 assert_eq!(workspace.active_pane().read(cx).active_item_index(), 3);
2306 });
2307
2308 cx.simulate_keystrokes("2 g shift-t");
2309 cx.workspace(|workspace, _, cx| {
2310 assert_eq!(workspace.active_pane().read(cx).active_item_index(), 1);
2311 });
2312
2313 cx.simulate_keystrokes("g shift-t");
2314 cx.workspace(|workspace, _, cx| {
2315 assert_eq!(workspace.active_pane().read(cx).active_item_index(), 0);
2316 });
2317
2318 // Wraparound: gT from first tab should go to last.
2319 cx.simulate_keystrokes("g shift-t");
2320 cx.workspace(|workspace, _, cx| {
2321 assert_eq!(workspace.active_pane().read(cx).active_item_index(), 3);
2322 });
2323
2324 cx.simulate_keystrokes("6 g shift-t");
2325 cx.workspace(|workspace, _, cx| {
2326 assert_eq!(workspace.active_pane().read(cx).active_item_index(), 1);
2327 });
2328 }
2329
2330 #[gpui::test]
2331 async fn test_temporary_mode(cx: &mut gpui::TestAppContext) {
2332 let mut cx = NeovimBackedTestContext::new(cx).await;
2333
2334 // Test jumping to the end of the line ($).
2335 cx.set_shared_state(indoc! {"lorem ˇipsum"}).await;
2336 cx.simulate_shared_keystrokes("i").await;
2337 cx.shared_state().await.assert_matches();
2338 cx.simulate_shared_keystrokes("ctrl-o $").await;
2339 cx.shared_state().await.assert_eq(indoc! {"lorem ipsumˇ"});
2340
2341 // Test jumping to the next word.
2342 cx.set_shared_state(indoc! {"loremˇ ipsum dolor"}).await;
2343 cx.simulate_shared_keystrokes("a").await;
2344 cx.shared_state().await.assert_matches();
2345 cx.simulate_shared_keystrokes("a n d space ctrl-o w").await;
2346 cx.shared_state()
2347 .await
2348 .assert_eq(indoc! {"lorem and ipsum ˇdolor"});
2349
2350 // Test yanking to end of line ($).
2351 cx.set_shared_state(indoc! {"lorem ˇipsum dolor"}).await;
2352 cx.simulate_shared_keystrokes("i").await;
2353 cx.shared_state().await.assert_matches();
2354 cx.simulate_shared_keystrokes("a n d space ctrl-o y $")
2355 .await;
2356 cx.shared_state()
2357 .await
2358 .assert_eq(indoc! {"lorem and ˇipsum dolor"});
2359 }
2360}