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