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