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