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