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