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