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