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