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