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