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