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