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