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 jumps
573 over the lazydog"},
574 )
575 .await;
576 }
577
578 #[gpui::test]
579 async fn test_end_of_document(cx: &mut gpui::TestAppContext) {
580 let mut cx = NeovimBackedTestContext::new(cx).await;
581 cx.assert_binding_matches_all(
582 ["shift-g"],
583 indoc! {"
584 The qˇuick
585
586 brown fox jumps
587 over ˇthe laˇzy dog"},
588 )
589 .await;
590 cx.assert_binding_matches(
591 ["shift-g"],
592 indoc! {"
593
594
595 brown fox jumps
596 over the laˇzy dog"},
597 )
598 .await;
599 cx.assert_binding_matches(
600 ["2", "shift-g"],
601 indoc! {"
602 ˇ
603
604 brown fox jumps
605 over the lazydog"},
606 )
607 .await;
608 }
609
610 #[gpui::test]
611 async fn test_a(cx: &mut gpui::TestAppContext) {
612 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["a"]);
613 cx.assert_all("The qˇuicˇk").await;
614 }
615
616 #[gpui::test]
617 async fn test_insert_end_of_line(cx: &mut gpui::TestAppContext) {
618 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-a"]);
619 cx.assert_all(indoc! {"
620 ˇ
621 The qˇuick
622 brown ˇfox "})
623 .await;
624 }
625
626 #[gpui::test]
627 async fn test_jump_to_first_non_whitespace(cx: &mut gpui::TestAppContext) {
628 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["^"]);
629 cx.assert("The qˇuick").await;
630 cx.assert(" The qˇuick").await;
631 cx.assert("ˇ").await;
632 cx.assert(indoc! {"
633 The qˇuick
634 brown fox"})
635 .await;
636 cx.assert(indoc! {"
637 ˇ
638 The quick"})
639 .await;
640 // Indoc disallows trailing whitspace.
641 cx.assert(" ˇ \nThe quick").await;
642 }
643
644 #[gpui::test]
645 async fn test_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
646 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-i"]);
647 cx.assert("The qˇuick").await;
648 cx.assert(" The qˇuick").await;
649 cx.assert("ˇ").await;
650 cx.assert(indoc! {"
651 The qˇuick
652 brown fox"})
653 .await;
654 cx.assert(indoc! {"
655 ˇ
656 The quick"})
657 .await;
658 }
659
660 #[gpui::test]
661 async fn test_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
662 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-d"]);
663 cx.assert(indoc! {"
664 The qˇuick
665 brown fox"})
666 .await;
667 cx.assert(indoc! {"
668 The quick
669 ˇ
670 brown fox"})
671 .await;
672 }
673
674 #[gpui::test]
675 async fn test_x(cx: &mut gpui::TestAppContext) {
676 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["x"]);
677 cx.assert_all("ˇTeˇsˇt").await;
678 cx.assert(indoc! {"
679 Tesˇt
680 test"})
681 .await;
682 }
683
684 #[gpui::test]
685 async fn test_delete_left(cx: &mut gpui::TestAppContext) {
686 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-x"]);
687 cx.assert_all("ˇTˇeˇsˇt").await;
688 cx.assert(indoc! {"
689 Test
690 ˇtest"})
691 .await;
692 }
693
694 #[gpui::test]
695 async fn test_o(cx: &mut gpui::TestAppContext) {
696 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["o"]);
697 cx.assert("ˇ").await;
698 cx.assert("The ˇquick").await;
699 cx.assert_all(indoc! {"
700 The qˇuick
701 brown ˇfox
702 jumps ˇover"})
703 .await;
704 cx.assert(indoc! {"
705 The quick
706 ˇ
707 brown fox"})
708 .await;
709 cx.assert(indoc! {"
710 fn test() {
711 println!(ˇ);
712 }
713 "})
714 .await;
715 cx.assert(indoc! {"
716 fn test(ˇ) {
717 println!();
718 }"})
719 .await;
720 }
721
722 #[gpui::test]
723 async fn test_insert_line_above(cx: &mut gpui::TestAppContext) {
724 let cx = NeovimBackedTestContext::new(cx).await;
725 let mut cx = cx.binding(["shift-o"]);
726 cx.assert("ˇ").await;
727 cx.assert("The ˇquick").await;
728 cx.assert_all(indoc! {"
729 The qˇuick
730 brown ˇfox
731 jumps ˇover"})
732 .await;
733 cx.assert(indoc! {"
734 The quick
735 ˇ
736 brown fox"})
737 .await;
738
739 // Our indentation is smarter than vims. So we don't match here
740 cx.assert_manual(
741 indoc! {"
742 fn test()
743 println!(ˇ);"},
744 Mode::Normal,
745 indoc! {"
746 fn test()
747 ˇ
748 println!();"},
749 Mode::Insert,
750 );
751 cx.assert_manual(
752 indoc! {"
753 fn test(ˇ) {
754 println!();
755 }"},
756 Mode::Normal,
757 indoc! {"
758 ˇ
759 fn test() {
760 println!();
761 }"},
762 Mode::Insert,
763 );
764 }
765
766 #[gpui::test]
767 async fn test_dd(cx: &mut gpui::TestAppContext) {
768 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "d"]);
769 cx.assert("ˇ").await;
770 cx.assert("The ˇquick").await;
771 cx.assert_all(indoc! {"
772 The qˇuick
773 brown ˇfox
774 jumps ˇover"})
775 .await;
776 cx.assert_exempted(
777 indoc! {"
778 The quick
779 ˇ
780 brown fox"},
781 ExemptionFeatures::DeletionOnEmptyLine,
782 )
783 .await;
784 }
785
786 #[gpui::test]
787 async fn test_cc(cx: &mut gpui::TestAppContext) {
788 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "c"]);
789 cx.assert("ˇ").await;
790 cx.assert("The ˇquick").await;
791 cx.assert_all(indoc! {"
792 The quˇick
793 brown ˇfox
794 jumps ˇover"})
795 .await;
796 cx.assert(indoc! {"
797 The quick
798 ˇ
799 brown fox"})
800 .await;
801 }
802
803 #[gpui::test]
804 async fn test_p(cx: &mut gpui::TestAppContext) {
805 let mut cx = NeovimBackedTestContext::new(cx).await;
806 cx.set_shared_state(indoc! {"
807 The quick brown
808 fox juˇmps over
809 the lazy dog"})
810 .await;
811
812 cx.simulate_shared_keystrokes(["d", "d"]).await;
813 cx.assert_state_matches().await;
814
815 cx.simulate_shared_keystroke("p").await;
816 cx.assert_state_matches().await;
817
818 cx.set_shared_state(indoc! {"
819 The quick brown
820 fox ˇjumps over
821 the lazy dog"})
822 .await;
823 cx.simulate_shared_keystrokes(["v", "w", "y"]).await;
824 cx.set_shared_state(indoc! {"
825 The quick brown
826 fox jumps oveˇr
827 the lazy dog"})
828 .await;
829 cx.simulate_shared_keystroke("p").await;
830 cx.assert_state_matches().await;
831 }
832
833 #[gpui::test]
834 async fn test_repeated_word(cx: &mut gpui::TestAppContext) {
835 let mut cx = NeovimBackedTestContext::new(cx).await;
836
837 for count in 1..=5 {
838 cx.assert_binding_matches_all(
839 [&count.to_string(), "w"],
840 indoc! {"
841 ˇThe quˇickˇ browˇn
842 ˇ
843 ˇfox ˇjumpsˇ-ˇoˇver
844 ˇthe lazy dog
845 "},
846 )
847 .await;
848 }
849 }
850}