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