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