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