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