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