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