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