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