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