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