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