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