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