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