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