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