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