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