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