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 use settings::SettingsStore;
383
384 use crate::{
385 state::Mode::{self},
386 test::{NeovimBackedTestContext, VimTestContext},
387 VimSettings,
388 };
389
390 #[gpui::test]
391 async fn test_h(cx: &mut gpui::TestAppContext) {
392 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["h"]);
393 cx.assert_all(indoc! {"
394 ˇThe qˇuick
395 ˇbrown"
396 })
397 .await;
398 }
399
400 #[gpui::test]
401 async fn test_backspace(cx: &mut gpui::TestAppContext) {
402 let mut cx = NeovimBackedTestContext::new(cx)
403 .await
404 .binding(["backspace"]);
405 cx.assert_all(indoc! {"
406 ˇThe qˇuick
407 ˇbrown"
408 })
409 .await;
410 }
411
412 #[gpui::test]
413 async fn test_j(cx: &mut gpui::TestAppContext) {
414 let mut cx = NeovimBackedTestContext::new(cx).await;
415
416 cx.set_shared_state(indoc! {"
417 aaˇaa
418 😃😃"
419 })
420 .await;
421 cx.simulate_shared_keystrokes(["j"]).await;
422 cx.assert_shared_state(indoc! {"
423 aaaa
424 😃ˇ😃"
425 })
426 .await;
427
428 for marked_position in cx.each_marked_position(indoc! {"
429 ˇThe qˇuick broˇwn
430 ˇfox jumps"
431 }) {
432 cx.assert_neovim_compatible(&marked_position, ["j"]).await;
433 }
434 }
435
436 #[gpui::test]
437 async fn test_enter(cx: &mut gpui::TestAppContext) {
438 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["enter"]);
439 cx.assert_all(indoc! {"
440 ˇThe qˇuick broˇwn
441 ˇfox jumps"
442 })
443 .await;
444 }
445
446 #[gpui::test]
447 async fn test_k(cx: &mut gpui::TestAppContext) {
448 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["k"]);
449 cx.assert_all(indoc! {"
450 ˇThe qˇuick
451 ˇbrown fˇox jumˇps"
452 })
453 .await;
454 }
455
456 #[gpui::test]
457 async fn test_l(cx: &mut gpui::TestAppContext) {
458 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["l"]);
459 cx.assert_all(indoc! {"
460 ˇThe qˇuicˇk
461 ˇbrowˇn"})
462 .await;
463 }
464
465 #[gpui::test]
466 async fn test_jump_to_line_boundaries(cx: &mut gpui::TestAppContext) {
467 let mut cx = NeovimBackedTestContext::new(cx).await;
468 cx.assert_binding_matches_all(
469 ["$"],
470 indoc! {"
471 ˇThe qˇuicˇk
472 ˇbrowˇn"},
473 )
474 .await;
475 cx.assert_binding_matches_all(
476 ["0"],
477 indoc! {"
478 ˇThe qˇuicˇk
479 ˇbrowˇn"},
480 )
481 .await;
482 }
483
484 #[gpui::test]
485 async fn test_jump_to_end(cx: &mut gpui::TestAppContext) {
486 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-g"]);
487
488 cx.assert_all(indoc! {"
489 The ˇquick
490
491 brown fox jumps
492 overˇ the lazy doˇg"})
493 .await;
494 cx.assert(indoc! {"
495 The quiˇck
496
497 brown"})
498 .await;
499 cx.assert(indoc! {"
500 The quiˇck
501
502 "})
503 .await;
504 }
505
506 #[gpui::test]
507 async fn test_w(cx: &mut gpui::TestAppContext) {
508 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["w"]);
509 cx.assert_all(indoc! {"
510 The ˇquickˇ-ˇbrown
511 ˇ
512 ˇ
513 ˇfox_jumps ˇover
514 ˇthˇe"})
515 .await;
516 let mut cx = cx.binding(["shift-w"]);
517 cx.assert_all(indoc! {"
518 The ˇquickˇ-ˇbrown
519 ˇ
520 ˇ
521 ˇfox_jumps ˇover
522 ˇthˇe"})
523 .await;
524 }
525
526 #[gpui::test]
527 async fn test_end_of_word(cx: &mut gpui::TestAppContext) {
528 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["e"]);
529 cx.assert_all(indoc! {"
530 Thˇe quicˇkˇ-browˇn
531
532
533 fox_jumpˇs oveˇr
534 thˇe"})
535 .await;
536 let mut cx = cx.binding(["shift-e"]);
537 cx.assert_all(indoc! {"
538 Thˇe quicˇkˇ-browˇn
539
540
541 fox_jumpˇs oveˇr
542 thˇe"})
543 .await;
544 }
545
546 #[gpui::test]
547 async fn test_b(cx: &mut gpui::TestAppContext) {
548 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["b"]);
549 cx.assert_all(indoc! {"
550 ˇThe ˇquickˇ-ˇbrown
551 ˇ
552 ˇ
553 ˇfox_jumps ˇover
554 ˇthe"})
555 .await;
556 let mut cx = cx.binding(["shift-b"]);
557 cx.assert_all(indoc! {"
558 ˇThe ˇquickˇ-ˇbrown
559 ˇ
560 ˇ
561 ˇfox_jumps ˇover
562 ˇthe"})
563 .await;
564 }
565
566 #[gpui::test]
567 async fn test_gg(cx: &mut gpui::TestAppContext) {
568 let mut cx = NeovimBackedTestContext::new(cx).await;
569 cx.assert_binding_matches_all(
570 ["g", "g"],
571 indoc! {"
572 The qˇuick
573
574 brown fox jumps
575 over ˇthe laˇzy dog"},
576 )
577 .await;
578 cx.assert_binding_matches(
579 ["g", "g"],
580 indoc! {"
581
582
583 brown fox jumps
584 over the laˇzy dog"},
585 )
586 .await;
587 cx.assert_binding_matches(
588 ["2", "g", "g"],
589 indoc! {"
590 ˇ
591
592 brown fox jumps
593 over the lazydog"},
594 )
595 .await;
596 }
597
598 #[gpui::test]
599 async fn test_end_of_document(cx: &mut gpui::TestAppContext) {
600 let mut cx = NeovimBackedTestContext::new(cx).await;
601 cx.assert_binding_matches_all(
602 ["shift-g"],
603 indoc! {"
604 The qˇuick
605
606 brown fox jumps
607 over ˇthe laˇzy dog"},
608 )
609 .await;
610 cx.assert_binding_matches(
611 ["shift-g"],
612 indoc! {"
613
614
615 brown fox jumps
616 over the laˇzy dog"},
617 )
618 .await;
619 cx.assert_binding_matches(
620 ["2", "shift-g"],
621 indoc! {"
622 ˇ
623
624 brown fox jumps
625 over the lazydog"},
626 )
627 .await;
628 }
629
630 #[gpui::test]
631 async fn test_a(cx: &mut gpui::TestAppContext) {
632 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["a"]);
633 cx.assert_all("The qˇuicˇk").await;
634 }
635
636 #[gpui::test]
637 async fn test_insert_end_of_line(cx: &mut gpui::TestAppContext) {
638 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-a"]);
639 cx.assert_all(indoc! {"
640 ˇ
641 The qˇuick
642 brown ˇfox "})
643 .await;
644 }
645
646 #[gpui::test]
647 async fn test_jump_to_first_non_whitespace(cx: &mut gpui::TestAppContext) {
648 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["^"]);
649 cx.assert("The qˇuick").await;
650 cx.assert(" The qˇuick").await;
651 cx.assert("ˇ").await;
652 cx.assert(indoc! {"
653 The qˇuick
654 brown fox"})
655 .await;
656 cx.assert(indoc! {"
657 ˇ
658 The quick"})
659 .await;
660 // Indoc disallows trailing whitespace.
661 cx.assert(" ˇ \nThe quick").await;
662 }
663
664 #[gpui::test]
665 async fn test_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
666 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-i"]);
667 cx.assert("The qˇuick").await;
668 cx.assert(" The qˇuick").await;
669 cx.assert("ˇ").await;
670 cx.assert(indoc! {"
671 The qˇuick
672 brown fox"})
673 .await;
674 cx.assert(indoc! {"
675 ˇ
676 The quick"})
677 .await;
678 }
679
680 #[gpui::test]
681 async fn test_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
682 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-d"]);
683 cx.assert(indoc! {"
684 The qˇuick
685 brown fox"})
686 .await;
687 cx.assert(indoc! {"
688 The quick
689 ˇ
690 brown fox"})
691 .await;
692 }
693
694 #[gpui::test]
695 async fn test_x(cx: &mut gpui::TestAppContext) {
696 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["x"]);
697 cx.assert_all("ˇTeˇsˇt").await;
698 cx.assert(indoc! {"
699 Tesˇt
700 test"})
701 .await;
702 }
703
704 #[gpui::test]
705 async fn test_delete_left(cx: &mut gpui::TestAppContext) {
706 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-x"]);
707 cx.assert_all("ˇTˇeˇsˇt").await;
708 cx.assert(indoc! {"
709 Test
710 ˇtest"})
711 .await;
712 }
713
714 #[gpui::test]
715 async fn test_o(cx: &mut gpui::TestAppContext) {
716 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["o"]);
717 cx.assert("ˇ").await;
718 cx.assert("The ˇquick").await;
719 cx.assert_all(indoc! {"
720 The qˇuick
721 brown ˇfox
722 jumps ˇover"})
723 .await;
724 cx.assert(indoc! {"
725 The quick
726 ˇ
727 brown fox"})
728 .await;
729
730 cx.assert_manual(
731 indoc! {"
732 fn test() {
733 println!(ˇ);
734 }"},
735 Mode::Normal,
736 indoc! {"
737 fn test() {
738 println!();
739 ˇ
740 }"},
741 Mode::Insert,
742 );
743
744 cx.assert_manual(
745 indoc! {"
746 fn test(ˇ) {
747 println!();
748 }"},
749 Mode::Normal,
750 indoc! {"
751 fn test() {
752 ˇ
753 println!();
754 }"},
755 Mode::Insert,
756 );
757 }
758
759 #[gpui::test]
760 async fn test_insert_line_above(cx: &mut gpui::TestAppContext) {
761 let cx = NeovimBackedTestContext::new(cx).await;
762 let mut cx = cx.binding(["shift-o"]);
763 cx.assert("ˇ").await;
764 cx.assert("The ˇquick").await;
765 cx.assert_all(indoc! {"
766 The qˇuick
767 brown ˇfox
768 jumps ˇover"})
769 .await;
770 cx.assert(indoc! {"
771 The quick
772 ˇ
773 brown fox"})
774 .await;
775
776 // Our indentation is smarter than vims. So we don't match here
777 cx.assert_manual(
778 indoc! {"
779 fn test() {
780 println!(ˇ);
781 }"},
782 Mode::Normal,
783 indoc! {"
784 fn test() {
785 ˇ
786 println!();
787 }"},
788 Mode::Insert,
789 );
790 cx.assert_manual(
791 indoc! {"
792 fn test(ˇ) {
793 println!();
794 }"},
795 Mode::Normal,
796 indoc! {"
797 ˇ
798 fn test() {
799 println!();
800 }"},
801 Mode::Insert,
802 );
803 }
804
805 #[gpui::test]
806 async fn test_dd(cx: &mut gpui::TestAppContext) {
807 let mut cx = NeovimBackedTestContext::new(cx).await;
808 cx.assert_neovim_compatible("ˇ", ["d", "d"]).await;
809 cx.assert_neovim_compatible("The ˇquick", ["d", "d"]).await;
810 for marked_text in cx.each_marked_position(indoc! {"
811 The qˇuick
812 brown ˇfox
813 jumps ˇover"})
814 {
815 cx.assert_neovim_compatible(&marked_text, ["d", "d"]).await;
816 }
817 cx.assert_neovim_compatible(
818 indoc! {"
819 The quick
820 ˇ
821 brown fox"},
822 ["d", "d"],
823 )
824 .await;
825 }
826
827 #[gpui::test]
828 async fn test_cc(cx: &mut gpui::TestAppContext) {
829 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "c"]);
830 cx.assert("ˇ").await;
831 cx.assert("The ˇquick").await;
832 cx.assert_all(indoc! {"
833 The quˇick
834 brown ˇfox
835 jumps ˇover"})
836 .await;
837 cx.assert(indoc! {"
838 The quick
839 ˇ
840 brown fox"})
841 .await;
842 }
843
844 #[gpui::test]
845 async fn test_repeated_word(cx: &mut gpui::TestAppContext) {
846 let mut cx = NeovimBackedTestContext::new(cx).await;
847
848 for count in 1..=5 {
849 cx.assert_binding_matches_all(
850 [&count.to_string(), "w"],
851 indoc! {"
852 ˇThe quˇickˇ browˇn
853 ˇ
854 ˇfox ˇjumpsˇ-ˇoˇver
855 ˇthe lazy dog
856 "},
857 )
858 .await;
859 }
860 }
861
862 #[gpui::test]
863 async fn test_h_through_unicode(cx: &mut gpui::TestAppContext) {
864 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["h"]);
865 cx.assert_all("Testˇ├ˇ──ˇ┐ˇTest").await;
866 }
867
868 #[gpui::test]
869 async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
870 let mut cx = NeovimBackedTestContext::new(cx).await;
871
872 for count in 1..=3 {
873 let test_case = indoc! {"
874 ˇaaaˇbˇ ˇbˇ ˇbˇbˇ aˇaaˇbaaa
875 ˇ ˇbˇaaˇa ˇbˇbˇb
876 ˇ
877 ˇb
878 "};
879
880 cx.assert_binding_matches_all([&count.to_string(), "f", "b"], test_case)
881 .await;
882
883 cx.assert_binding_matches_all([&count.to_string(), "t", "b"], test_case)
884 .await;
885 }
886 }
887
888 #[gpui::test]
889 async fn test_capital_f_and_capital_t(cx: &mut gpui::TestAppContext) {
890 let mut cx = NeovimBackedTestContext::new(cx).await;
891 let test_case = indoc! {"
892 ˇaaaˇbˇ ˇbˇ ˇbˇbˇ aˇaaˇbaaa
893 ˇ ˇbˇaaˇa ˇbˇbˇb
894 ˇ•••
895 ˇb
896 "
897 };
898
899 for count in 1..=3 {
900 cx.assert_binding_matches_all([&count.to_string(), "shift-f", "b"], test_case)
901 .await;
902
903 cx.assert_binding_matches_all([&count.to_string(), "shift-t", "b"], test_case)
904 .await;
905 }
906 }
907
908 #[gpui::test]
909 async fn test_f_and_t_multiline(cx: &mut gpui::TestAppContext) {
910 let mut cx = VimTestContext::new(cx, true).await;
911 cx.update_global(|store: &mut SettingsStore, cx| {
912 store.update_user_settings::<VimSettings>(cx, |s| {
913 s.use_multiline_find = Some(true);
914 });
915 });
916
917 cx.assert_binding(
918 ["f", "l"],
919 indoc! {"
920 ˇfunction print() {
921 console.log('ok')
922 }
923 "},
924 Mode::Normal,
925 indoc! {"
926 function print() {
927 consoˇle.log('ok')
928 }
929 "},
930 Mode::Normal,
931 );
932
933 cx.assert_binding(
934 ["t", "l"],
935 indoc! {"
936 ˇfunction print() {
937 console.log('ok')
938 }
939 "},
940 Mode::Normal,
941 indoc! {"
942 function print() {
943 consˇole.log('ok')
944 }
945 "},
946 Mode::Normal,
947 );
948 }
949
950 #[gpui::test]
951 async fn test_capital_f_and_capital_t_multiline(cx: &mut gpui::TestAppContext) {
952 let mut cx = VimTestContext::new(cx, true).await;
953 cx.update_global(|store: &mut SettingsStore, cx| {
954 store.update_user_settings::<VimSettings>(cx, |s| {
955 s.use_multiline_find = Some(true);
956 });
957 });
958
959 cx.assert_binding(
960 ["shift-f", "p"],
961 indoc! {"
962 function print() {
963 console.ˇlog('ok')
964 }
965 "},
966 Mode::Normal,
967 indoc! {"
968 function ˇprint() {
969 console.log('ok')
970 }
971 "},
972 Mode::Normal,
973 );
974
975 cx.assert_binding(
976 ["shift-t", "p"],
977 indoc! {"
978 function print() {
979 console.ˇlog('ok')
980 }
981 "},
982 Mode::Normal,
983 indoc! {"
984 function pˇrint() {
985 console.log('ok')
986 }
987 "},
988 Mode::Normal,
989 );
990 }
991
992 #[gpui::test]
993 async fn test_percent(cx: &mut TestAppContext) {
994 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["%"]);
995 cx.assert_all("ˇconsole.logˇ(ˇvaˇrˇ)ˇ;").await;
996 cx.assert_all("ˇconsole.logˇ(ˇ'var', ˇ[ˇ1, ˇ2, 3ˇ]ˇ)ˇ;")
997 .await;
998 cx.assert_all("let result = curried_funˇ(ˇ)ˇ(ˇ)ˇ;").await;
999 }
1000}