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