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