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