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