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