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
427pub(crate) fn normal_replace(text: &str, cx: &mut MutableAppContext) {
428 Vim::update(cx, |vim, cx| {
429 vim.update_active_editor(cx, |editor, cx| {
430 editor.transact(cx, |editor, cx| {
431 editor.set_clip_at_line_ends(false, cx);
432 let (map, display_selections) = editor.selections.all_display(cx);
433 // Selections are biased right at the start. So we need to store
434 // anchors that are biased left so that we can restore the selections
435 // after the change
436 let stable_anchors = editor
437 .selections
438 .disjoint_anchors()
439 .into_iter()
440 .map(|selection| {
441 let start = selection.start.bias_left(&map.buffer_snapshot);
442 start..start
443 })
444 .collect::<Vec<_>>();
445
446 let edits = display_selections
447 .into_iter()
448 .map(|selection| {
449 let mut range = selection.range();
450 *range.end.column_mut() += 1;
451 range.end = map.clip_point(range.end, Bias::Right);
452
453 (
454 range.start.to_offset(&map, Bias::Left)
455 ..range.end.to_offset(&map, Bias::Left),
456 text,
457 )
458 })
459 .collect::<Vec<_>>();
460
461 editor.buffer().update(cx, |buffer, cx| {
462 buffer.edit(edits, None, cx);
463 });
464 editor.set_clip_at_line_ends(true, cx);
465 editor.change_selections(None, cx, |s| {
466 s.select_anchor_ranges(stable_anchors);
467 });
468 });
469 });
470 vim.pop_operator(cx)
471 });
472}
473
474#[cfg(test)]
475mod test {
476 use indoc::indoc;
477
478 use crate::{
479 state::{
480 Mode::{self, *},
481 Namespace, Operator,
482 },
483 test::{ExemptionFeatures, NeovimBackedTestContext, VimTestContext},
484 };
485
486 #[gpui::test]
487 async fn test_h(cx: &mut gpui::TestAppContext) {
488 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["h"]);
489 cx.assert_all(indoc! {"
490 ˇThe qˇuick
491 ˇbrown"
492 })
493 .await;
494 }
495
496 #[gpui::test]
497 async fn test_backspace(cx: &mut gpui::TestAppContext) {
498 let mut cx = NeovimBackedTestContext::new(cx)
499 .await
500 .binding(["backspace"]);
501 cx.assert_all(indoc! {"
502 ˇThe qˇuick
503 ˇbrown"
504 })
505 .await;
506 }
507
508 #[gpui::test]
509 async fn test_j(cx: &mut gpui::TestAppContext) {
510 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["j"]);
511 cx.assert_all(indoc! {"
512 ˇThe qˇuick broˇwn
513 ˇfox jumps"
514 })
515 .await;
516 }
517
518 // #[gpui::test]
519 // async fn test_enter(cx: &mut gpui::TestAppContext) {
520 // let mut cx = NeovimBackedTestContext::new(cx).await.binding(["enter"]);
521 // cx.assert_all(indoc! {"
522 // ˇThe qˇuick broˇwn
523 // ˇfox jumps"
524 // })
525 // .await;
526 // }
527
528 #[gpui::test]
529 async fn test_k(cx: &mut gpui::TestAppContext) {
530 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["k"]);
531 cx.assert_all(indoc! {"
532 ˇThe qˇuick
533 ˇbrown fˇox jumˇps"
534 })
535 .await;
536 }
537
538 #[gpui::test]
539 async fn test_l(cx: &mut gpui::TestAppContext) {
540 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["l"]);
541 cx.assert_all(indoc! {"
542 ˇThe qˇuicˇk
543 ˇbrowˇn"})
544 .await;
545 }
546
547 #[gpui::test]
548 async fn test_jump_to_line_boundaries(cx: &mut gpui::TestAppContext) {
549 let mut cx = NeovimBackedTestContext::new(cx).await;
550 cx.assert_binding_matches_all(
551 ["$"],
552 indoc! {"
553 ˇThe qˇuicˇk
554 ˇbrowˇn"},
555 )
556 .await;
557 cx.assert_binding_matches_all(
558 ["0"],
559 indoc! {"
560 ˇThe qˇuicˇk
561 ˇbrowˇn"},
562 )
563 .await;
564 }
565
566 #[gpui::test]
567 async fn test_jump_to_end(cx: &mut gpui::TestAppContext) {
568 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-g"]);
569
570 cx.assert_all(indoc! {"
571 The ˇquick
572
573 brown fox jumps
574 overˇ the lazy doˇg"})
575 .await;
576 cx.assert(indoc! {"
577 The quiˇck
578
579 brown"})
580 .await;
581 cx.assert(indoc! {"
582 The quiˇck
583
584 "})
585 .await;
586 }
587
588 #[gpui::test]
589 async fn test_w(cx: &mut gpui::TestAppContext) {
590 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["w"]);
591 cx.assert_all(indoc! {"
592 The ˇquickˇ-ˇbrown
593 ˇ
594 ˇ
595 ˇfox_jumps ˇover
596 ˇthˇe"})
597 .await;
598 let mut cx = cx.binding(["shift-w"]);
599 cx.assert_all(indoc! {"
600 The ˇquickˇ-ˇbrown
601 ˇ
602 ˇ
603 ˇfox_jumps ˇover
604 ˇthˇe"})
605 .await;
606 }
607
608 #[gpui::test]
609 async fn test_e(cx: &mut gpui::TestAppContext) {
610 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["e"]);
611 cx.assert_all(indoc! {"
612 Thˇe quicˇkˇ-browˇn
613
614
615 fox_jumpˇs oveˇr
616 thˇe"})
617 .await;
618 let mut cx = cx.binding(["shift-e"]);
619 cx.assert_all(indoc! {"
620 Thˇe quicˇkˇ-browˇn
621
622
623 fox_jumpˇs oveˇr
624 thˇe"})
625 .await;
626 }
627
628 #[gpui::test]
629 async fn test_b(cx: &mut gpui::TestAppContext) {
630 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["b"]);
631 cx.assert_all(indoc! {"
632 ˇThe ˇquickˇ-ˇbrown
633 ˇ
634 ˇ
635 ˇfox_jumps ˇover
636 ˇthe"})
637 .await;
638 let mut cx = cx.binding(["shift-b"]);
639 cx.assert_all(indoc! {"
640 ˇThe ˇquickˇ-ˇbrown
641 ˇ
642 ˇ
643 ˇfox_jumps ˇover
644 ˇthe"})
645 .await;
646 }
647
648 #[gpui::test]
649 async fn test_g_prefix_and_abort(cx: &mut gpui::TestAppContext) {
650 let mut cx = VimTestContext::new(cx, true).await;
651
652 // Can abort with escape to get back to normal mode
653 cx.simulate_keystroke("g");
654 assert_eq!(cx.mode(), Normal);
655 assert_eq!(
656 cx.active_operator(),
657 Some(Operator::Namespace(Namespace::G))
658 );
659 cx.simulate_keystroke("escape");
660 assert_eq!(cx.mode(), Normal);
661 assert_eq!(cx.active_operator(), None);
662 }
663
664 #[gpui::test]
665 async fn test_gg(cx: &mut gpui::TestAppContext) {
666 let mut cx = NeovimBackedTestContext::new(cx).await;
667 cx.assert_binding_matches_all(
668 ["g", "g"],
669 indoc! {"
670 The qˇuick
671
672 brown fox jumps
673 over ˇthe laˇzy dog"},
674 )
675 .await;
676 cx.assert_binding_matches(
677 ["g", "g"],
678 indoc! {"
679
680
681 brown fox jumps
682 over the laˇzy dog"},
683 )
684 .await;
685 cx.assert_binding_matches(
686 ["2", "g", "g"],
687 indoc! {"
688 ˇ
689
690 brown fox jumps
691 over the lazydog"},
692 )
693 .await;
694 }
695
696 #[gpui::test]
697 async fn test_end_of_document(cx: &mut gpui::TestAppContext) {
698 let mut cx = NeovimBackedTestContext::new(cx).await;
699 cx.assert_binding_matches_all(
700 ["shift-g"],
701 indoc! {"
702 The qˇuick
703
704 brown fox jumps
705 over ˇthe laˇzy dog"},
706 )
707 .await;
708 cx.assert_binding_matches(
709 ["shift-g"],
710 indoc! {"
711
712
713 brown fox jumps
714 over the laˇzy dog"},
715 )
716 .await;
717 cx.assert_binding_matches(
718 ["2", "shift-g"],
719 indoc! {"
720 ˇ
721
722 brown fox jumps
723 over the lazydog"},
724 )
725 .await;
726 }
727
728 #[gpui::test]
729 async fn test_a(cx: &mut gpui::TestAppContext) {
730 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["a"]);
731 cx.assert_all("The qˇuicˇk").await;
732 }
733
734 #[gpui::test]
735 async fn test_insert_end_of_line(cx: &mut gpui::TestAppContext) {
736 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-a"]);
737 cx.assert_all(indoc! {"
738 ˇ
739 The qˇuick
740 brown ˇfox "})
741 .await;
742 }
743
744 #[gpui::test]
745 async fn test_jump_to_first_non_whitespace(cx: &mut gpui::TestAppContext) {
746 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["^"]);
747 cx.assert("The qˇuick").await;
748 cx.assert(" The qˇuick").await;
749 cx.assert("ˇ").await;
750 cx.assert(indoc! {"
751 The qˇuick
752 brown fox"})
753 .await;
754 cx.assert(indoc! {"
755 ˇ
756 The quick"})
757 .await;
758 // Indoc disallows trailing whitspace.
759 cx.assert(" ˇ \nThe quick").await;
760 }
761
762 #[gpui::test]
763 async fn test_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
764 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-i"]);
765 cx.assert("The qˇuick").await;
766 cx.assert(" The qˇuick").await;
767 cx.assert("ˇ").await;
768 cx.assert(indoc! {"
769 The qˇuick
770 brown fox"})
771 .await;
772 cx.assert(indoc! {"
773 ˇ
774 The quick"})
775 .await;
776 }
777
778 #[gpui::test]
779 async fn test_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
780 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-d"]);
781 cx.assert(indoc! {"
782 The qˇuick
783 brown fox"})
784 .await;
785 cx.assert(indoc! {"
786 The quick
787 ˇ
788 brown fox"})
789 .await;
790 }
791
792 #[gpui::test]
793 async fn test_x(cx: &mut gpui::TestAppContext) {
794 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["x"]);
795 cx.assert_all("ˇTeˇsˇt").await;
796 cx.assert(indoc! {"
797 Tesˇt
798 test"})
799 .await;
800 }
801
802 #[gpui::test]
803 async fn test_delete_left(cx: &mut gpui::TestAppContext) {
804 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-x"]);
805 cx.assert_all("ˇTˇeˇsˇt").await;
806 cx.assert(indoc! {"
807 Test
808 ˇtest"})
809 .await;
810 }
811
812 #[gpui::test]
813 async fn test_o(cx: &mut gpui::TestAppContext) {
814 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["o"]);
815 cx.assert("ˇ").await;
816 cx.assert("The ˇquick").await;
817 cx.assert_all(indoc! {"
818 The qˇuick
819 brown ˇfox
820 jumps ˇover"})
821 .await;
822 cx.assert(indoc! {"
823 The quick
824 ˇ
825 brown fox"})
826 .await;
827 cx.assert(indoc! {"
828 fn test() {
829 println!(ˇ);
830 }
831 "})
832 .await;
833 cx.assert(indoc! {"
834 fn test(ˇ) {
835 println!();
836 }"})
837 .await;
838 }
839
840 #[gpui::test]
841 async fn test_insert_line_above(cx: &mut gpui::TestAppContext) {
842 let cx = NeovimBackedTestContext::new(cx).await;
843 let mut cx = cx.binding(["shift-o"]);
844 cx.assert("ˇ").await;
845 cx.assert("The ˇquick").await;
846 cx.assert_all(indoc! {"
847 The qˇuick
848 brown ˇfox
849 jumps ˇover"})
850 .await;
851 cx.assert(indoc! {"
852 The quick
853 ˇ
854 brown fox"})
855 .await;
856
857 // Our indentation is smarter than vims. So we don't match here
858 cx.assert_manual(
859 indoc! {"
860 fn test()
861 println!(ˇ);"},
862 Mode::Normal,
863 indoc! {"
864 fn test()
865 ˇ
866 println!();"},
867 Mode::Insert,
868 );
869 cx.assert_manual(
870 indoc! {"
871 fn test(ˇ) {
872 println!();
873 }"},
874 Mode::Normal,
875 indoc! {"
876 ˇ
877 fn test() {
878 println!();
879 }"},
880 Mode::Insert,
881 );
882 }
883
884 #[gpui::test]
885 async fn test_dd(cx: &mut gpui::TestAppContext) {
886 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "d"]);
887 cx.assert("ˇ").await;
888 cx.assert("The ˇquick").await;
889 cx.assert_all(indoc! {"
890 The qˇuick
891 brown ˇfox
892 jumps ˇover"})
893 .await;
894 cx.assert_exempted(
895 indoc! {"
896 The quick
897 ˇ
898 brown fox"},
899 ExemptionFeatures::DeletionOnEmptyLine,
900 )
901 .await;
902 }
903
904 #[gpui::test]
905 async fn test_cc(cx: &mut gpui::TestAppContext) {
906 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "c"]);
907 cx.assert("ˇ").await;
908 cx.assert("The ˇquick").await;
909 cx.assert_all(indoc! {"
910 The quˇick
911 brown ˇfox
912 jumps ˇover"})
913 .await;
914 cx.assert(indoc! {"
915 The quick
916 ˇ
917 brown fox"})
918 .await;
919 }
920
921 #[gpui::test]
922 async fn test_p(cx: &mut gpui::TestAppContext) {
923 let mut cx = NeovimBackedTestContext::new(cx).await;
924 cx.set_shared_state(indoc! {"
925 The quick brown
926 fox juˇmps over
927 the lazy dog"})
928 .await;
929
930 cx.simulate_shared_keystrokes(["d", "d"]).await;
931 cx.assert_state_matches().await;
932
933 cx.simulate_shared_keystroke("p").await;
934 cx.assert_state_matches().await;
935
936 cx.set_shared_state(indoc! {"
937 The quick brown
938 fox ˇjumps over
939 the lazy dog"})
940 .await;
941 cx.simulate_shared_keystrokes(["v", "w", "y"]).await;
942 cx.set_shared_state(indoc! {"
943 The quick brown
944 fox jumps oveˇr
945 the lazy dog"})
946 .await;
947 cx.simulate_shared_keystroke("p").await;
948 cx.assert_state_matches().await;
949 }
950
951 #[gpui::test]
952 async fn test_repeated_word(cx: &mut gpui::TestAppContext) {
953 let mut cx = NeovimBackedTestContext::new(cx).await;
954
955 for count in 1..=5 {
956 cx.assert_binding_matches_all(
957 [&count.to_string(), "w"],
958 indoc! {"
959 ˇThe quˇickˇ browˇn
960 ˇ
961 ˇfox ˇjumpsˇ-ˇoˇver
962 ˇthe lazy dog
963 "},
964 )
965 .await;
966 }
967 }
968
969 #[gpui::test]
970 async fn test_h_through_unicode(cx: &mut gpui::TestAppContext) {
971 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["h"]);
972 cx.assert_all("Testˇ├ˇ──ˇ┐ˇTest").await;
973 }
974
975 #[gpui::test]
976 async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
977 let mut cx = NeovimBackedTestContext::new(cx).await;
978 for count in 1..=3 {
979 let test_case = indoc! {"
980 ˇaaaˇbˇ ˇbˇ ˇbˇbˇ aˇaaˇbaaa
981 ˇ ˇbˇaaˇa ˇbˇbˇb
982 ˇ
983 ˇb
984 "};
985
986 cx.assert_binding_matches_all([&count.to_string(), "f", "b"], test_case)
987 .await;
988
989 cx.assert_binding_matches_all([&count.to_string(), "t", "b"], test_case)
990 .await;
991 }
992 }
993
994 #[gpui::test]
995 async fn test_capital_f_and_capital_t(cx: &mut gpui::TestAppContext) {
996 let mut cx = NeovimBackedTestContext::new(cx).await;
997 for count in 1..=3 {
998 let test_case = indoc! {"
999 ˇaaaˇbˇ ˇbˇ ˇbˇbˇ aˇaaˇbaaa
1000 ˇ ˇbˇaaˇa ˇbˇbˇb
1001 ˇ
1002 ˇb
1003 "};
1004
1005 cx.assert_binding_matches_all([&count.to_string(), "shift-f", "b"], test_case)
1006 .await;
1007
1008 cx.assert_binding_matches_all([&count.to_string(), "shift-t", "b"], test_case)
1009 .await;
1010 }
1011 }
1012}