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