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 // We need to use `text.chars().count()` instead of `text.len()` here as
969 // `len()` counts bytes, not characters.
970 let char_count = text.chars().count();
971 let count = Vim::take_count(cx).unwrap_or(char_count);
972 let is_return_char = text == "\n".into() || text == "\r".into();
973 let repeat_count = match (is_return_char, char_count) {
974 (true, _) => 0,
975 (_, 1) => count,
976 (_, _) => 1,
977 };
978
979 Vim::take_forced_motion(cx);
980 self.stop_recording(cx);
981 self.update_editor(cx, |_, editor, cx| {
982 editor.transact(window, cx, |editor, window, cx| {
983 editor.set_clip_at_line_ends(false, cx);
984 let display_map = editor.display_snapshot(cx);
985 let display_selections = editor.selections.all_display(&display_map);
986
987 let mut edits = Vec::with_capacity(display_selections.len());
988 for selection in &display_selections {
989 let mut range = selection.range();
990 for _ in 0..count {
991 let new_point = movement::saturating_right(&display_map, range.end);
992 if range.end == new_point {
993 return;
994 }
995 range.end = new_point;
996 }
997
998 edits.push((
999 range.start.to_offset(&display_map, Bias::Left)
1000 ..range.end.to_offset(&display_map, Bias::Left),
1001 text.repeat(repeat_count),
1002 ));
1003 }
1004
1005 editor.edit(edits, cx);
1006 if is_return_char {
1007 editor.newline(&editor::actions::Newline, window, cx);
1008 }
1009 editor.set_clip_at_line_ends(true, cx);
1010 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1011 s.move_with(|map, selection| {
1012 let point = movement::saturating_left(map, selection.head());
1013 selection.collapse_to(point, SelectionGoal::None)
1014 });
1015 });
1016 });
1017 });
1018 self.pop_operator(window, cx);
1019 }
1020
1021 pub fn save_selection_starts(
1022 &self,
1023 editor: &Editor,
1024 cx: &mut Context<Editor>,
1025 ) -> HashMap<usize, Anchor> {
1026 let display_map = editor.display_snapshot(cx);
1027 let selections = editor.selections.all_display(&display_map);
1028 selections
1029 .iter()
1030 .map(|selection| {
1031 (
1032 selection.id,
1033 display_map.display_point_to_anchor(selection.start, Bias::Right),
1034 )
1035 })
1036 .collect::<HashMap<_, _>>()
1037 }
1038
1039 pub fn restore_selection_cursors(
1040 &self,
1041 editor: &mut Editor,
1042 window: &mut Window,
1043 cx: &mut Context<Editor>,
1044 mut positions: HashMap<usize, Anchor>,
1045 ) {
1046 editor.change_selections(Default::default(), window, cx, |s| {
1047 s.move_with(|map, selection| {
1048 if let Some(anchor) = positions.remove(&selection.id) {
1049 selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None);
1050 }
1051 });
1052 });
1053 }
1054
1055 fn exit_temporary_normal(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1056 if self.temp_mode {
1057 self.switch_mode(Mode::Insert, true, window, cx);
1058 }
1059 }
1060}
1061
1062#[cfg(test)]
1063mod test {
1064 use gpui::{KeyBinding, TestAppContext, UpdateGlobal};
1065 use indoc::indoc;
1066 use settings::SettingsStore;
1067
1068 use crate::{
1069 motion,
1070 state::Mode::{self},
1071 test::{NeovimBackedTestContext, VimTestContext},
1072 };
1073
1074 #[gpui::test]
1075 async fn test_h(cx: &mut gpui::TestAppContext) {
1076 let mut cx = NeovimBackedTestContext::new(cx).await;
1077 cx.simulate_at_each_offset(
1078 "h",
1079 indoc! {"
1080 ˇThe qˇuick
1081 ˇbrown"
1082 },
1083 )
1084 .await
1085 .assert_matches();
1086 }
1087
1088 #[gpui::test]
1089 async fn test_backspace(cx: &mut gpui::TestAppContext) {
1090 let mut cx = NeovimBackedTestContext::new(cx).await;
1091 cx.simulate_at_each_offset(
1092 "backspace",
1093 indoc! {"
1094 ˇThe qˇuick
1095 ˇbrown"
1096 },
1097 )
1098 .await
1099 .assert_matches();
1100 }
1101
1102 #[gpui::test]
1103 async fn test_j(cx: &mut gpui::TestAppContext) {
1104 let mut cx = NeovimBackedTestContext::new(cx).await;
1105
1106 cx.set_shared_state(indoc! {"
1107 aaˇaa
1108 😃😃"
1109 })
1110 .await;
1111 cx.simulate_shared_keystrokes("j").await;
1112 cx.shared_state().await.assert_eq(indoc! {"
1113 aaaa
1114 😃ˇ😃"
1115 });
1116
1117 cx.simulate_at_each_offset(
1118 "j",
1119 indoc! {"
1120 ˇThe qˇuick broˇwn
1121 ˇfox jumps"
1122 },
1123 )
1124 .await
1125 .assert_matches();
1126 }
1127
1128 #[gpui::test]
1129 async fn test_enter(cx: &mut gpui::TestAppContext) {
1130 let mut cx = NeovimBackedTestContext::new(cx).await;
1131 cx.simulate_at_each_offset(
1132 "enter",
1133 indoc! {"
1134 ˇThe qˇuick broˇwn
1135 ˇfox jumps"
1136 },
1137 )
1138 .await
1139 .assert_matches();
1140 }
1141
1142 #[gpui::test]
1143 async fn test_k(cx: &mut gpui::TestAppContext) {
1144 let mut cx = NeovimBackedTestContext::new(cx).await;
1145 cx.simulate_at_each_offset(
1146 "k",
1147 indoc! {"
1148 ˇThe qˇuick
1149 ˇbrown fˇox jumˇps"
1150 },
1151 )
1152 .await
1153 .assert_matches();
1154 }
1155
1156 #[gpui::test]
1157 async fn test_l(cx: &mut gpui::TestAppContext) {
1158 let mut cx = NeovimBackedTestContext::new(cx).await;
1159 cx.simulate_at_each_offset(
1160 "l",
1161 indoc! {"
1162 ˇThe qˇuicˇk
1163 ˇbrowˇn"},
1164 )
1165 .await
1166 .assert_matches();
1167 }
1168
1169 #[gpui::test]
1170 async fn test_jump_to_line_boundaries(cx: &mut gpui::TestAppContext) {
1171 let mut cx = NeovimBackedTestContext::new(cx).await;
1172 cx.simulate_at_each_offset(
1173 "$",
1174 indoc! {"
1175 ˇThe qˇuicˇk
1176 ˇbrowˇn"},
1177 )
1178 .await
1179 .assert_matches();
1180 cx.simulate_at_each_offset(
1181 "0",
1182 indoc! {"
1183 ˇThe qˇuicˇk
1184 ˇbrowˇn"},
1185 )
1186 .await
1187 .assert_matches();
1188 }
1189
1190 #[gpui::test]
1191 async fn test_jump_to_end(cx: &mut gpui::TestAppContext) {
1192 let mut cx = NeovimBackedTestContext::new(cx).await;
1193
1194 cx.simulate_at_each_offset(
1195 "shift-g",
1196 indoc! {"
1197 The ˇquick
1198
1199 brown fox jumps
1200 overˇ the lazy doˇg"},
1201 )
1202 .await
1203 .assert_matches();
1204 cx.simulate(
1205 "shift-g",
1206 indoc! {"
1207 The quiˇck
1208
1209 brown"},
1210 )
1211 .await
1212 .assert_matches();
1213 cx.simulate(
1214 "shift-g",
1215 indoc! {"
1216 The quiˇck
1217
1218 "},
1219 )
1220 .await
1221 .assert_matches();
1222 }
1223
1224 #[gpui::test]
1225 async fn test_w(cx: &mut gpui::TestAppContext) {
1226 let mut cx = NeovimBackedTestContext::new(cx).await;
1227 cx.simulate_at_each_offset(
1228 "w",
1229 indoc! {"
1230 The ˇquickˇ-ˇbrown
1231 ˇ
1232 ˇ
1233 ˇfox_jumps ˇover
1234 ˇthˇe"},
1235 )
1236 .await
1237 .assert_matches();
1238 cx.simulate_at_each_offset(
1239 "shift-w",
1240 indoc! {"
1241 The ˇquickˇ-ˇbrown
1242 ˇ
1243 ˇ
1244 ˇfox_jumps ˇover
1245 ˇthˇe"},
1246 )
1247 .await
1248 .assert_matches();
1249 }
1250
1251 #[gpui::test]
1252 async fn test_end_of_word(cx: &mut gpui::TestAppContext) {
1253 let mut cx = NeovimBackedTestContext::new(cx).await;
1254 cx.simulate_at_each_offset(
1255 "e",
1256 indoc! {"
1257 Thˇe quicˇkˇ-browˇn
1258
1259
1260 fox_jumpˇs oveˇr
1261 thˇe"},
1262 )
1263 .await
1264 .assert_matches();
1265 cx.simulate_at_each_offset(
1266 "shift-e",
1267 indoc! {"
1268 Thˇe quicˇkˇ-browˇn
1269
1270
1271 fox_jumpˇs oveˇr
1272 thˇe"},
1273 )
1274 .await
1275 .assert_matches();
1276 }
1277
1278 #[gpui::test]
1279 async fn test_b(cx: &mut gpui::TestAppContext) {
1280 let mut cx = NeovimBackedTestContext::new(cx).await;
1281 cx.simulate_at_each_offset(
1282 "b",
1283 indoc! {"
1284 ˇThe ˇquickˇ-ˇbrown
1285 ˇ
1286 ˇ
1287 ˇfox_jumps ˇover
1288 ˇthe"},
1289 )
1290 .await
1291 .assert_matches();
1292 cx.simulate_at_each_offset(
1293 "shift-b",
1294 indoc! {"
1295 ˇThe ˇquickˇ-ˇbrown
1296 ˇ
1297 ˇ
1298 ˇfox_jumps ˇover
1299 ˇthe"},
1300 )
1301 .await
1302 .assert_matches();
1303 }
1304
1305 #[gpui::test]
1306 async fn test_gg(cx: &mut gpui::TestAppContext) {
1307 let mut cx = NeovimBackedTestContext::new(cx).await;
1308 cx.simulate_at_each_offset(
1309 "g g",
1310 indoc! {"
1311 The qˇuick
1312
1313 brown fox jumps
1314 over ˇthe laˇzy dog"},
1315 )
1316 .await
1317 .assert_matches();
1318 cx.simulate(
1319 "g g",
1320 indoc! {"
1321
1322
1323 brown fox jumps
1324 over the laˇzy dog"},
1325 )
1326 .await
1327 .assert_matches();
1328 cx.simulate(
1329 "2 g g",
1330 indoc! {"
1331 ˇ
1332
1333 brown fox jumps
1334 over the lazydog"},
1335 )
1336 .await
1337 .assert_matches();
1338 }
1339
1340 #[gpui::test]
1341 async fn test_end_of_document(cx: &mut gpui::TestAppContext) {
1342 let mut cx = NeovimBackedTestContext::new(cx).await;
1343 cx.simulate_at_each_offset(
1344 "shift-g",
1345 indoc! {"
1346 The qˇuick
1347
1348 brown fox jumps
1349 over ˇthe laˇzy dog"},
1350 )
1351 .await
1352 .assert_matches();
1353 cx.simulate(
1354 "shift-g",
1355 indoc! {"
1356
1357
1358 brown fox jumps
1359 over the laˇzy dog"},
1360 )
1361 .await
1362 .assert_matches();
1363 cx.simulate(
1364 "2 shift-g",
1365 indoc! {"
1366 ˇ
1367
1368 brown fox jumps
1369 over the lazydog"},
1370 )
1371 .await
1372 .assert_matches();
1373 }
1374
1375 #[gpui::test]
1376 async fn test_a(cx: &mut gpui::TestAppContext) {
1377 let mut cx = NeovimBackedTestContext::new(cx).await;
1378 cx.simulate_at_each_offset("a", "The qˇuicˇk")
1379 .await
1380 .assert_matches();
1381 }
1382
1383 #[gpui::test]
1384 async fn test_insert_end_of_line(cx: &mut gpui::TestAppContext) {
1385 let mut cx = NeovimBackedTestContext::new(cx).await;
1386 cx.simulate_at_each_offset(
1387 "shift-a",
1388 indoc! {"
1389 ˇ
1390 The qˇuick
1391 brown ˇfox "},
1392 )
1393 .await
1394 .assert_matches();
1395 }
1396
1397 #[gpui::test]
1398 async fn test_jump_to_first_non_whitespace(cx: &mut gpui::TestAppContext) {
1399 let mut cx = NeovimBackedTestContext::new(cx).await;
1400 cx.simulate("^", "The qˇuick").await.assert_matches();
1401 cx.simulate("^", " The qˇuick").await.assert_matches();
1402 cx.simulate("^", "ˇ").await.assert_matches();
1403 cx.simulate(
1404 "^",
1405 indoc! {"
1406 The qˇuick
1407 brown fox"},
1408 )
1409 .await
1410 .assert_matches();
1411 cx.simulate(
1412 "^",
1413 indoc! {"
1414 ˇ
1415 The quick"},
1416 )
1417 .await
1418 .assert_matches();
1419 // Indoc disallows trailing whitespace.
1420 cx.simulate("^", " ˇ \nThe quick").await.assert_matches();
1421 }
1422
1423 #[gpui::test]
1424 async fn test_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
1425 let mut cx = NeovimBackedTestContext::new(cx).await;
1426 cx.simulate("shift-i", "The qˇuick").await.assert_matches();
1427 cx.simulate("shift-i", " The qˇuick").await.assert_matches();
1428 cx.simulate("shift-i", "ˇ").await.assert_matches();
1429 cx.simulate(
1430 "shift-i",
1431 indoc! {"
1432 The qˇuick
1433 brown fox"},
1434 )
1435 .await
1436 .assert_matches();
1437 cx.simulate(
1438 "shift-i",
1439 indoc! {"
1440 ˇ
1441 The quick"},
1442 )
1443 .await
1444 .assert_matches();
1445 }
1446
1447 #[gpui::test]
1448 async fn test_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
1449 let mut cx = NeovimBackedTestContext::new(cx).await;
1450 cx.simulate(
1451 "shift-d",
1452 indoc! {"
1453 The qˇuick
1454 brown fox"},
1455 )
1456 .await
1457 .assert_matches();
1458 cx.simulate(
1459 "shift-d",
1460 indoc! {"
1461 The quick
1462 ˇ
1463 brown fox"},
1464 )
1465 .await
1466 .assert_matches();
1467 }
1468
1469 #[gpui::test]
1470 async fn test_x(cx: &mut gpui::TestAppContext) {
1471 let mut cx = NeovimBackedTestContext::new(cx).await;
1472 cx.simulate_at_each_offset("x", "ˇTeˇsˇt")
1473 .await
1474 .assert_matches();
1475 cx.simulate(
1476 "x",
1477 indoc! {"
1478 Tesˇt
1479 test"},
1480 )
1481 .await
1482 .assert_matches();
1483 }
1484
1485 #[gpui::test]
1486 async fn test_delete_left(cx: &mut gpui::TestAppContext) {
1487 let mut cx = NeovimBackedTestContext::new(cx).await;
1488 cx.simulate_at_each_offset("shift-x", "ˇTˇeˇsˇt")
1489 .await
1490 .assert_matches();
1491 cx.simulate(
1492 "shift-x",
1493 indoc! {"
1494 Test
1495 ˇtest"},
1496 )
1497 .await
1498 .assert_matches();
1499 }
1500
1501 #[gpui::test]
1502 async fn test_o(cx: &mut gpui::TestAppContext) {
1503 let mut cx = NeovimBackedTestContext::new(cx).await;
1504 cx.simulate("o", "ˇ").await.assert_matches();
1505 cx.simulate("o", "The ˇquick").await.assert_matches();
1506 cx.simulate_at_each_offset(
1507 "o",
1508 indoc! {"
1509 The qˇuick
1510 brown ˇfox
1511 jumps ˇover"},
1512 )
1513 .await
1514 .assert_matches();
1515 cx.simulate(
1516 "o",
1517 indoc! {"
1518 The quick
1519 ˇ
1520 brown fox"},
1521 )
1522 .await
1523 .assert_matches();
1524
1525 cx.assert_binding(
1526 "o",
1527 indoc! {"
1528 fn test() {
1529 println!(ˇ);
1530 }"},
1531 Mode::Normal,
1532 indoc! {"
1533 fn test() {
1534 println!();
1535 ˇ
1536 }"},
1537 Mode::Insert,
1538 );
1539
1540 cx.assert_binding(
1541 "o",
1542 indoc! {"
1543 fn test(ˇ) {
1544 println!();
1545 }"},
1546 Mode::Normal,
1547 indoc! {"
1548 fn test() {
1549 ˇ
1550 println!();
1551 }"},
1552 Mode::Insert,
1553 );
1554 }
1555
1556 #[gpui::test]
1557 async fn test_insert_line_above(cx: &mut gpui::TestAppContext) {
1558 let mut cx = NeovimBackedTestContext::new(cx).await;
1559 cx.simulate("shift-o", "ˇ").await.assert_matches();
1560 cx.simulate("shift-o", "The ˇquick").await.assert_matches();
1561 cx.simulate_at_each_offset(
1562 "shift-o",
1563 indoc! {"
1564 The qˇuick
1565 brown ˇfox
1566 jumps ˇover"},
1567 )
1568 .await
1569 .assert_matches();
1570 cx.simulate(
1571 "shift-o",
1572 indoc! {"
1573 The quick
1574 ˇ
1575 brown fox"},
1576 )
1577 .await
1578 .assert_matches();
1579
1580 // Our indentation is smarter than vims. So we don't match here
1581 cx.assert_binding(
1582 "shift-o",
1583 indoc! {"
1584 fn test() {
1585 println!(ˇ);
1586 }"},
1587 Mode::Normal,
1588 indoc! {"
1589 fn test() {
1590 ˇ
1591 println!();
1592 }"},
1593 Mode::Insert,
1594 );
1595 cx.assert_binding(
1596 "shift-o",
1597 indoc! {"
1598 fn test(ˇ) {
1599 println!();
1600 }"},
1601 Mode::Normal,
1602 indoc! {"
1603 ˇ
1604 fn test() {
1605 println!();
1606 }"},
1607 Mode::Insert,
1608 );
1609 }
1610
1611 #[gpui::test]
1612 async fn test_insert_empty_line(cx: &mut gpui::TestAppContext) {
1613 let mut cx = NeovimBackedTestContext::new(cx).await;
1614 cx.simulate("[ space", "ˇ").await.assert_matches();
1615 cx.simulate("[ space", "The ˇquick").await.assert_matches();
1616 cx.simulate_at_each_offset(
1617 "3 [ space",
1618 indoc! {"
1619 The qˇuick
1620 brown ˇfox
1621 jumps ˇover"},
1622 )
1623 .await
1624 .assert_matches();
1625 cx.simulate_at_each_offset(
1626 "[ space",
1627 indoc! {"
1628 The qˇuick
1629 brown ˇfox
1630 jumps ˇover"},
1631 )
1632 .await
1633 .assert_matches();
1634 cx.simulate(
1635 "[ space",
1636 indoc! {"
1637 The quick
1638 ˇ
1639 brown fox"},
1640 )
1641 .await
1642 .assert_matches();
1643
1644 cx.simulate("] space", "ˇ").await.assert_matches();
1645 cx.simulate("] space", "The ˇquick").await.assert_matches();
1646 cx.simulate_at_each_offset(
1647 "3 ] space",
1648 indoc! {"
1649 The qˇuick
1650 brown ˇfox
1651 jumps ˇover"},
1652 )
1653 .await
1654 .assert_matches();
1655 cx.simulate_at_each_offset(
1656 "] space",
1657 indoc! {"
1658 The qˇuick
1659 brown ˇfox
1660 jumps ˇover"},
1661 )
1662 .await
1663 .assert_matches();
1664 cx.simulate(
1665 "] space",
1666 indoc! {"
1667 The quick
1668 ˇ
1669 brown fox"},
1670 )
1671 .await
1672 .assert_matches();
1673 }
1674
1675 #[gpui::test]
1676 async fn test_dd(cx: &mut gpui::TestAppContext) {
1677 let mut cx = NeovimBackedTestContext::new(cx).await;
1678 cx.simulate("d d", "ˇ").await.assert_matches();
1679 cx.simulate("d d", "The ˇquick").await.assert_matches();
1680 cx.simulate_at_each_offset(
1681 "d d",
1682 indoc! {"
1683 The qˇuick
1684 brown ˇfox
1685 jumps ˇover"},
1686 )
1687 .await
1688 .assert_matches();
1689 cx.simulate(
1690 "d d",
1691 indoc! {"
1692 The quick
1693 ˇ
1694 brown fox"},
1695 )
1696 .await
1697 .assert_matches();
1698 }
1699
1700 #[gpui::test]
1701 async fn test_cc(cx: &mut gpui::TestAppContext) {
1702 let mut cx = NeovimBackedTestContext::new(cx).await;
1703 cx.simulate("c c", "ˇ").await.assert_matches();
1704 cx.simulate("c c", "The ˇquick").await.assert_matches();
1705 cx.simulate_at_each_offset(
1706 "c c",
1707 indoc! {"
1708 The quˇick
1709 brown ˇfox
1710 jumps ˇover"},
1711 )
1712 .await
1713 .assert_matches();
1714 cx.simulate(
1715 "c c",
1716 indoc! {"
1717 The quick
1718 ˇ
1719 brown fox"},
1720 )
1721 .await
1722 .assert_matches();
1723 }
1724
1725 #[gpui::test]
1726 async fn test_repeated_word(cx: &mut gpui::TestAppContext) {
1727 let mut cx = NeovimBackedTestContext::new(cx).await;
1728
1729 for count in 1..=5 {
1730 cx.simulate_at_each_offset(
1731 &format!("{count} w"),
1732 indoc! {"
1733 ˇThe quˇickˇ browˇn
1734 ˇ
1735 ˇfox ˇjumpsˇ-ˇoˇver
1736 ˇthe lazy dog
1737 "},
1738 )
1739 .await
1740 .assert_matches();
1741 }
1742 }
1743
1744 #[gpui::test]
1745 async fn test_h_through_unicode(cx: &mut gpui::TestAppContext) {
1746 let mut cx = NeovimBackedTestContext::new(cx).await;
1747 cx.simulate_at_each_offset("h", "Testˇ├ˇ──ˇ┐ˇTest")
1748 .await
1749 .assert_matches();
1750 }
1751
1752 #[gpui::test]
1753 async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
1754 let mut cx = NeovimBackedTestContext::new(cx).await;
1755
1756 for count in 1..=3 {
1757 let test_case = indoc! {"
1758 ˇaaaˇbˇ ˇbˇ ˇbˇbˇ aˇaaˇbaaa
1759 ˇ ˇbˇaaˇa ˇbˇbˇb
1760 ˇ
1761 ˇb
1762 "};
1763
1764 cx.simulate_at_each_offset(&format!("{count} f b"), test_case)
1765 .await
1766 .assert_matches();
1767
1768 cx.simulate_at_each_offset(&format!("{count} t b"), test_case)
1769 .await
1770 .assert_matches();
1771 }
1772 }
1773
1774 #[gpui::test]
1775 async fn test_capital_f_and_capital_t(cx: &mut gpui::TestAppContext) {
1776 let mut cx = NeovimBackedTestContext::new(cx).await;
1777 let test_case = indoc! {"
1778 ˇaaaˇbˇ ˇbˇ ˇbˇbˇ aˇaaˇbaaa
1779 ˇ ˇbˇaaˇa ˇbˇbˇb
1780 ˇ•••
1781 ˇb
1782 "
1783 };
1784
1785 for count in 1..=3 {
1786 cx.simulate_at_each_offset(&format!("{count} shift-f b"), test_case)
1787 .await
1788 .assert_matches();
1789
1790 cx.simulate_at_each_offset(&format!("{count} shift-t b"), test_case)
1791 .await
1792 .assert_matches();
1793 }
1794 }
1795
1796 #[gpui::test]
1797 async fn test_f_and_t_smartcase(cx: &mut gpui::TestAppContext) {
1798 let mut cx = VimTestContext::new(cx, true).await;
1799 cx.update_global(|store: &mut SettingsStore, cx| {
1800 store.update_user_settings(cx, |s| {
1801 s.vim.get_or_insert_default().use_smartcase_find = Some(true);
1802 });
1803 });
1804
1805 cx.assert_binding(
1806 "f p",
1807 indoc! {"ˇfmt.Println(\"Hello, World!\")"},
1808 Mode::Normal,
1809 indoc! {"fmt.ˇPrintln(\"Hello, World!\")"},
1810 Mode::Normal,
1811 );
1812
1813 cx.assert_binding(
1814 "shift-f p",
1815 indoc! {"fmt.Printlnˇ(\"Hello, World!\")"},
1816 Mode::Normal,
1817 indoc! {"fmt.ˇPrintln(\"Hello, World!\")"},
1818 Mode::Normal,
1819 );
1820
1821 cx.assert_binding(
1822 "t p",
1823 indoc! {"ˇfmt.Println(\"Hello, World!\")"},
1824 Mode::Normal,
1825 indoc! {"fmtˇ.Println(\"Hello, World!\")"},
1826 Mode::Normal,
1827 );
1828
1829 cx.assert_binding(
1830 "shift-t p",
1831 indoc! {"fmt.Printlnˇ(\"Hello, World!\")"},
1832 Mode::Normal,
1833 indoc! {"fmt.Pˇrintln(\"Hello, World!\")"},
1834 Mode::Normal,
1835 );
1836 }
1837
1838 #[gpui::test]
1839 async fn test_percent(cx: &mut TestAppContext) {
1840 let mut cx = NeovimBackedTestContext::new(cx).await;
1841 cx.simulate_at_each_offset("%", "ˇconsole.logˇ(ˇvaˇrˇ)ˇ;")
1842 .await
1843 .assert_matches();
1844 cx.simulate_at_each_offset("%", "ˇconsole.logˇ(ˇ'var', ˇ[ˇ1, ˇ2, 3ˇ]ˇ)ˇ;")
1845 .await
1846 .assert_matches();
1847 cx.simulate_at_each_offset("%", "let result = curried_funˇ(ˇ)ˇ(ˇ)ˇ;")
1848 .await
1849 .assert_matches();
1850 }
1851
1852 #[gpui::test]
1853 async fn test_end_of_line_with_neovim(cx: &mut gpui::TestAppContext) {
1854 let mut cx = NeovimBackedTestContext::new(cx).await;
1855
1856 // goes to current line end
1857 cx.set_shared_state(indoc! {"ˇaa\nbb\ncc"}).await;
1858 cx.simulate_shared_keystrokes("$").await;
1859 cx.shared_state().await.assert_eq("aˇa\nbb\ncc");
1860
1861 // goes to next line end
1862 cx.simulate_shared_keystrokes("2 $").await;
1863 cx.shared_state().await.assert_eq("aa\nbˇb\ncc");
1864
1865 // try to exceed the final line.
1866 cx.simulate_shared_keystrokes("4 $").await;
1867 cx.shared_state().await.assert_eq("aa\nbb\ncˇc");
1868 }
1869
1870 #[gpui::test]
1871 async fn test_subword_motions(cx: &mut gpui::TestAppContext) {
1872 let mut cx = VimTestContext::new(cx, true).await;
1873 cx.update(|_, cx| {
1874 cx.bind_keys(vec![
1875 KeyBinding::new(
1876 "w",
1877 motion::NextSubwordStart {
1878 ignore_punctuation: false,
1879 },
1880 Some("Editor && VimControl && !VimWaiting && !menu"),
1881 ),
1882 KeyBinding::new(
1883 "b",
1884 motion::PreviousSubwordStart {
1885 ignore_punctuation: false,
1886 },
1887 Some("Editor && VimControl && !VimWaiting && !menu"),
1888 ),
1889 KeyBinding::new(
1890 "e",
1891 motion::NextSubwordEnd {
1892 ignore_punctuation: false,
1893 },
1894 Some("Editor && VimControl && !VimWaiting && !menu"),
1895 ),
1896 KeyBinding::new(
1897 "g e",
1898 motion::PreviousSubwordEnd {
1899 ignore_punctuation: false,
1900 },
1901 Some("Editor && VimControl && !VimWaiting && !menu"),
1902 ),
1903 ]);
1904 });
1905
1906 cx.assert_binding_normal("w", indoc! {"ˇassert_binding"}, indoc! {"assert_ˇbinding"});
1907 // Special case: In 'cw', 'w' acts like 'e'
1908 cx.assert_binding(
1909 "c w",
1910 indoc! {"ˇassert_binding"},
1911 Mode::Normal,
1912 indoc! {"ˇ_binding"},
1913 Mode::Insert,
1914 );
1915
1916 cx.assert_binding_normal("e", indoc! {"ˇassert_binding"}, indoc! {"asserˇt_binding"});
1917
1918 cx.assert_binding_normal("b", indoc! {"assert_ˇbinding"}, indoc! {"ˇassert_binding"});
1919
1920 cx.assert_binding_normal(
1921 "g e",
1922 indoc! {"assert_bindinˇg"},
1923 indoc! {"asserˇt_binding"},
1924 );
1925 }
1926
1927 #[gpui::test]
1928 async fn test_r(cx: &mut gpui::TestAppContext) {
1929 let mut cx = NeovimBackedTestContext::new(cx).await;
1930
1931 cx.set_shared_state("ˇhello\n").await;
1932 cx.simulate_shared_keystrokes("r -").await;
1933 cx.shared_state().await.assert_eq("ˇ-ello\n");
1934
1935 cx.set_shared_state("ˇhello\n").await;
1936 cx.simulate_shared_keystrokes("3 r -").await;
1937 cx.shared_state().await.assert_eq("--ˇ-lo\n");
1938
1939 cx.set_shared_state("ˇhello\n").await;
1940 cx.simulate_shared_keystrokes("r - 2 l .").await;
1941 cx.shared_state().await.assert_eq("-eˇ-lo\n");
1942
1943 cx.set_shared_state("ˇhello world\n").await;
1944 cx.simulate_shared_keystrokes("2 r - f w .").await;
1945 cx.shared_state().await.assert_eq("--llo -ˇ-rld\n");
1946
1947 cx.set_shared_state("ˇhello world\n").await;
1948 cx.simulate_shared_keystrokes("2 0 r - ").await;
1949 cx.shared_state().await.assert_eq("ˇhello world\n");
1950
1951 cx.set_shared_state(" helloˇ world\n").await;
1952 cx.simulate_shared_keystrokes("r enter").await;
1953 cx.shared_state().await.assert_eq(" hello\n ˇ world\n");
1954
1955 cx.set_shared_state(" helloˇ world\n").await;
1956 cx.simulate_shared_keystrokes("2 r enter").await;
1957 cx.shared_state().await.assert_eq(" hello\n ˇ orld\n");
1958 }
1959
1960 #[gpui::test]
1961 async fn test_gq(cx: &mut gpui::TestAppContext) {
1962 let mut cx = NeovimBackedTestContext::new(cx).await;
1963 cx.set_neovim_option("textwidth=5").await;
1964
1965 cx.update(|_, cx| {
1966 SettingsStore::update_global(cx, |settings, cx| {
1967 settings.update_user_settings(cx, |settings| {
1968 settings
1969 .project
1970 .all_languages
1971 .defaults
1972 .preferred_line_length = Some(5);
1973 });
1974 })
1975 });
1976
1977 cx.set_shared_state("ˇth th th th th th\n").await;
1978 cx.simulate_shared_keystrokes("g q q").await;
1979 cx.shared_state().await.assert_eq("th th\nth th\nˇth th\n");
1980
1981 cx.set_shared_state("ˇth th th th th th\nth th th th th th\n")
1982 .await;
1983 cx.simulate_shared_keystrokes("v j g q").await;
1984 cx.shared_state()
1985 .await
1986 .assert_eq("th th\nth th\nth th\nth th\nth th\nˇth th\n");
1987 }
1988
1989 #[gpui::test]
1990 async fn test_o_comment(cx: &mut gpui::TestAppContext) {
1991 let mut cx = NeovimBackedTestContext::new(cx).await;
1992 cx.set_neovim_option("filetype=rust").await;
1993
1994 cx.set_shared_state("// helloˇ\n").await;
1995 cx.simulate_shared_keystrokes("o").await;
1996 cx.shared_state().await.assert_eq("// hello\n// ˇ\n");
1997 cx.simulate_shared_keystrokes("x escape shift-o").await;
1998 cx.shared_state().await.assert_eq("// hello\n// ˇ\n// x\n");
1999 }
2000
2001 #[gpui::test]
2002 async fn test_yank_line_with_trailing_newline(cx: &mut gpui::TestAppContext) {
2003 let mut cx = NeovimBackedTestContext::new(cx).await;
2004 cx.set_shared_state("heˇllo\n").await;
2005 cx.simulate_shared_keystrokes("y y p").await;
2006 cx.shared_state().await.assert_eq("hello\nˇhello\n");
2007 }
2008
2009 #[gpui::test]
2010 async fn test_yank_line_without_trailing_newline(cx: &mut gpui::TestAppContext) {
2011 let mut cx = NeovimBackedTestContext::new(cx).await;
2012 cx.set_shared_state("heˇllo").await;
2013 cx.simulate_shared_keystrokes("y y p").await;
2014 cx.shared_state().await.assert_eq("hello\nˇhello");
2015 }
2016
2017 #[gpui::test]
2018 async fn test_yank_multiline_without_trailing_newline(cx: &mut gpui::TestAppContext) {
2019 let mut cx = NeovimBackedTestContext::new(cx).await;
2020 cx.set_shared_state("heˇllo\nhello").await;
2021 cx.simulate_shared_keystrokes("2 y y p").await;
2022 cx.shared_state()
2023 .await
2024 .assert_eq("hello\nˇhello\nhello\nhello");
2025 }
2026
2027 #[gpui::test]
2028 async fn test_dd_then_paste_without_trailing_newline(cx: &mut gpui::TestAppContext) {
2029 let mut cx = NeovimBackedTestContext::new(cx).await;
2030 cx.set_shared_state("heˇllo").await;
2031 cx.simulate_shared_keystrokes("d d").await;
2032 cx.shared_state().await.assert_eq("ˇ");
2033 cx.simulate_shared_keystrokes("p p").await;
2034 cx.shared_state().await.assert_eq("\nhello\nˇhello");
2035 }
2036
2037 #[gpui::test]
2038 async fn test_visual_mode_insert_before_after(cx: &mut gpui::TestAppContext) {
2039 let mut cx = NeovimBackedTestContext::new(cx).await;
2040
2041 cx.set_shared_state("heˇllo").await;
2042 cx.simulate_shared_keystrokes("v i w shift-i").await;
2043 cx.shared_state().await.assert_eq("ˇhello");
2044
2045 cx.set_shared_state(indoc! {"
2046 The quick brown
2047 fox ˇjumps over
2048 the lazy dog"})
2049 .await;
2050 cx.simulate_shared_keystrokes("shift-v shift-i").await;
2051 cx.shared_state().await.assert_eq(indoc! {"
2052 The quick brown
2053 ˇfox jumps over
2054 the lazy dog"});
2055
2056 cx.set_shared_state(indoc! {"
2057 The quick brown
2058 fox ˇjumps over
2059 the lazy dog"})
2060 .await;
2061 cx.simulate_shared_keystrokes("shift-v shift-a").await;
2062 cx.shared_state().await.assert_eq(indoc! {"
2063 The quick brown
2064 fox jˇumps over
2065 the lazy dog"});
2066 }
2067
2068 #[gpui::test]
2069 async fn test_jump_list(cx: &mut gpui::TestAppContext) {
2070 let mut cx = NeovimBackedTestContext::new(cx).await;
2071
2072 cx.set_shared_state(indoc! {"
2073 ˇfn a() { }
2074
2075
2076
2077
2078
2079 fn b() { }
2080
2081
2082
2083
2084
2085 fn b() { }"})
2086 .await;
2087 cx.simulate_shared_keystrokes("3 }").await;
2088 cx.shared_state().await.assert_matches();
2089 cx.simulate_shared_keystrokes("ctrl-o").await;
2090 cx.shared_state().await.assert_matches();
2091 cx.simulate_shared_keystrokes("ctrl-i").await;
2092 cx.shared_state().await.assert_matches();
2093 cx.simulate_shared_keystrokes("1 1 k").await;
2094 cx.shared_state().await.assert_matches();
2095 cx.simulate_shared_keystrokes("ctrl-o").await;
2096 cx.shared_state().await.assert_matches();
2097 }
2098
2099 #[gpui::test]
2100 async fn test_undo_last_line(cx: &mut gpui::TestAppContext) {
2101 let mut cx = NeovimBackedTestContext::new(cx).await;
2102
2103 cx.set_shared_state(indoc! {"
2104 ˇfn a() { }
2105 fn a() { }
2106 fn a() { }
2107 "})
2108 .await;
2109 // do a jump to reset vim's undo grouping
2110 cx.simulate_shared_keystrokes("shift-g").await;
2111 cx.shared_state().await.assert_matches();
2112 cx.simulate_shared_keystrokes("r a").await;
2113 cx.shared_state().await.assert_matches();
2114 cx.simulate_shared_keystrokes("shift-u").await;
2115 cx.shared_state().await.assert_matches();
2116 cx.simulate_shared_keystrokes("shift-u").await;
2117 cx.shared_state().await.assert_matches();
2118 cx.simulate_shared_keystrokes("g g shift-u").await;
2119 cx.shared_state().await.assert_matches();
2120 }
2121
2122 #[gpui::test]
2123 async fn test_undo_last_line_newline(cx: &mut gpui::TestAppContext) {
2124 let mut cx = NeovimBackedTestContext::new(cx).await;
2125
2126 cx.set_shared_state(indoc! {"
2127 ˇfn a() { }
2128 fn a() { }
2129 fn a() { }
2130 "})
2131 .await;
2132 // do a jump to reset vim's undo grouping
2133 cx.simulate_shared_keystrokes("shift-g k").await;
2134 cx.shared_state().await.assert_matches();
2135 cx.simulate_shared_keystrokes("o h e l l o escape").await;
2136 cx.shared_state().await.assert_matches();
2137 cx.simulate_shared_keystrokes("shift-u").await;
2138 cx.shared_state().await.assert_matches();
2139 cx.simulate_shared_keystrokes("shift-u").await;
2140 }
2141
2142 #[gpui::test]
2143 async fn test_undo_last_line_newline_many_changes(cx: &mut gpui::TestAppContext) {
2144 let mut cx = NeovimBackedTestContext::new(cx).await;
2145
2146 cx.set_shared_state(indoc! {"
2147 ˇfn a() { }
2148 fn a() { }
2149 fn a() { }
2150 "})
2151 .await;
2152 // do a jump to reset vim's undo grouping
2153 cx.simulate_shared_keystrokes("x shift-g k").await;
2154 cx.shared_state().await.assert_matches();
2155 cx.simulate_shared_keystrokes("x f a x f { x").await;
2156 cx.shared_state().await.assert_matches();
2157 cx.simulate_shared_keystrokes("shift-u").await;
2158 cx.shared_state().await.assert_matches();
2159 cx.simulate_shared_keystrokes("shift-u").await;
2160 cx.shared_state().await.assert_matches();
2161 cx.simulate_shared_keystrokes("shift-u").await;
2162 cx.shared_state().await.assert_matches();
2163 cx.simulate_shared_keystrokes("shift-u").await;
2164 cx.shared_state().await.assert_matches();
2165 }
2166
2167 #[gpui::test]
2168 async fn test_undo_last_line_multicursor(cx: &mut gpui::TestAppContext) {
2169 let mut cx = VimTestContext::new(cx, true).await;
2170
2171 cx.set_state(
2172 indoc! {"
2173 ˇone two ˇone
2174 two ˇone two
2175 "},
2176 Mode::Normal,
2177 );
2178 cx.simulate_keystrokes("3 r a");
2179 cx.assert_state(
2180 indoc! {"
2181 aaˇa two aaˇa
2182 two aaˇa two
2183 "},
2184 Mode::Normal,
2185 );
2186 cx.simulate_keystrokes("escape escape");
2187 cx.simulate_keystrokes("shift-u");
2188 cx.set_state(
2189 indoc! {"
2190 onˇe two onˇe
2191 two onˇe two
2192 "},
2193 Mode::Normal,
2194 );
2195 }
2196
2197 #[gpui::test]
2198 async fn test_go_to_tab_with_count(cx: &mut gpui::TestAppContext) {
2199 let mut cx = VimTestContext::new(cx, true).await;
2200
2201 // Open 4 tabs.
2202 cx.simulate_keystrokes(": tabnew");
2203 cx.simulate_keystrokes("enter");
2204 cx.simulate_keystrokes(": tabnew");
2205 cx.simulate_keystrokes("enter");
2206 cx.simulate_keystrokes(": tabnew");
2207 cx.simulate_keystrokes("enter");
2208 cx.workspace(|workspace, _, cx| {
2209 assert_eq!(workspace.items(cx).count(), 4);
2210 assert_eq!(workspace.active_pane().read(cx).active_item_index(), 3);
2211 });
2212
2213 cx.simulate_keystrokes("1 g t");
2214 cx.workspace(|workspace, _, cx| {
2215 assert_eq!(workspace.active_pane().read(cx).active_item_index(), 0);
2216 });
2217
2218 cx.simulate_keystrokes("3 g t");
2219 cx.workspace(|workspace, _, cx| {
2220 assert_eq!(workspace.active_pane().read(cx).active_item_index(), 2);
2221 });
2222
2223 cx.simulate_keystrokes("4 g t");
2224 cx.workspace(|workspace, _, cx| {
2225 assert_eq!(workspace.active_pane().read(cx).active_item_index(), 3);
2226 });
2227
2228 cx.simulate_keystrokes("1 g t");
2229 cx.simulate_keystrokes("g t");
2230 cx.workspace(|workspace, _, cx| {
2231 assert_eq!(workspace.active_pane().read(cx).active_item_index(), 1);
2232 });
2233 }
2234
2235 #[gpui::test]
2236 async fn test_go_to_previous_tab_with_count(cx: &mut gpui::TestAppContext) {
2237 let mut cx = VimTestContext::new(cx, true).await;
2238
2239 // Open 4 tabs.
2240 cx.simulate_keystrokes(": tabnew");
2241 cx.simulate_keystrokes("enter");
2242 cx.simulate_keystrokes(": tabnew");
2243 cx.simulate_keystrokes("enter");
2244 cx.simulate_keystrokes(": tabnew");
2245 cx.simulate_keystrokes("enter");
2246 cx.workspace(|workspace, _, cx| {
2247 assert_eq!(workspace.items(cx).count(), 4);
2248 assert_eq!(workspace.active_pane().read(cx).active_item_index(), 3);
2249 });
2250
2251 cx.simulate_keystrokes("2 g shift-t");
2252 cx.workspace(|workspace, _, cx| {
2253 assert_eq!(workspace.active_pane().read(cx).active_item_index(), 1);
2254 });
2255
2256 cx.simulate_keystrokes("g shift-t");
2257 cx.workspace(|workspace, _, cx| {
2258 assert_eq!(workspace.active_pane().read(cx).active_item_index(), 0);
2259 });
2260
2261 // Wraparound: gT from first tab should go to last.
2262 cx.simulate_keystrokes("g shift-t");
2263 cx.workspace(|workspace, _, cx| {
2264 assert_eq!(workspace.active_pane().read(cx).active_item_index(), 3);
2265 });
2266
2267 cx.simulate_keystrokes("6 g shift-t");
2268 cx.workspace(|workspace, _, cx| {
2269 assert_eq!(workspace.active_pane().read(cx).active_item_index(), 1);
2270 });
2271 }
2272}