1mod change;
2mod delete;
3mod yank;
4
5use std::borrow::Cow;
6
7use crate::{
8 motion::Motion,
9 object::Object,
10 state::{Mode, Operator},
11 Vim,
12};
13use collections::{HashMap, HashSet};
14use editor::{
15 display_map::ToDisplayPoint, Anchor, Autoscroll, Bias, ClipboardSelection, DisplayPoint,
16};
17use gpui::{actions, MutableAppContext, ViewContext};
18use language::{AutoindentMode, SelectionGoal};
19use rope::point::Point;
20use workspace::Workspace;
21
22use self::{
23 change::{change_motion, change_object},
24 delete::{delete_motion, delete_object},
25 yank::{yank_motion, yank_object},
26};
27
28actions!(
29 vim,
30 [
31 InsertAfter,
32 InsertFirstNonWhitespace,
33 InsertEndOfLine,
34 InsertLineAbove,
35 InsertLineBelow,
36 DeleteLeft,
37 DeleteRight,
38 ChangeToEndOfLine,
39 DeleteToEndOfLine,
40 Paste,
41 Yank,
42 ]
43);
44
45pub fn init(cx: &mut MutableAppContext) {
46 cx.add_action(insert_after);
47 cx.add_action(insert_first_non_whitespace);
48 cx.add_action(insert_end_of_line);
49 cx.add_action(insert_line_above);
50 cx.add_action(insert_line_below);
51 cx.add_action(|_: &mut Workspace, _: &DeleteLeft, cx| {
52 Vim::update(cx, |vim, cx| {
53 let times = vim.pop_number_operator(cx);
54 delete_motion(vim, Motion::Left, times, cx);
55 })
56 });
57 cx.add_action(|_: &mut Workspace, _: &DeleteRight, cx| {
58 Vim::update(cx, |vim, cx| {
59 let times = vim.pop_number_operator(cx);
60 delete_motion(vim, Motion::Right, times, cx);
61 })
62 });
63 cx.add_action(|_: &mut Workspace, _: &ChangeToEndOfLine, cx| {
64 Vim::update(cx, |vim, cx| {
65 let times = vim.pop_number_operator(cx);
66 change_motion(vim, Motion::EndOfLine, times, cx);
67 })
68 });
69 cx.add_action(|_: &mut Workspace, _: &DeleteToEndOfLine, cx| {
70 Vim::update(cx, |vim, cx| {
71 let times = vim.pop_number_operator(cx);
72 delete_motion(vim, Motion::EndOfLine, times, cx);
73 })
74 });
75 cx.add_action(paste);
76}
77
78pub fn normal_motion(
79 motion: Motion,
80 operator: Option<Operator>,
81 times: usize,
82 cx: &mut MutableAppContext,
83) {
84 Vim::update(cx, |vim, cx| {
85 match operator {
86 None => move_cursor(vim, motion, times, cx),
87 Some(Operator::Change) => change_motion(vim, motion, times, cx),
88 Some(Operator::Delete) => delete_motion(vim, motion, times, cx),
89 Some(Operator::Yank) => yank_motion(vim, motion, times, cx),
90 _ => {
91 // Can't do anything for text objects or namespace operators. Ignoring
92 }
93 }
94 });
95}
96
97pub fn normal_object(object: Object, cx: &mut MutableAppContext) {
98 Vim::update(cx, |vim, cx| {
99 match vim.state.operator_stack.pop() {
100 Some(Operator::Object { around }) => match vim.state.operator_stack.pop() {
101 Some(Operator::Change) => change_object(vim, object, around, cx),
102 Some(Operator::Delete) => delete_object(vim, object, around, cx),
103 Some(Operator::Yank) => yank_object(vim, object, around, cx),
104 _ => {
105 // Can't do anything for namespace operators. Ignoring
106 }
107 },
108 _ => {
109 // Can't do anything with change/delete/yank and text objects. Ignoring
110 }
111 }
112 vim.clear_operator(cx);
113 })
114}
115
116fn move_cursor(vim: &mut Vim, motion: Motion, times: usize, cx: &mut MutableAppContext) {
117 vim.update_active_editor(cx, |editor, cx| {
118 editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
119 s.move_cursors_with(|map, cursor, goal| motion.move_point(map, cursor, goal, times))
120 })
121 });
122}
123
124fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext<Workspace>) {
125 Vim::update(cx, |vim, cx| {
126 vim.switch_mode(Mode::Insert, false, cx);
127 vim.update_active_editor(cx, |editor, cx| {
128 editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
129 s.move_cursors_with(|map, cursor, goal| {
130 Motion::Right.move_point(map, cursor, goal, 1)
131 });
132 });
133 });
134 });
135}
136
137fn insert_first_non_whitespace(
138 _: &mut Workspace,
139 _: &InsertFirstNonWhitespace,
140 cx: &mut ViewContext<Workspace>,
141) {
142 Vim::update(cx, |vim, cx| {
143 vim.switch_mode(Mode::Insert, false, cx);
144 vim.update_active_editor(cx, |editor, cx| {
145 editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
146 s.move_cursors_with(|map, cursor, goal| {
147 Motion::FirstNonWhitespace.move_point(map, cursor, goal, 1)
148 });
149 });
150 });
151 });
152}
153
154fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewContext<Workspace>) {
155 Vim::update(cx, |vim, cx| {
156 vim.switch_mode(Mode::Insert, false, cx);
157 vim.update_active_editor(cx, |editor, cx| {
158 editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
159 s.move_cursors_with(|map, cursor, goal| {
160 Motion::EndOfLine.move_point(map, cursor, goal, 1)
161 });
162 });
163 });
164 });
165}
166
167fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContext<Workspace>) {
168 Vim::update(cx, |vim, cx| {
169 vim.switch_mode(Mode::Insert, false, cx);
170 vim.update_active_editor(cx, |editor, cx| {
171 editor.transact(cx, |editor, cx| {
172 let (map, old_selections) = editor.selections.all_display(cx);
173 let selection_start_rows: HashSet<u32> = old_selections
174 .into_iter()
175 .map(|selection| selection.start.row())
176 .collect();
177 let edits = selection_start_rows.into_iter().map(|row| {
178 let (indent, _) = map.line_indent(row);
179 let start_of_line = map
180 .clip_point(DisplayPoint::new(row, 0), Bias::Left)
181 .to_point(&map);
182 let mut new_text = " ".repeat(indent as usize);
183 new_text.push('\n');
184 (start_of_line..start_of_line, new_text)
185 });
186 editor.edit_with_autoindent(edits, cx);
187 editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
188 s.move_cursors_with(|map, mut cursor, _| {
189 *cursor.row_mut() -= 1;
190 *cursor.column_mut() = map.line_len(cursor.row());
191 (map.clip_point(cursor, Bias::Left), SelectionGoal::None)
192 });
193 });
194 });
195 });
196 });
197}
198
199fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContext<Workspace>) {
200 Vim::update(cx, |vim, cx| {
201 vim.switch_mode(Mode::Insert, false, cx);
202 vim.update_active_editor(cx, |editor, cx| {
203 editor.transact(cx, |editor, cx| {
204 let (map, old_selections) = editor.selections.all_display(cx);
205 let selection_end_rows: HashSet<u32> = old_selections
206 .into_iter()
207 .map(|selection| selection.end.row())
208 .collect();
209 let edits = selection_end_rows.into_iter().map(|row| {
210 let (indent, _) = map.line_indent(row);
211 let end_of_line = map
212 .clip_point(DisplayPoint::new(row, map.line_len(row)), Bias::Left)
213 .to_point(&map);
214 let mut new_text = "\n".to_string();
215 new_text.push_str(&" ".repeat(indent as usize));
216 (end_of_line..end_of_line, new_text)
217 });
218 editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
219 s.move_cursors_with(|map, cursor, goal| {
220 Motion::EndOfLine.move_point(map, cursor, goal, 1)
221 });
222 });
223 editor.edit_with_autoindent(edits, cx);
224 });
225 });
226 });
227}
228
229fn paste(_: &mut Workspace, _: &Paste, cx: &mut ViewContext<Workspace>) {
230 Vim::update(cx, |vim, cx| {
231 vim.update_active_editor(cx, |editor, cx| {
232 editor.transact(cx, |editor, cx| {
233 editor.set_clip_at_line_ends(false, cx);
234 if let Some(item) = cx.as_mut().read_from_clipboard() {
235 let mut clipboard_text = Cow::Borrowed(item.text());
236 if let Some(mut clipboard_selections) =
237 item.metadata::<Vec<ClipboardSelection>>()
238 {
239 let (display_map, selections) = editor.selections.all_display(cx);
240 let all_selections_were_entire_line =
241 clipboard_selections.iter().all(|s| s.is_entire_line);
242 if clipboard_selections.len() != selections.len() {
243 let mut newline_separated_text = String::new();
244 let mut clipboard_selections =
245 clipboard_selections.drain(..).peekable();
246 let mut ix = 0;
247 while let Some(clipboard_selection) = clipboard_selections.next() {
248 newline_separated_text
249 .push_str(&clipboard_text[ix..ix + clipboard_selection.len]);
250 ix += clipboard_selection.len;
251 if clipboard_selections.peek().is_some() {
252 newline_separated_text.push('\n');
253 }
254 }
255 clipboard_text = Cow::Owned(newline_separated_text);
256 }
257
258 // If the pasted text is a single line, the cursor should be placed after
259 // the newly pasted text. This is easiest done with an anchor after the
260 // insertion, and then with a fixup to move the selection back one position.
261 // However if the pasted text is linewise, the cursor should be placed at the start
262 // of the new text on the following line. This is easiest done with a manually adjusted
263 // point.
264 // This enum lets us represent both cases
265 enum NewPosition {
266 Inside(Point),
267 After(Anchor),
268 }
269 let mut new_selections: HashMap<usize, NewPosition> = Default::default();
270 editor.buffer().update(cx, |buffer, cx| {
271 let snapshot = buffer.snapshot(cx);
272 let mut start_offset = 0;
273 let mut edits = Vec::new();
274 for (ix, selection) in selections.iter().enumerate() {
275 let to_insert;
276 let linewise;
277 if let Some(clipboard_selection) = clipboard_selections.get(ix) {
278 let end_offset = start_offset + clipboard_selection.len;
279 to_insert = &clipboard_text[start_offset..end_offset];
280 linewise = clipboard_selection.is_entire_line;
281 start_offset = end_offset;
282 } else {
283 to_insert = clipboard_text.as_str();
284 linewise = all_selections_were_entire_line;
285 }
286
287 // If the clipboard text was copied linewise, and the current selection
288 // is empty, then paste the text after this line and move the selection
289 // to the start of the pasted text
290 let insert_at = if linewise {
291 let (point, _) = display_map
292 .next_line_boundary(selection.start.to_point(&display_map));
293
294 if !to_insert.starts_with('\n') {
295 // Add newline before pasted text so that it shows up
296 edits.push((point..point, "\n"));
297 }
298 // Drop selection at the start of the next line
299 new_selections.insert(
300 selection.id,
301 NewPosition::Inside(Point::new(point.row + 1, 0)),
302 );
303 point
304 } else {
305 let mut point = selection.end;
306 // Paste the text after the current selection
307 *point.column_mut() = point.column() + 1;
308 let point = display_map
309 .clip_point(point, Bias::Right)
310 .to_point(&display_map);
311
312 new_selections.insert(
313 selection.id,
314 if to_insert.contains('\n') {
315 NewPosition::Inside(point)
316 } else {
317 NewPosition::After(snapshot.anchor_after(point))
318 },
319 );
320 point
321 };
322
323 if linewise && to_insert.ends_with('\n') {
324 edits.push((
325 insert_at..insert_at,
326 &to_insert[0..to_insert.len().saturating_sub(1)],
327 ))
328 } else {
329 edits.push((insert_at..insert_at, to_insert));
330 }
331 }
332 drop(snapshot);
333 buffer.edit(edits, Some(AutoindentMode::EachLine), cx);
334 });
335
336 editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
337 s.move_with(|map, selection| {
338 if let Some(new_position) = new_selections.get(&selection.id) {
339 match new_position {
340 NewPosition::Inside(new_point) => {
341 selection.collapse_to(
342 new_point.to_display_point(map),
343 SelectionGoal::None,
344 );
345 }
346 NewPosition::After(after_point) => {
347 let mut new_point = after_point.to_display_point(map);
348 *new_point.column_mut() =
349 new_point.column().saturating_sub(1);
350 new_point = map.clip_point(new_point, Bias::Left);
351 selection.collapse_to(new_point, SelectionGoal::None);
352 }
353 }
354 }
355 });
356 });
357 } else {
358 editor.insert(&clipboard_text, cx);
359 }
360 }
361 editor.set_clip_at_line_ends(true, cx);
362 });
363 });
364 });
365}
366
367#[cfg(test)]
368mod test {
369 use indoc::indoc;
370
371 use crate::{
372 state::{
373 Mode::{self, *},
374 Namespace, Operator,
375 },
376 test::{NeovimBackedTestContext, VimTestContext},
377 };
378
379 #[gpui::test]
380 async fn test_h(cx: &mut gpui::TestAppContext) {
381 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["h"]);
382 cx.assert_all(indoc! {"
383 ˇThe qˇuick
384 ˇbrown"
385 })
386 .await;
387 }
388
389 #[gpui::test]
390 async fn test_backspace(cx: &mut gpui::TestAppContext) {
391 let mut cx = NeovimBackedTestContext::new(cx)
392 .await
393 .binding(["backspace"]);
394 cx.assert_all(indoc! {"
395 ˇThe qˇuick
396 ˇbrown"
397 })
398 .await;
399 }
400
401 #[gpui::test]
402 async fn test_j(cx: &mut gpui::TestAppContext) {
403 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["j"]);
404 cx.assert_all(indoc! {"
405 ˇThe qˇuick broˇwn
406 ˇfox jumps"
407 })
408 .await;
409 }
410
411 #[gpui::test]
412 async fn test_k(cx: &mut gpui::TestAppContext) {
413 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["k"]);
414 cx.assert_all(indoc! {"
415 ˇThe qˇuick
416 ˇbrown fˇox jumˇps"
417 })
418 .await;
419 }
420
421 #[gpui::test]
422 async fn test_l(cx: &mut gpui::TestAppContext) {
423 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["l"]);
424 cx.assert_all(indoc! {"
425 ˇThe qˇuicˇk
426 ˇbrowˇn"})
427 .await;
428 }
429
430 #[gpui::test]
431 async fn test_jump_to_line_boundaries(cx: &mut gpui::TestAppContext) {
432 let mut cx = NeovimBackedTestContext::new(cx).await;
433 cx.assert_binding_matches_all(
434 ["$"],
435 indoc! {"
436 ˇThe qˇuicˇk
437 ˇbrowˇn"},
438 )
439 .await;
440 cx.assert_binding_matches_all(
441 ["0"],
442 indoc! {"
443 ˇThe qˇuicˇk
444 ˇbrowˇn"},
445 )
446 .await;
447 }
448
449 #[gpui::test]
450 async fn test_jump_to_end(cx: &mut gpui::TestAppContext) {
451 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-g"]);
452
453 cx.assert_all(indoc! {"
454 The ˇquick
455
456 brown fox jumps
457 overˇ the lazy doˇg"})
458 .await;
459 cx.assert(indoc! {"
460 The quiˇck
461
462 brown"})
463 .await;
464 cx.assert(indoc! {"
465 The quiˇck
466
467 "})
468 .await;
469 }
470
471 #[gpui::test]
472 async fn test_w(cx: &mut gpui::TestAppContext) {
473 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["w"]);
474 cx.assert_all(indoc! {"
475 The ˇquickˇ-ˇbrown
476 ˇ
477 ˇ
478 ˇfox_jumps ˇover
479 ˇthˇe"})
480 .await;
481 let mut cx = cx.binding(["shift-w"]);
482 cx.assert_all(indoc! {"
483 The ˇquickˇ-ˇbrown
484 ˇ
485 ˇ
486 ˇfox_jumps ˇover
487 ˇthˇe"})
488 .await;
489 }
490
491 #[gpui::test]
492 async fn test_e(cx: &mut gpui::TestAppContext) {
493 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["e"]);
494 cx.assert_all(indoc! {"
495 Thˇe quicˇkˇ-browˇn
496
497
498 fox_jumpˇs oveˇr
499 thˇe"})
500 .await;
501 let mut cx = cx.binding(["shift-e"]);
502 cx.assert_all(indoc! {"
503 Thˇe quicˇkˇ-browˇn
504
505
506 fox_jumpˇs oveˇr
507 thˇe"})
508 .await;
509 }
510
511 #[gpui::test]
512 async fn test_b(cx: &mut gpui::TestAppContext) {
513 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["b"]);
514 cx.assert_all(indoc! {"
515 ˇThe ˇquickˇ-ˇbrown
516 ˇ
517 ˇ
518 ˇfox_jumps ˇover
519 ˇthe"})
520 .await;
521 let mut cx = cx.binding(["shift-b"]);
522 cx.assert_all(indoc! {"
523 ˇThe ˇquickˇ-ˇbrown
524 ˇ
525 ˇ
526 ˇfox_jumps ˇover
527 ˇthe"})
528 .await;
529 }
530
531 #[gpui::test]
532 async fn test_g_prefix_and_abort(cx: &mut gpui::TestAppContext) {
533 let mut cx = VimTestContext::new(cx, true).await;
534
535 // Can abort with escape to get back to normal mode
536 cx.simulate_keystroke("g");
537 assert_eq!(cx.mode(), Normal);
538 assert_eq!(
539 cx.active_operator(),
540 Some(Operator::Namespace(Namespace::G))
541 );
542 cx.simulate_keystroke("escape");
543 assert_eq!(cx.mode(), Normal);
544 assert_eq!(cx.active_operator(), None);
545 }
546
547 #[gpui::test]
548 async fn test_gg(cx: &mut gpui::TestAppContext) {
549 let mut cx = NeovimBackedTestContext::new(cx).await;
550 cx.assert_binding_matches_all(
551 ["g", "g"],
552 indoc! {"
553 The qˇuick
554
555 brown fox jumps
556 over ˇthe laˇzy dog"},
557 )
558 .await;
559 cx.assert_binding_matches(
560 ["g", "g"],
561 indoc! {"
562
563
564 brown fox jumps
565 over the laˇzy dog"},
566 )
567 .await;
568 cx.assert_binding_matches(
569 ["2", "g", "g"],
570 indoc! {"
571
572
573 brown fox juˇmps
574 over the lazydog"},
575 )
576 .await;
577 }
578
579 #[gpui::test]
580 async fn test_a(cx: &mut gpui::TestAppContext) {
581 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["a"]);
582 cx.assert_all("The qˇuicˇk").await;
583 }
584
585 #[gpui::test]
586 async fn test_insert_end_of_line(cx: &mut gpui::TestAppContext) {
587 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-a"]);
588 cx.assert_all(indoc! {"
589 ˇ
590 The qˇuick
591 brown ˇfox "})
592 .await;
593 }
594
595 #[gpui::test]
596 async fn test_jump_to_first_non_whitespace(cx: &mut gpui::TestAppContext) {
597 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["^"]);
598 cx.assert("The qˇuick").await;
599 cx.assert(" The qˇuick").await;
600 cx.assert("ˇ").await;
601 cx.assert(indoc! {"
602 The qˇuick
603 brown fox"})
604 .await;
605 cx.assert(indoc! {"
606 ˇ
607 The quick"})
608 .await;
609 // Indoc disallows trailing whitspace.
610 cx.assert(" ˇ \nThe quick").await;
611 }
612
613 #[gpui::test]
614 async fn test_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
615 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-i"]);
616 cx.assert("The qˇuick").await;
617 cx.assert(" The qˇuick").await;
618 cx.assert("ˇ").await;
619 cx.assert(indoc! {"
620 The qˇuick
621 brown fox"})
622 .await;
623 cx.assert(indoc! {"
624 ˇ
625 The quick"})
626 .await;
627 }
628
629 #[gpui::test]
630 async fn test_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
631 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-d"]);
632 cx.assert(indoc! {"
633 The qˇuick
634 brown fox"})
635 .await;
636 cx.assert(indoc! {"
637 The quick
638 ˇ
639 brown fox"})
640 .await;
641 }
642
643 #[gpui::test]
644 async fn test_x(cx: &mut gpui::TestAppContext) {
645 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["x"]);
646 cx.assert_all("ˇTeˇsˇt").await;
647 cx.assert(indoc! {"
648 Tesˇt
649 test"})
650 .await;
651 }
652
653 #[gpui::test]
654 async fn test_delete_left(cx: &mut gpui::TestAppContext) {
655 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-x"]);
656 cx.assert_all("ˇTˇeˇsˇt").await;
657 cx.assert(indoc! {"
658 Test
659 ˇtest"})
660 .await;
661 }
662
663 #[gpui::test]
664 async fn test_o(cx: &mut gpui::TestAppContext) {
665 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["o"]);
666 cx.assert("ˇ").await;
667 cx.assert("The ˇquick").await;
668 cx.assert_all(indoc! {"
669 The qˇuick
670 brown ˇfox
671 jumps ˇover"})
672 .await;
673 cx.assert(indoc! {"
674 The quick
675 ˇ
676 brown fox"})
677 .await;
678 cx.assert(indoc! {"
679 fn test() {
680 println!(ˇ);
681 }
682 "})
683 .await;
684 cx.assert(indoc! {"
685 fn test(ˇ) {
686 println!();
687 }"})
688 .await;
689 }
690
691 #[gpui::test]
692 async fn test_insert_line_above(cx: &mut gpui::TestAppContext) {
693 let cx = NeovimBackedTestContext::new(cx).await;
694 let mut cx = cx.binding(["shift-o"]);
695 cx.assert("ˇ").await;
696 cx.assert("The ˇquick").await;
697 cx.assert_all(indoc! {"
698 The qˇuick
699 brown ˇfox
700 jumps ˇover"})
701 .await;
702 cx.assert(indoc! {"
703 The quick
704 ˇ
705 brown fox"})
706 .await;
707
708 // Our indentation is smarter than vims. So we don't match here
709 cx.assert_manual(
710 indoc! {"
711 fn test()
712 println!(ˇ);"},
713 Mode::Normal,
714 indoc! {"
715 fn test()
716 ˇ
717 println!();"},
718 Mode::Insert,
719 );
720 cx.assert_manual(
721 indoc! {"
722 fn test(ˇ) {
723 println!();
724 }"},
725 Mode::Normal,
726 indoc! {"
727 ˇ
728 fn test() {
729 println!();
730 }"},
731 Mode::Insert,
732 );
733 }
734
735 #[gpui::test]
736 async fn test_dd(cx: &mut gpui::TestAppContext) {
737 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "d"]);
738 cx.assert("ˇ").await;
739 cx.assert("The ˇquick").await;
740 cx.assert_all(indoc! {"
741 The qˇuick
742 brown ˇfox
743 jumps ˇover"})
744 .await;
745 cx.assert(indoc! {"
746 The quick
747 ˇ
748 brown fox"})
749 .await;
750 }
751
752 #[gpui::test]
753 async fn test_cc(cx: &mut gpui::TestAppContext) {
754 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "c"]);
755 cx.assert("ˇ").await;
756 cx.assert("The ˇquick").await;
757 cx.assert_all(indoc! {"
758 The quˇick
759 brown ˇfox
760 jumps ˇover"})
761 .await;
762 cx.assert(indoc! {"
763 The quick
764 ˇ
765 brown fox"})
766 .await;
767 }
768
769 #[gpui::test]
770 async fn test_p(cx: &mut gpui::TestAppContext) {
771 let mut cx = NeovimBackedTestContext::new(cx).await;
772 cx.set_shared_state(indoc! {"
773 The quick brown
774 fox juˇmps over
775 the lazy dog"})
776 .await;
777
778 cx.simulate_shared_keystrokes(["d", "d"]).await;
779 cx.assert_state_matches().await;
780
781 cx.simulate_shared_keystroke("p").await;
782 cx.assert_state_matches().await;
783
784 cx.set_shared_state(indoc! {"
785 The quick brown
786 fox ˇjumps over
787 the lazy dog"})
788 .await;
789 cx.simulate_shared_keystrokes(["v", "w", "y"]).await;
790 cx.set_shared_state(indoc! {"
791 The quick brown
792 fox jumps oveˇr
793 the lazy dog"})
794 .await;
795 cx.simulate_shared_keystroke("p").await;
796 cx.assert_state_matches().await;
797 }
798
799 #[gpui::test]
800 async fn test_repeated_word(cx: &mut gpui::TestAppContext) {
801 let mut cx = NeovimBackedTestContext::new(cx).await;
802
803 for count in 1..=5 {
804 cx.assert_binding_matches_all(
805 [&count.to_string(), "w"],
806 indoc! {"
807 ˇThe quˇickˇ browˇn
808 ˇ
809 ˇfox ˇjumpsˇ-ˇoˇver
810 ˇthe lazy dog
811 "},
812 )
813 .await;
814 }
815 }
816}