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