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