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::{self, 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 =
218 motion::start_of_line(&map, false, DisplayPoint::new(row, 0))
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, cursor, _| {
227 let previous_line = motion::up(map, cursor, SelectionGoal::None, 1).0;
228 let insert_point = motion::end_of_line(map, false, previous_line);
229 (insert_point, 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
244 let selection_end_rows: HashSet<u32> = old_selections
245 .into_iter()
246 .map(|selection| selection.end.row())
247 .collect();
248 let edits = selection_end_rows.into_iter().map(|row| {
249 let (indent, _) = map.line_indent(row);
250 let end_of_line =
251 motion::end_of_line(&map, false, DisplayPoint::new(row, 0)).to_point(&map);
252
253 let mut new_text = "\n".to_string();
254 new_text.push_str(&" ".repeat(indent as usize));
255 (end_of_line..end_of_line, new_text)
256 });
257 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
258 s.maybe_move_cursors_with(|map, cursor, goal| {
259 Motion::CurrentLine.move_point(map, cursor, goal, None)
260 });
261 });
262 editor.edit_with_autoindent(edits, cx);
263 });
264 });
265 });
266}
267
268pub(crate) fn normal_replace(text: Arc<str>, cx: &mut WindowContext) {
269 Vim::update(cx, |vim, cx| {
270 vim.update_active_editor(cx, |editor, cx| {
271 editor.transact(cx, |editor, cx| {
272 editor.set_clip_at_line_ends(false, cx);
273 let (map, display_selections) = editor.selections.all_display(cx);
274 // Selections are biased right at the start. So we need to store
275 // anchors that are biased left so that we can restore the selections
276 // after the change
277 let stable_anchors = editor
278 .selections
279 .disjoint_anchors()
280 .into_iter()
281 .map(|selection| {
282 let start = selection.start.bias_left(&map.buffer_snapshot);
283 start..start
284 })
285 .collect::<Vec<_>>();
286
287 let edits = display_selections
288 .into_iter()
289 .map(|selection| {
290 let mut range = selection.range();
291 *range.end.column_mut() += 1;
292 range.end = map.clip_point(range.end, Bias::Right);
293
294 (
295 range.start.to_offset(&map, Bias::Left)
296 ..range.end.to_offset(&map, Bias::Left),
297 text.clone(),
298 )
299 })
300 .collect::<Vec<_>>();
301
302 editor.buffer().update(cx, |buffer, cx| {
303 buffer.edit(edits, None, cx);
304 });
305 editor.set_clip_at_line_ends(true, cx);
306 editor.change_selections(None, cx, |s| {
307 s.select_anchor_ranges(stable_anchors);
308 });
309 });
310 });
311 vim.pop_operator(cx)
312 });
313}
314
315#[cfg(test)]
316mod test {
317 use gpui::TestAppContext;
318 use indoc::indoc;
319
320 use crate::{
321 state::Mode::{self},
322 test::{ExemptionFeatures, NeovimBackedTestContext},
323 };
324
325 #[gpui::test]
326 async fn test_h(cx: &mut gpui::TestAppContext) {
327 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["h"]);
328 cx.assert_all(indoc! {"
329 ˇThe qˇuick
330 ˇbrown"
331 })
332 .await;
333 }
334
335 #[gpui::test]
336 async fn test_backspace(cx: &mut gpui::TestAppContext) {
337 let mut cx = NeovimBackedTestContext::new(cx)
338 .await
339 .binding(["backspace"]);
340 cx.assert_all(indoc! {"
341 ˇThe qˇuick
342 ˇbrown"
343 })
344 .await;
345 }
346
347 #[gpui::test]
348 async fn test_j(cx: &mut gpui::TestAppContext) {
349 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["j"]);
350 cx.assert_all(indoc! {"
351 ˇThe qˇuick broˇwn
352 ˇfox jumps"
353 })
354 .await;
355 }
356
357 #[gpui::test]
358 async fn test_enter(cx: &mut gpui::TestAppContext) {
359 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["enter"]);
360 cx.assert_all(indoc! {"
361 ˇThe qˇuick broˇwn
362 ˇfox jumps"
363 })
364 .await;
365 }
366
367 #[gpui::test]
368 async fn test_k(cx: &mut gpui::TestAppContext) {
369 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["k"]);
370 cx.assert_all(indoc! {"
371 ˇThe qˇuick
372 ˇbrown fˇox jumˇps"
373 })
374 .await;
375 }
376
377 #[gpui::test]
378 async fn test_l(cx: &mut gpui::TestAppContext) {
379 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["l"]);
380 cx.assert_all(indoc! {"
381 ˇThe qˇuicˇk
382 ˇbrowˇn"})
383 .await;
384 }
385
386 #[gpui::test]
387 async fn test_jump_to_line_boundaries(cx: &mut gpui::TestAppContext) {
388 let mut cx = NeovimBackedTestContext::new(cx).await;
389 cx.assert_binding_matches_all(
390 ["$"],
391 indoc! {"
392 ˇThe qˇuicˇk
393 ˇbrowˇn"},
394 )
395 .await;
396 cx.assert_binding_matches_all(
397 ["0"],
398 indoc! {"
399 ˇThe qˇuicˇk
400 ˇbrowˇn"},
401 )
402 .await;
403 }
404
405 #[gpui::test]
406 async fn test_jump_to_end(cx: &mut gpui::TestAppContext) {
407 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-g"]);
408
409 cx.assert_all(indoc! {"
410 The ˇquick
411
412 brown fox jumps
413 overˇ the lazy doˇg"})
414 .await;
415 cx.assert(indoc! {"
416 The quiˇck
417
418 brown"})
419 .await;
420 cx.assert(indoc! {"
421 The quiˇck
422
423 "})
424 .await;
425 }
426
427 #[gpui::test]
428 async fn test_w(cx: &mut gpui::TestAppContext) {
429 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["w"]);
430 cx.assert_all(indoc! {"
431 The ˇquickˇ-ˇbrown
432 ˇ
433 ˇ
434 ˇfox_jumps ˇover
435 ˇthˇe"})
436 .await;
437 let mut cx = cx.binding(["shift-w"]);
438 cx.assert_all(indoc! {"
439 The ˇquickˇ-ˇbrown
440 ˇ
441 ˇ
442 ˇfox_jumps ˇover
443 ˇthˇe"})
444 .await;
445 }
446
447 #[gpui::test]
448 async fn test_e(cx: &mut gpui::TestAppContext) {
449 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["e"]);
450 cx.assert_all(indoc! {"
451 Thˇe quicˇkˇ-browˇn
452
453
454 fox_jumpˇs oveˇr
455 thˇe"})
456 .await;
457 let mut cx = cx.binding(["shift-e"]);
458 cx.assert_all(indoc! {"
459 Thˇe quicˇkˇ-browˇn
460
461
462 fox_jumpˇs oveˇr
463 thˇe"})
464 .await;
465 }
466
467 #[gpui::test]
468 async fn test_b(cx: &mut gpui::TestAppContext) {
469 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["b"]);
470 cx.assert_all(indoc! {"
471 ˇThe ˇquickˇ-ˇbrown
472 ˇ
473 ˇ
474 ˇfox_jumps ˇover
475 ˇthe"})
476 .await;
477 let mut cx = cx.binding(["shift-b"]);
478 cx.assert_all(indoc! {"
479 ˇThe ˇquickˇ-ˇbrown
480 ˇ
481 ˇ
482 ˇfox_jumps ˇover
483 ˇthe"})
484 .await;
485 }
486
487 #[gpui::test]
488 async fn test_gg(cx: &mut gpui::TestAppContext) {
489 let mut cx = NeovimBackedTestContext::new(cx).await;
490 cx.assert_binding_matches_all(
491 ["g", "g"],
492 indoc! {"
493 The qˇuick
494
495 brown fox jumps
496 over ˇthe laˇzy dog"},
497 )
498 .await;
499 cx.assert_binding_matches(
500 ["g", "g"],
501 indoc! {"
502
503
504 brown fox jumps
505 over the laˇzy dog"},
506 )
507 .await;
508 cx.assert_binding_matches(
509 ["2", "g", "g"],
510 indoc! {"
511 ˇ
512
513 brown fox jumps
514 over the lazydog"},
515 )
516 .await;
517 }
518
519 #[gpui::test]
520 async fn test_end_of_document(cx: &mut gpui::TestAppContext) {
521 let mut cx = NeovimBackedTestContext::new(cx).await;
522 cx.assert_binding_matches_all(
523 ["shift-g"],
524 indoc! {"
525 The qˇuick
526
527 brown fox jumps
528 over ˇthe laˇzy dog"},
529 )
530 .await;
531 cx.assert_binding_matches(
532 ["shift-g"],
533 indoc! {"
534
535
536 brown fox jumps
537 over the laˇzy dog"},
538 )
539 .await;
540 cx.assert_binding_matches(
541 ["2", "shift-g"],
542 indoc! {"
543 ˇ
544
545 brown fox jumps
546 over the lazydog"},
547 )
548 .await;
549 }
550
551 #[gpui::test]
552 async fn test_a(cx: &mut gpui::TestAppContext) {
553 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["a"]);
554 cx.assert_all("The qˇuicˇk").await;
555 }
556
557 #[gpui::test]
558 async fn test_insert_end_of_line(cx: &mut gpui::TestAppContext) {
559 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-a"]);
560 cx.assert_all(indoc! {"
561 ˇ
562 The qˇuick
563 brown ˇfox "})
564 .await;
565 }
566
567 #[gpui::test]
568 async fn test_jump_to_first_non_whitespace(cx: &mut gpui::TestAppContext) {
569 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["^"]);
570 cx.assert("The qˇuick").await;
571 cx.assert(" The qˇuick").await;
572 cx.assert("ˇ").await;
573 cx.assert(indoc! {"
574 The qˇuick
575 brown fox"})
576 .await;
577 cx.assert(indoc! {"
578 ˇ
579 The quick"})
580 .await;
581 // Indoc disallows trailing whitespace.
582 cx.assert(" ˇ \nThe quick").await;
583 }
584
585 #[gpui::test]
586 async fn test_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
587 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-i"]);
588 cx.assert("The qˇuick").await;
589 cx.assert(" The qˇuick").await;
590 cx.assert("ˇ").await;
591 cx.assert(indoc! {"
592 The qˇuick
593 brown fox"})
594 .await;
595 cx.assert(indoc! {"
596 ˇ
597 The quick"})
598 .await;
599 }
600
601 #[gpui::test]
602 async fn test_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
603 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-d"]);
604 cx.assert(indoc! {"
605 The qˇuick
606 brown fox"})
607 .await;
608 cx.assert(indoc! {"
609 The quick
610 ˇ
611 brown fox"})
612 .await;
613 }
614
615 #[gpui::test]
616 async fn test_x(cx: &mut gpui::TestAppContext) {
617 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["x"]);
618 cx.assert_all("ˇTeˇsˇt").await;
619 cx.assert(indoc! {"
620 Tesˇt
621 test"})
622 .await;
623 }
624
625 #[gpui::test]
626 async fn test_delete_left(cx: &mut gpui::TestAppContext) {
627 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-x"]);
628 cx.assert_all("ˇTˇeˇsˇt").await;
629 cx.assert(indoc! {"
630 Test
631 ˇtest"})
632 .await;
633 }
634
635 #[gpui::test]
636 async fn test_o(cx: &mut gpui::TestAppContext) {
637 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["o"]);
638 cx.assert("ˇ").await;
639 cx.assert("The ˇquick").await;
640 cx.assert_all(indoc! {"
641 The qˇuick
642 brown ˇfox
643 jumps ˇover"})
644 .await;
645 cx.assert(indoc! {"
646 The quick
647 ˇ
648 brown fox"})
649 .await;
650
651 cx.assert_manual(
652 indoc! {"
653 fn test() {
654 println!(ˇ);
655 }"},
656 Mode::Normal,
657 indoc! {"
658 fn test() {
659 println!();
660 ˇ
661 }"},
662 Mode::Insert,
663 );
664
665 cx.assert_manual(
666 indoc! {"
667 fn test(ˇ) {
668 println!();
669 }"},
670 Mode::Normal,
671 indoc! {"
672 fn test() {
673 ˇ
674 println!();
675 }"},
676 Mode::Insert,
677 );
678 }
679
680 #[gpui::test]
681 async fn test_insert_line_above(cx: &mut gpui::TestAppContext) {
682 let cx = NeovimBackedTestContext::new(cx).await;
683 let mut cx = cx.binding(["shift-o"]);
684 cx.assert("ˇ").await;
685 cx.assert("The ˇquick").await;
686 cx.assert_all(indoc! {"
687 The qˇuick
688 brown ˇfox
689 jumps ˇover"})
690 .await;
691 cx.assert(indoc! {"
692 The quick
693 ˇ
694 brown fox"})
695 .await;
696
697 // Our indentation is smarter than vims. So we don't match here
698 cx.assert_manual(
699 indoc! {"
700 fn test() {
701 println!(ˇ);
702 }"},
703 Mode::Normal,
704 indoc! {"
705 fn test() {
706 ˇ
707 println!();
708 }"},
709 Mode::Insert,
710 );
711 cx.assert_manual(
712 indoc! {"
713 fn test(ˇ) {
714 println!();
715 }"},
716 Mode::Normal,
717 indoc! {"
718 ˇ
719 fn test() {
720 println!();
721 }"},
722 Mode::Insert,
723 );
724 }
725
726 #[gpui::test]
727 async fn test_dd(cx: &mut gpui::TestAppContext) {
728 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "d"]);
729 cx.assert("ˇ").await;
730 cx.assert("The ˇquick").await;
731 cx.assert_all(indoc! {"
732 The qˇuick
733 brown ˇfox
734 jumps ˇover"})
735 .await;
736 cx.assert_exempted(
737 indoc! {"
738 The quick
739 ˇ
740 brown fox"},
741 ExemptionFeatures::DeletionOnEmptyLine,
742 )
743 .await;
744 }
745
746 #[gpui::test]
747 async fn test_cc(cx: &mut gpui::TestAppContext) {
748 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "c"]);
749 cx.assert("ˇ").await;
750 cx.assert("The ˇquick").await;
751 cx.assert_all(indoc! {"
752 The quˇick
753 brown ˇfox
754 jumps ˇover"})
755 .await;
756 cx.assert(indoc! {"
757 The quick
758 ˇ
759 brown fox"})
760 .await;
761 }
762
763 #[gpui::test]
764 async fn test_repeated_word(cx: &mut gpui::TestAppContext) {
765 let mut cx = NeovimBackedTestContext::new(cx).await;
766
767 for count in 1..=5 {
768 cx.assert_binding_matches_all(
769 [&count.to_string(), "w"],
770 indoc! {"
771 ˇThe quˇickˇ browˇn
772 ˇ
773 ˇfox ˇjumpsˇ-ˇoˇver
774 ˇthe lazy dog
775 "},
776 )
777 .await;
778 }
779 }
780
781 #[gpui::test]
782 async fn test_h_through_unicode(cx: &mut gpui::TestAppContext) {
783 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["h"]);
784 cx.assert_all("Testˇ├ˇ──ˇ┐ˇTest").await;
785 }
786
787 #[gpui::test]
788 async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
789 let mut cx = NeovimBackedTestContext::new(cx).await;
790 for count in 1..=3 {
791 let test_case = indoc! {"
792 ˇaaaˇbˇ ˇbˇ ˇbˇbˇ aˇaaˇbaaa
793 ˇ ˇbˇaaˇa ˇbˇbˇb
794 ˇ
795 ˇb
796 "};
797
798 cx.assert_binding_matches_all([&count.to_string(), "f", "b"], test_case)
799 .await;
800
801 cx.assert_binding_matches_all([&count.to_string(), "t", "b"], test_case)
802 .await;
803 }
804 }
805
806 #[gpui::test]
807 async fn test_capital_f_and_capital_t(cx: &mut gpui::TestAppContext) {
808 let mut cx = NeovimBackedTestContext::new(cx).await;
809 let test_case = indoc! {"
810 ˇaaaˇbˇ ˇbˇ ˇbˇbˇ aˇaaˇbaaa
811 ˇ ˇbˇaaˇa ˇbˇbˇb
812 ˇ•••
813 ˇb
814 "
815 };
816
817 for count in 1..=3 {
818 cx.assert_binding_matches_all([&count.to_string(), "shift-f", "b"], test_case)
819 .await;
820
821 cx.assert_binding_matches_all([&count.to_string(), "shift-t", "b"], test_case)
822 .await;
823 }
824 }
825
826 #[gpui::test]
827 async fn test_percent(cx: &mut TestAppContext) {
828 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["%"]);
829 cx.assert_all("ˇconsole.logˇ(ˇvaˇrˇ)ˇ;").await;
830 cx.assert_all("ˇconsole.logˇ(ˇ'var', ˇ[ˇ1, ˇ2, 3ˇ]ˇ)ˇ;")
831 .await;
832 cx.assert_all("let result = curried_funˇ(ˇ)ˇ(ˇ)ˇ;").await;
833 }
834}