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