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