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