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