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