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