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