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