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