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