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::BTreeSet;
21use editor::scroll::Autoscroll;
22use editor::Bias;
23use gpui::{actions, ViewContext, WindowContext};
24use language::{Point, 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 Indent,
55 Outdent,
56 ]
57);
58
59pub(crate) fn register(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
60 workspace.register_action(insert_after);
61 workspace.register_action(insert_before);
62 workspace.register_action(insert_first_non_whitespace);
63 workspace.register_action(insert_end_of_line);
64 workspace.register_action(insert_line_above);
65 workspace.register_action(insert_line_below);
66 workspace.register_action(change_case);
67 workspace.register_action(convert_to_upper_case);
68 workspace.register_action(convert_to_lower_case);
69 workspace.register_action(yank_line);
70
71 workspace.register_action(|_: &mut Workspace, _: &DeleteLeft, cx| {
72 Vim::update(cx, |vim, cx| {
73 vim.record_current_action(cx);
74 let times = vim.take_count(cx);
75 delete_motion(vim, Motion::Left, times, cx);
76 })
77 });
78 workspace.register_action(|_: &mut Workspace, _: &DeleteRight, cx| {
79 Vim::update(cx, |vim, cx| {
80 vim.record_current_action(cx);
81 let times = vim.take_count(cx);
82 delete_motion(vim, Motion::Right, times, cx);
83 })
84 });
85 workspace.register_action(|_: &mut Workspace, _: &ChangeToEndOfLine, cx| {
86 Vim::update(cx, |vim, cx| {
87 vim.start_recording(cx);
88 let times = vim.take_count(cx);
89 change_motion(
90 vim,
91 Motion::EndOfLine {
92 display_lines: false,
93 },
94 times,
95 cx,
96 );
97 })
98 });
99 workspace.register_action(|_: &mut Workspace, _: &DeleteToEndOfLine, cx| {
100 Vim::update(cx, |vim, cx| {
101 vim.record_current_action(cx);
102 let times = vim.take_count(cx);
103 delete_motion(
104 vim,
105 Motion::EndOfLine {
106 display_lines: false,
107 },
108 times,
109 cx,
110 );
111 })
112 });
113 workspace.register_action(|_: &mut Workspace, _: &JoinLines, cx| {
114 Vim::update(cx, |vim, cx| {
115 vim.record_current_action(cx);
116 let mut times = vim.take_count(cx).unwrap_or(1);
117 if vim.state().mode.is_visual() {
118 times = 1;
119 } else if times > 1 {
120 // 2J joins two lines together (same as J or 1J)
121 times -= 1;
122 }
123
124 vim.update_active_editor(cx, |_, editor, cx| {
125 editor.transact(cx, |editor, cx| {
126 for _ in 0..times {
127 editor.join_lines(&Default::default(), cx)
128 }
129 })
130 });
131 if vim.state().mode.is_visual() {
132 vim.switch_mode(Mode::Normal, false, cx)
133 }
134 });
135 });
136
137 workspace.register_action(|_: &mut Workspace, _: &Indent, cx| {
138 Vim::update(cx, |vim, cx| {
139 vim.record_current_action(cx);
140 vim.update_active_editor(cx, |_, editor, cx| {
141 editor.transact(cx, |editor, cx| editor.indent(&Default::default(), cx))
142 });
143 if vim.state().mode.is_visual() {
144 vim.switch_mode(Mode::Normal, false, cx)
145 }
146 });
147 });
148
149 workspace.register_action(|_: &mut Workspace, _: &Outdent, cx| {
150 Vim::update(cx, |vim, cx| {
151 vim.record_current_action(cx);
152 vim.update_active_editor(cx, |_, editor, cx| {
153 editor.transact(cx, |editor, cx| editor.outdent(&Default::default(), cx))
154 });
155 if vim.state().mode.is_visual() {
156 vim.switch_mode(Mode::Normal, false, cx)
157 }
158 });
159 });
160
161 paste::register(workspace, cx);
162 repeat::register(workspace, cx);
163 scroll::register(workspace, cx);
164 search::register(workspace, cx);
165 substitute::register(workspace, cx);
166 increment::register(workspace, cx);
167}
168
169pub fn normal_motion(
170 motion: Motion,
171 operator: Option<Operator>,
172 times: Option<usize>,
173 cx: &mut WindowContext,
174) {
175 Vim::update(cx, |vim, cx| {
176 match operator {
177 None => move_cursor(vim, motion, times, cx),
178 Some(Operator::Change) => change_motion(vim, motion, times, cx),
179 Some(Operator::Delete) => delete_motion(vim, motion, times, cx),
180 Some(Operator::Yank) => yank_motion(vim, motion, times, cx),
181 Some(operator) => {
182 // Can't do anything for text objects, Ignoring
183 error!("Unexpected normal mode motion operator: {:?}", operator)
184 }
185 }
186 });
187}
188
189pub fn normal_object(object: Object, cx: &mut WindowContext) {
190 Vim::update(cx, |vim, cx| {
191 match vim.maybe_pop_operator() {
192 Some(Operator::Object { around }) => match vim.maybe_pop_operator() {
193 Some(Operator::Change) => change_object(vim, object, around, cx),
194 Some(Operator::Delete) => delete_object(vim, object, around, cx),
195 Some(Operator::Yank) => yank_object(vim, object, around, cx),
196 _ => {
197 // Can't do anything for namespace operators. Ignoring
198 }
199 },
200 _ => {
201 // Can't do anything with change/delete/yank and text objects. Ignoring
202 }
203 }
204 vim.clear_operator(cx);
205 })
206}
207
208pub(crate) fn move_cursor(
209 vim: &mut Vim,
210 motion: Motion,
211 times: Option<usize>,
212 cx: &mut WindowContext,
213) {
214 vim.update_active_editor(cx, |_, editor, cx| {
215 let text_layout_details = editor.text_layout_details(cx);
216 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
217 s.move_cursors_with(|map, cursor, goal| {
218 motion
219 .move_point(map, cursor, goal, times, &text_layout_details)
220 .unwrap_or((cursor, goal))
221 })
222 })
223 });
224}
225
226fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext<Workspace>) {
227 Vim::update(cx, |vim, cx| {
228 vim.start_recording(cx);
229 vim.switch_mode(Mode::Insert, false, cx);
230 vim.update_active_editor(cx, |_, editor, cx| {
231 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
232 s.move_cursors_with(|map, cursor, _| (right(map, cursor, 1), SelectionGoal::None));
233 });
234 });
235 });
236}
237
238fn insert_before(_: &mut Workspace, _: &InsertBefore, cx: &mut ViewContext<Workspace>) {
239 Vim::update(cx, |vim, cx| {
240 vim.start_recording(cx);
241 vim.switch_mode(Mode::Insert, false, cx);
242 });
243}
244
245fn insert_first_non_whitespace(
246 _: &mut Workspace,
247 _: &InsertFirstNonWhitespace,
248 cx: &mut ViewContext<Workspace>,
249) {
250 Vim::update(cx, |vim, cx| {
251 vim.start_recording(cx);
252 vim.switch_mode(Mode::Insert, false, cx);
253 vim.update_active_editor(cx, |_, editor, cx| {
254 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
255 s.move_cursors_with(|map, cursor, _| {
256 (
257 first_non_whitespace(map, false, cursor),
258 SelectionGoal::None,
259 )
260 });
261 });
262 });
263 });
264}
265
266fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewContext<Workspace>) {
267 Vim::update(cx, |vim, cx| {
268 vim.start_recording(cx);
269 vim.switch_mode(Mode::Insert, false, cx);
270 vim.update_active_editor(cx, |_, editor, cx| {
271 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
272 s.move_cursors_with(|map, cursor, _| {
273 (next_line_end(map, cursor, 1), SelectionGoal::None)
274 });
275 });
276 });
277 });
278}
279
280fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, 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 editor.transact(cx, |editor, cx| {
286 let selections = editor.selections.all::<Point>(cx);
287 let snapshot = editor.buffer().read(cx).snapshot(cx);
288
289 let selection_start_rows: BTreeSet<u32> = selections
290 .into_iter()
291 .map(|selection| selection.start.row)
292 .collect();
293 let edits = selection_start_rows.into_iter().map(|row| {
294 let indent = snapshot
295 .indent_size_for_line(row)
296 .chars()
297 .collect::<String>();
298 let start_of_line = Point::new(row, 0);
299 (start_of_line..start_of_line, indent + "\n")
300 });
301 editor.edit_with_autoindent(edits, cx);
302 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
303 s.move_cursors_with(|map, cursor, _| {
304 let previous_line = motion::start_of_relative_buffer_row(map, cursor, -1);
305 let insert_point = motion::end_of_line(map, false, previous_line, 1);
306 (insert_point, SelectionGoal::None)
307 });
308 });
309 });
310 });
311 });
312}
313
314fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContext<Workspace>) {
315 Vim::update(cx, |vim, cx| {
316 vim.start_recording(cx);
317 vim.switch_mode(Mode::Insert, false, cx);
318 vim.update_active_editor(cx, |_, editor, cx| {
319 let text_layout_details = editor.text_layout_details(cx);
320 editor.transact(cx, |editor, cx| {
321 let selections = editor.selections.all::<Point>(cx);
322 let snapshot = editor.buffer().read(cx).snapshot(cx);
323
324 let selection_end_rows: BTreeSet<u32> = selections
325 .into_iter()
326 .map(|selection| selection.end.row)
327 .collect();
328 let edits = selection_end_rows.into_iter().map(|row| {
329 let indent = snapshot
330 .indent_size_for_line(row)
331 .chars()
332 .collect::<String>();
333 let end_of_line = Point::new(row, snapshot.line_len(row));
334 (end_of_line..end_of_line, "\n".to_string() + &indent)
335 });
336 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
337 s.maybe_move_cursors_with(|map, cursor, goal| {
338 Motion::CurrentLine.move_point(
339 map,
340 cursor,
341 goal,
342 None,
343 &text_layout_details,
344 )
345 });
346 });
347 editor.edit_with_autoindent(edits, cx);
348 });
349 });
350 });
351}
352
353fn yank_line(_: &mut Workspace, _: &YankLine, cx: &mut ViewContext<Workspace>) {
354 Vim::update(cx, |vim, cx| {
355 let count = vim.take_count(cx);
356 yank_motion(vim, motion::Motion::CurrentLine, count, cx)
357 })
358}
359
360pub(crate) fn normal_replace(text: Arc<str>, cx: &mut WindowContext) {
361 Vim::update(cx, |vim, cx| {
362 vim.stop_recording();
363 vim.update_active_editor(cx, |_, editor, cx| {
364 editor.transact(cx, |editor, cx| {
365 editor.set_clip_at_line_ends(false, cx);
366 let (map, display_selections) = editor.selections.all_display(cx);
367 // Selections are biased right at the start. So we need to store
368 // anchors that are biased left so that we can restore the selections
369 // after the change
370 let stable_anchors = editor
371 .selections
372 .disjoint_anchors()
373 .into_iter()
374 .map(|selection| {
375 let start = selection.start.bias_left(&map.buffer_snapshot);
376 start..start
377 })
378 .collect::<Vec<_>>();
379
380 let edits = display_selections
381 .into_iter()
382 .map(|selection| {
383 let mut range = selection.range();
384 *range.end.column_mut() += 1;
385 range.end = map.clip_point(range.end, Bias::Right);
386
387 (
388 range.start.to_offset(&map, Bias::Left)
389 ..range.end.to_offset(&map, Bias::Left),
390 text.clone(),
391 )
392 })
393 .collect::<Vec<_>>();
394
395 editor.buffer().update(cx, |buffer, cx| {
396 buffer.edit(edits, None, cx);
397 });
398 editor.set_clip_at_line_ends(true, cx);
399 editor.change_selections(None, cx, |s| {
400 s.select_anchor_ranges(stable_anchors);
401 });
402 });
403 });
404 vim.pop_operator(cx)
405 });
406}
407
408#[cfg(test)]
409mod test {
410 use gpui::{KeyBinding, TestAppContext};
411 use indoc::indoc;
412 use settings::SettingsStore;
413
414 use crate::{
415 motion,
416 state::Mode::{self},
417 test::{NeovimBackedTestContext, VimTestContext},
418 VimSettings,
419 };
420
421 #[gpui::test]
422 async fn test_h(cx: &mut gpui::TestAppContext) {
423 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["h"]);
424 cx.assert_all(indoc! {"
425 ˇThe qˇuick
426 ˇbrown"
427 })
428 .await;
429 }
430
431 #[gpui::test]
432 async fn test_backspace(cx: &mut gpui::TestAppContext) {
433 let mut cx = NeovimBackedTestContext::new(cx)
434 .await
435 .binding(["backspace"]);
436 cx.assert_all(indoc! {"
437 ˇThe qˇuick
438 ˇbrown"
439 })
440 .await;
441 }
442
443 #[gpui::test]
444 async fn test_j(cx: &mut gpui::TestAppContext) {
445 let mut cx = NeovimBackedTestContext::new(cx).await;
446
447 cx.set_shared_state(indoc! {"
448 aaˇaa
449 😃😃"
450 })
451 .await;
452 cx.simulate_shared_keystrokes(["j"]).await;
453 cx.assert_shared_state(indoc! {"
454 aaaa
455 😃ˇ😃"
456 })
457 .await;
458
459 for marked_position in cx.each_marked_position(indoc! {"
460 ˇThe qˇuick broˇwn
461 ˇfox jumps"
462 }) {
463 cx.assert_neovim_compatible(&marked_position, ["j"]).await;
464 }
465 }
466
467 #[gpui::test]
468 async fn test_enter(cx: &mut gpui::TestAppContext) {
469 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["enter"]);
470 cx.assert_all(indoc! {"
471 ˇThe qˇuick broˇwn
472 ˇfox jumps"
473 })
474 .await;
475 }
476
477 #[gpui::test]
478 async fn test_k(cx: &mut gpui::TestAppContext) {
479 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["k"]);
480 cx.assert_all(indoc! {"
481 ˇThe qˇuick
482 ˇbrown fˇox jumˇps"
483 })
484 .await;
485 }
486
487 #[gpui::test]
488 async fn test_l(cx: &mut gpui::TestAppContext) {
489 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["l"]);
490 cx.assert_all(indoc! {"
491 ˇThe qˇuicˇk
492 ˇbrowˇn"})
493 .await;
494 }
495
496 #[gpui::test]
497 async fn test_jump_to_line_boundaries(cx: &mut gpui::TestAppContext) {
498 let mut cx = NeovimBackedTestContext::new(cx).await;
499 cx.assert_binding_matches_all(
500 ["$"],
501 indoc! {"
502 ˇThe qˇuicˇk
503 ˇbrowˇn"},
504 )
505 .await;
506 cx.assert_binding_matches_all(
507 ["0"],
508 indoc! {"
509 ˇThe qˇuicˇk
510 ˇbrowˇn"},
511 )
512 .await;
513 }
514
515 #[gpui::test]
516 async fn test_jump_to_end(cx: &mut gpui::TestAppContext) {
517 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-g"]);
518
519 cx.assert_all(indoc! {"
520 The ˇquick
521
522 brown fox jumps
523 overˇ the lazy doˇg"})
524 .await;
525 cx.assert(indoc! {"
526 The quiˇck
527
528 brown"})
529 .await;
530 cx.assert(indoc! {"
531 The quiˇck
532
533 "})
534 .await;
535 }
536
537 #[gpui::test]
538 async fn test_w(cx: &mut gpui::TestAppContext) {
539 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["w"]);
540 cx.assert_all(indoc! {"
541 The ˇquickˇ-ˇbrown
542 ˇ
543 ˇ
544 ˇfox_jumps ˇover
545 ˇthˇe"})
546 .await;
547 let mut cx = cx.binding(["shift-w"]);
548 cx.assert_all(indoc! {"
549 The ˇquickˇ-ˇbrown
550 ˇ
551 ˇ
552 ˇfox_jumps ˇover
553 ˇthˇe"})
554 .await;
555 }
556
557 #[gpui::test]
558 async fn test_end_of_word(cx: &mut gpui::TestAppContext) {
559 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["e"]);
560 cx.assert_all(indoc! {"
561 Thˇe quicˇkˇ-browˇn
562
563
564 fox_jumpˇs oveˇr
565 thˇe"})
566 .await;
567 let mut cx = cx.binding(["shift-e"]);
568 cx.assert_all(indoc! {"
569 Thˇe quicˇkˇ-browˇn
570
571
572 fox_jumpˇs oveˇr
573 thˇe"})
574 .await;
575 }
576
577 #[gpui::test]
578 async fn test_b(cx: &mut gpui::TestAppContext) {
579 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["b"]);
580 cx.assert_all(indoc! {"
581 ˇThe ˇquickˇ-ˇbrown
582 ˇ
583 ˇ
584 ˇfox_jumps ˇover
585 ˇthe"})
586 .await;
587 let mut cx = cx.binding(["shift-b"]);
588 cx.assert_all(indoc! {"
589 ˇThe ˇquickˇ-ˇbrown
590 ˇ
591 ˇ
592 ˇfox_jumps ˇover
593 ˇthe"})
594 .await;
595 }
596
597 #[gpui::test]
598 async fn test_gg(cx: &mut gpui::TestAppContext) {
599 let mut cx = NeovimBackedTestContext::new(cx).await;
600 cx.assert_binding_matches_all(
601 ["g", "g"],
602 indoc! {"
603 The qˇuick
604
605 brown fox jumps
606 over ˇthe laˇzy dog"},
607 )
608 .await;
609 cx.assert_binding_matches(
610 ["g", "g"],
611 indoc! {"
612
613
614 brown fox jumps
615 over the laˇzy dog"},
616 )
617 .await;
618 cx.assert_binding_matches(
619 ["2", "g", "g"],
620 indoc! {"
621 ˇ
622
623 brown fox jumps
624 over the lazydog"},
625 )
626 .await;
627 }
628
629 #[gpui::test]
630 async fn test_end_of_document(cx: &mut gpui::TestAppContext) {
631 let mut cx = NeovimBackedTestContext::new(cx).await;
632 cx.assert_binding_matches_all(
633 ["shift-g"],
634 indoc! {"
635 The qˇuick
636
637 brown fox jumps
638 over ˇthe laˇzy dog"},
639 )
640 .await;
641 cx.assert_binding_matches(
642 ["shift-g"],
643 indoc! {"
644
645
646 brown fox jumps
647 over the laˇzy dog"},
648 )
649 .await;
650 cx.assert_binding_matches(
651 ["2", "shift-g"],
652 indoc! {"
653 ˇ
654
655 brown fox jumps
656 over the lazydog"},
657 )
658 .await;
659 }
660
661 #[gpui::test]
662 async fn test_a(cx: &mut gpui::TestAppContext) {
663 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["a"]);
664 cx.assert_all("The qˇuicˇk").await;
665 }
666
667 #[gpui::test]
668 async fn test_insert_end_of_line(cx: &mut gpui::TestAppContext) {
669 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-a"]);
670 cx.assert_all(indoc! {"
671 ˇ
672 The qˇuick
673 brown ˇfox "})
674 .await;
675 }
676
677 #[gpui::test]
678 async fn test_jump_to_first_non_whitespace(cx: &mut gpui::TestAppContext) {
679 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["^"]);
680 cx.assert("The qˇuick").await;
681 cx.assert(" The qˇuick").await;
682 cx.assert("ˇ").await;
683 cx.assert(indoc! {"
684 The qˇuick
685 brown fox"})
686 .await;
687 cx.assert(indoc! {"
688 ˇ
689 The quick"})
690 .await;
691 // Indoc disallows trailing whitespace.
692 cx.assert(" ˇ \nThe quick").await;
693 }
694
695 #[gpui::test]
696 async fn test_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
697 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-i"]);
698 cx.assert("The qˇuick").await;
699 cx.assert(" The qˇuick").await;
700 cx.assert("ˇ").await;
701 cx.assert(indoc! {"
702 The qˇuick
703 brown fox"})
704 .await;
705 cx.assert(indoc! {"
706 ˇ
707 The quick"})
708 .await;
709 }
710
711 #[gpui::test]
712 async fn test_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
713 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-d"]);
714 cx.assert(indoc! {"
715 The qˇuick
716 brown fox"})
717 .await;
718 cx.assert(indoc! {"
719 The quick
720 ˇ
721 brown fox"})
722 .await;
723 }
724
725 #[gpui::test]
726 async fn test_x(cx: &mut gpui::TestAppContext) {
727 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["x"]);
728 cx.assert_all("ˇTeˇsˇt").await;
729 cx.assert(indoc! {"
730 Tesˇt
731 test"})
732 .await;
733 }
734
735 #[gpui::test]
736 async fn test_delete_left(cx: &mut gpui::TestAppContext) {
737 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-x"]);
738 cx.assert_all("ˇTˇeˇsˇt").await;
739 cx.assert(indoc! {"
740 Test
741 ˇtest"})
742 .await;
743 }
744
745 #[gpui::test]
746 async fn test_o(cx: &mut gpui::TestAppContext) {
747 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["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 cx.assert_manual(
762 indoc! {"
763 fn test() {
764 println!(ˇ);
765 }"},
766 Mode::Normal,
767 indoc! {"
768 fn test() {
769 println!();
770 ˇ
771 }"},
772 Mode::Insert,
773 );
774
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 }
789
790 #[gpui::test]
791 async fn test_insert_line_above(cx: &mut gpui::TestAppContext) {
792 let cx = NeovimBackedTestContext::new(cx).await;
793 let mut cx = cx.binding(["shift-o"]);
794 cx.assert("ˇ").await;
795 cx.assert("The ˇquick").await;
796 cx.assert_all(indoc! {"
797 The qˇuick
798 brown ˇfox
799 jumps ˇover"})
800 .await;
801 cx.assert(indoc! {"
802 The quick
803 ˇ
804 brown fox"})
805 .await;
806
807 // Our indentation is smarter than vims. So we don't match here
808 cx.assert_manual(
809 indoc! {"
810 fn test() {
811 println!(ˇ);
812 }"},
813 Mode::Normal,
814 indoc! {"
815 fn test() {
816 ˇ
817 println!();
818 }"},
819 Mode::Insert,
820 );
821 cx.assert_manual(
822 indoc! {"
823 fn test(ˇ) {
824 println!();
825 }"},
826 Mode::Normal,
827 indoc! {"
828 ˇ
829 fn test() {
830 println!();
831 }"},
832 Mode::Insert,
833 );
834 }
835
836 #[gpui::test]
837 async fn test_dd(cx: &mut gpui::TestAppContext) {
838 let mut cx = NeovimBackedTestContext::new(cx).await;
839 cx.assert_neovim_compatible("ˇ", ["d", "d"]).await;
840 cx.assert_neovim_compatible("The ˇquick", ["d", "d"]).await;
841 for marked_text in cx.each_marked_position(indoc! {"
842 The qˇuick
843 brown ˇfox
844 jumps ˇover"})
845 {
846 cx.assert_neovim_compatible(&marked_text, ["d", "d"]).await;
847 }
848 cx.assert_neovim_compatible(
849 indoc! {"
850 The quick
851 ˇ
852 brown fox"},
853 ["d", "d"],
854 )
855 .await;
856 }
857
858 #[gpui::test]
859 async fn test_cc(cx: &mut gpui::TestAppContext) {
860 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "c"]);
861 cx.assert("ˇ").await;
862 cx.assert("The ˇquick").await;
863 cx.assert_all(indoc! {"
864 The quˇick
865 brown ˇfox
866 jumps ˇover"})
867 .await;
868 cx.assert(indoc! {"
869 The quick
870 ˇ
871 brown fox"})
872 .await;
873 }
874
875 #[gpui::test]
876 async fn test_repeated_word(cx: &mut gpui::TestAppContext) {
877 let mut cx = NeovimBackedTestContext::new(cx).await;
878
879 for count in 1..=5 {
880 cx.assert_binding_matches_all(
881 [&count.to_string(), "w"],
882 indoc! {"
883 ˇThe quˇickˇ browˇn
884 ˇ
885 ˇfox ˇjumpsˇ-ˇoˇver
886 ˇthe lazy dog
887 "},
888 )
889 .await;
890 }
891 }
892
893 #[gpui::test]
894 async fn test_h_through_unicode(cx: &mut gpui::TestAppContext) {
895 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["h"]);
896 cx.assert_all("Testˇ├ˇ──ˇ┐ˇTest").await;
897 }
898
899 #[gpui::test]
900 async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
901 let mut cx = NeovimBackedTestContext::new(cx).await;
902
903 for count in 1..=3 {
904 let test_case = indoc! {"
905 ˇaaaˇbˇ ˇbˇ ˇbˇbˇ aˇaaˇbaaa
906 ˇ ˇbˇaaˇa ˇbˇbˇb
907 ˇ
908 ˇb
909 "};
910
911 cx.assert_binding_matches_all([&count.to_string(), "f", "b"], test_case)
912 .await;
913
914 cx.assert_binding_matches_all([&count.to_string(), "t", "b"], test_case)
915 .await;
916 }
917 }
918
919 #[gpui::test]
920 async fn test_capital_f_and_capital_t(cx: &mut gpui::TestAppContext) {
921 let mut cx = NeovimBackedTestContext::new(cx).await;
922 let test_case = indoc! {"
923 ˇaaaˇbˇ ˇbˇ ˇbˇbˇ aˇaaˇbaaa
924 ˇ ˇbˇaaˇa ˇbˇbˇb
925 ˇ•••
926 ˇb
927 "
928 };
929
930 for count in 1..=3 {
931 cx.assert_binding_matches_all([&count.to_string(), "shift-f", "b"], test_case)
932 .await;
933
934 cx.assert_binding_matches_all([&count.to_string(), "shift-t", "b"], test_case)
935 .await;
936 }
937 }
938
939 #[gpui::test]
940 async fn test_f_and_t_multiline(cx: &mut gpui::TestAppContext) {
941 let mut cx = VimTestContext::new(cx, true).await;
942 cx.update_global(|store: &mut SettingsStore, cx| {
943 store.update_user_settings::<VimSettings>(cx, |s| {
944 s.use_multiline_find = Some(true);
945 });
946 });
947
948 cx.assert_binding(
949 ["f", "l"],
950 indoc! {"
951 ˇfunction print() {
952 console.log('ok')
953 }
954 "},
955 Mode::Normal,
956 indoc! {"
957 function print() {
958 consoˇle.log('ok')
959 }
960 "},
961 Mode::Normal,
962 );
963
964 cx.assert_binding(
965 ["t", "l"],
966 indoc! {"
967 ˇfunction print() {
968 console.log('ok')
969 }
970 "},
971 Mode::Normal,
972 indoc! {"
973 function print() {
974 consˇole.log('ok')
975 }
976 "},
977 Mode::Normal,
978 );
979 }
980
981 #[gpui::test]
982 async fn test_capital_f_and_capital_t_multiline(cx: &mut gpui::TestAppContext) {
983 let mut cx = VimTestContext::new(cx, true).await;
984 cx.update_global(|store: &mut SettingsStore, cx| {
985 store.update_user_settings::<VimSettings>(cx, |s| {
986 s.use_multiline_find = Some(true);
987 });
988 });
989
990 cx.assert_binding(
991 ["shift-f", "p"],
992 indoc! {"
993 function print() {
994 console.ˇlog('ok')
995 }
996 "},
997 Mode::Normal,
998 indoc! {"
999 function ˇprint() {
1000 console.log('ok')
1001 }
1002 "},
1003 Mode::Normal,
1004 );
1005
1006 cx.assert_binding(
1007 ["shift-t", "p"],
1008 indoc! {"
1009 function print() {
1010 console.ˇlog('ok')
1011 }
1012 "},
1013 Mode::Normal,
1014 indoc! {"
1015 function pˇrint() {
1016 console.log('ok')
1017 }
1018 "},
1019 Mode::Normal,
1020 );
1021 }
1022
1023 #[gpui::test]
1024 async fn test_f_and_t_smartcase(cx: &mut gpui::TestAppContext) {
1025 let mut cx = VimTestContext::new(cx, true).await;
1026 cx.update_global(|store: &mut SettingsStore, cx| {
1027 store.update_user_settings::<VimSettings>(cx, |s| {
1028 s.use_smartcase_find = Some(true);
1029 });
1030 });
1031
1032 cx.assert_binding(
1033 ["f", "p"],
1034 indoc! {"ˇfmt.Println(\"Hello, World!\")"},
1035 Mode::Normal,
1036 indoc! {"fmt.ˇPrintln(\"Hello, World!\")"},
1037 Mode::Normal,
1038 );
1039
1040 cx.assert_binding(
1041 ["shift-f", "p"],
1042 indoc! {"fmt.Printlnˇ(\"Hello, World!\")"},
1043 Mode::Normal,
1044 indoc! {"fmt.ˇPrintln(\"Hello, World!\")"},
1045 Mode::Normal,
1046 );
1047
1048 cx.assert_binding(
1049 ["t", "p"],
1050 indoc! {"ˇfmt.Println(\"Hello, World!\")"},
1051 Mode::Normal,
1052 indoc! {"fmtˇ.Println(\"Hello, World!\")"},
1053 Mode::Normal,
1054 );
1055
1056 cx.assert_binding(
1057 ["shift-t", "p"],
1058 indoc! {"fmt.Printlnˇ(\"Hello, World!\")"},
1059 Mode::Normal,
1060 indoc! {"fmt.Pˇrintln(\"Hello, World!\")"},
1061 Mode::Normal,
1062 );
1063 }
1064
1065 #[gpui::test]
1066 async fn test_percent(cx: &mut TestAppContext) {
1067 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["%"]);
1068 cx.assert_all("ˇconsole.logˇ(ˇvaˇrˇ)ˇ;").await;
1069 cx.assert_all("ˇconsole.logˇ(ˇ'var', ˇ[ˇ1, ˇ2, 3ˇ]ˇ)ˇ;")
1070 .await;
1071 cx.assert_all("let result = curried_funˇ(ˇ)ˇ(ˇ)ˇ;").await;
1072 }
1073
1074 #[gpui::test]
1075 async fn test_end_of_line_with_neovim(cx: &mut gpui::TestAppContext) {
1076 let mut cx = NeovimBackedTestContext::new(cx).await;
1077
1078 // goes to current line end
1079 cx.set_shared_state(indoc! {"ˇaa\nbb\ncc"}).await;
1080 cx.simulate_shared_keystrokes(["$"]).await;
1081 cx.assert_shared_state(indoc! {"aˇa\nbb\ncc"}).await;
1082
1083 // goes to next line end
1084 cx.simulate_shared_keystrokes(["2", "$"]).await;
1085 cx.assert_shared_state("aa\nbˇb\ncc").await;
1086
1087 // try to exceed the final line.
1088 cx.simulate_shared_keystrokes(["4", "$"]).await;
1089 cx.assert_shared_state("aa\nbb\ncˇc").await;
1090 }
1091
1092 #[gpui::test]
1093 async fn test_subword_motions(cx: &mut gpui::TestAppContext) {
1094 let mut cx = VimTestContext::new(cx, true).await;
1095 cx.update(|cx| {
1096 cx.bind_keys(vec![
1097 KeyBinding::new(
1098 "w",
1099 motion::NextSubwordStart {
1100 ignore_punctuation: false,
1101 },
1102 Some("Editor && VimControl && !VimWaiting && !menu"),
1103 ),
1104 KeyBinding::new(
1105 "b",
1106 motion::PreviousSubwordStart {
1107 ignore_punctuation: false,
1108 },
1109 Some("Editor && VimControl && !VimWaiting && !menu"),
1110 ),
1111 KeyBinding::new(
1112 "e",
1113 motion::NextSubwordEnd {
1114 ignore_punctuation: false,
1115 },
1116 Some("Editor && VimControl && !VimWaiting && !menu"),
1117 ),
1118 KeyBinding::new(
1119 "g e",
1120 motion::PreviousSubwordEnd {
1121 ignore_punctuation: false,
1122 },
1123 Some("Editor && VimControl && !VimWaiting && !menu"),
1124 ),
1125 ]);
1126 });
1127
1128 cx.assert_binding_normal(
1129 ["w"],
1130 indoc! {"ˇassert_binding"},
1131 indoc! {"assert_ˇbinding"},
1132 );
1133 // Special case: In 'cw', 'w' acts like 'e'
1134 cx.assert_binding(
1135 ["c", "w"],
1136 indoc! {"ˇassert_binding"},
1137 Mode::Normal,
1138 indoc! {"ˇ_binding"},
1139 Mode::Insert,
1140 );
1141
1142 cx.assert_binding_normal(
1143 ["e"],
1144 indoc! {"ˇassert_binding"},
1145 indoc! {"asserˇt_binding"},
1146 );
1147
1148 cx.assert_binding_normal(
1149 ["b"],
1150 indoc! {"assert_ˇbinding"},
1151 indoc! {"ˇassert_binding"},
1152 );
1153
1154 cx.assert_binding_normal(
1155 ["g", "e"],
1156 indoc! {"assert_bindinˇg"},
1157 indoc! {"asserˇt_binding"},
1158 );
1159 }
1160}