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