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