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