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