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