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