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