1mod neovim_backed_test_context;
2mod neovim_connection;
3mod vim_test_context;
4
5use std::time::Duration;
6
7use collections::HashMap;
8use command_palette::CommandPalette;
9use editor::{actions::DeleteLine, display_map::DisplayRow, DisplayPoint};
10use futures::StreamExt;
11use gpui::{KeyBinding, Modifiers, MouseButton, TestAppContext};
12pub use neovim_backed_test_context::*;
13use settings::SettingsStore;
14pub use vim_test_context::*;
15
16use indoc::indoc;
17use search::BufferSearchBar;
18use workspace::WorkspaceSettings;
19
20use crate::{insert::NormalBefore, motion, state::Mode};
21
22#[gpui::test]
23async fn test_initially_disabled(cx: &mut gpui::TestAppContext) {
24 let mut cx = VimTestContext::new(cx, false).await;
25 cx.simulate_keystrokes("h j k l");
26 cx.assert_editor_state("hjklˇ");
27}
28
29#[gpui::test]
30async fn test_neovim(cx: &mut gpui::TestAppContext) {
31 let mut cx = NeovimBackedTestContext::new(cx).await;
32
33 cx.simulate_shared_keystrokes("i").await;
34 cx.shared_state().await.assert_matches();
35 cx.simulate_shared_keystrokes("shift-t e s t space t e s t escape 0 d w")
36 .await;
37 cx.shared_state().await.assert_matches();
38 cx.assert_editor_state("ˇtest");
39}
40
41#[gpui::test]
42async fn test_toggle_through_settings(cx: &mut gpui::TestAppContext) {
43 let mut cx = VimTestContext::new(cx, true).await;
44
45 cx.simulate_keystrokes("i");
46 assert_eq!(cx.mode(), Mode::Insert);
47
48 // Editor acts as though vim is disabled
49 cx.disable_vim();
50 cx.simulate_keystrokes("h j k l");
51 cx.assert_editor_state("hjklˇ");
52
53 // Selections aren't changed if editor is blurred but vim-mode is still disabled.
54 cx.cx.set_state("«hjklˇ»");
55 cx.assert_editor_state("«hjklˇ»");
56 cx.update_editor(|_, cx| cx.blur());
57 cx.assert_editor_state("«hjklˇ»");
58 cx.update_editor(|_, cx| cx.focus_self());
59 cx.assert_editor_state("«hjklˇ»");
60
61 // Enabling dynamically sets vim mode again and restores normal mode
62 cx.enable_vim();
63 assert_eq!(cx.mode(), Mode::Normal);
64 cx.simulate_keystrokes("h h h l");
65 assert_eq!(cx.buffer_text(), "hjkl".to_owned());
66 cx.assert_editor_state("hˇjkl");
67 cx.simulate_keystrokes("i T e s t");
68 cx.assert_editor_state("hTestˇjkl");
69
70 // Disabling and enabling resets to normal mode
71 assert_eq!(cx.mode(), Mode::Insert);
72 cx.disable_vim();
73 cx.enable_vim();
74 assert_eq!(cx.mode(), Mode::Normal);
75}
76
77#[gpui::test]
78async fn test_cancel_selection(cx: &mut gpui::TestAppContext) {
79 let mut cx = VimTestContext::new(cx, true).await;
80
81 cx.set_state(
82 indoc! {"The quick brown fox juˇmps over the lazy dog"},
83 Mode::Normal,
84 );
85 // jumps
86 cx.simulate_keystrokes("v l l");
87 cx.assert_editor_state("The quick brown fox ju«mpsˇ» over the lazy dog");
88
89 cx.simulate_keystrokes("escape");
90 cx.assert_editor_state("The quick brown fox jumpˇs over the lazy dog");
91
92 // go back to the same selection state
93 cx.simulate_keystrokes("v h h");
94 cx.assert_editor_state("The quick brown fox ju«ˇmps» over the lazy dog");
95
96 // Ctrl-[ should behave like Esc
97 cx.simulate_keystrokes("ctrl-[");
98 cx.assert_editor_state("The quick brown fox juˇmps over the lazy dog");
99}
100
101#[gpui::test]
102async fn test_buffer_search(cx: &mut gpui::TestAppContext) {
103 let mut cx = VimTestContext::new(cx, true).await;
104
105 cx.set_state(
106 indoc! {"
107 The quick brown
108 fox juˇmps over
109 the lazy dog"},
110 Mode::Normal,
111 );
112 cx.simulate_keystrokes("/");
113
114 let search_bar = cx.workspace(|workspace, cx| {
115 workspace
116 .active_pane()
117 .read(cx)
118 .toolbar()
119 .read(cx)
120 .item_of_type::<BufferSearchBar>()
121 .expect("Buffer search bar should be deployed")
122 });
123
124 cx.update_view(search_bar, |bar, cx| {
125 assert_eq!(bar.query(cx), "");
126 })
127}
128
129#[gpui::test]
130async fn test_count_down(cx: &mut gpui::TestAppContext) {
131 let mut cx = VimTestContext::new(cx, true).await;
132
133 cx.set_state(indoc! {"aˇa\nbb\ncc\ndd\nee"}, Mode::Normal);
134 cx.simulate_keystrokes("2 down");
135 cx.assert_editor_state("aa\nbb\ncˇc\ndd\nee");
136 cx.simulate_keystrokes("9 down");
137 cx.assert_editor_state("aa\nbb\ncc\ndd\neˇe");
138}
139
140#[gpui::test]
141async fn test_end_of_document_710(cx: &mut gpui::TestAppContext) {
142 let mut cx = VimTestContext::new(cx, true).await;
143
144 // goes to end by default
145 cx.set_state(indoc! {"aˇa\nbb\ncc"}, Mode::Normal);
146 cx.simulate_keystrokes("shift-g");
147 cx.assert_editor_state("aa\nbb\ncˇc");
148
149 // can go to line 1 (https://github.com/zed-industries/zed/issues/5812)
150 cx.simulate_keystrokes("1 shift-g");
151 cx.assert_editor_state("aˇa\nbb\ncc");
152}
153
154#[gpui::test]
155async fn test_end_of_line_with_times(cx: &mut gpui::TestAppContext) {
156 let mut cx = VimTestContext::new(cx, true).await;
157
158 // goes to current line end
159 cx.set_state(indoc! {"ˇaa\nbb\ncc"}, Mode::Normal);
160 cx.simulate_keystrokes("$");
161 cx.assert_editor_state("aˇa\nbb\ncc");
162
163 // goes to next line end
164 cx.simulate_keystrokes("2 $");
165 cx.assert_editor_state("aa\nbˇb\ncc");
166
167 // try to exceed the final line.
168 cx.simulate_keystrokes("4 $");
169 cx.assert_editor_state("aa\nbb\ncˇc");
170}
171
172#[gpui::test]
173async fn test_indent_outdent(cx: &mut gpui::TestAppContext) {
174 let mut cx = VimTestContext::new(cx, true).await;
175
176 // works in normal mode
177 cx.set_state(indoc! {"aa\nbˇb\ncc"}, Mode::Normal);
178 cx.simulate_keystrokes("> >");
179 cx.assert_editor_state("aa\n bˇb\ncc");
180 cx.simulate_keystrokes("< <");
181 cx.assert_editor_state("aa\nbˇb\ncc");
182
183 // works in visual mode
184 cx.simulate_keystrokes("shift-v down >");
185 cx.assert_editor_state("aa\n bˇb\n cc");
186
187 // works as operator
188 cx.set_state("aa\nbˇb\ncc\n", Mode::Normal);
189 cx.simulate_keystrokes("> j");
190 cx.assert_editor_state("aa\n bˇb\n cc\n");
191 cx.simulate_keystrokes("< k");
192 cx.assert_editor_state("aa\nbˇb\n cc\n");
193 cx.simulate_keystrokes("> i p");
194 cx.assert_editor_state(" aa\n bˇb\n cc\n");
195 cx.simulate_keystrokes("< i p");
196 cx.assert_editor_state("aa\nbˇb\n cc\n");
197 cx.simulate_keystrokes("< i p");
198 cx.assert_editor_state("aa\nbˇb\ncc\n");
199
200 cx.set_state("ˇaa\nbb\ncc\n", Mode::Normal);
201 cx.simulate_keystrokes("> 2 j");
202 cx.assert_editor_state(" ˇaa\n bb\n cc\n");
203
204 cx.set_state("aa\nbb\nˇcc\n", Mode::Normal);
205 cx.simulate_keystrokes("> 2 k");
206 cx.assert_editor_state(" aa\n bb\n ˇcc\n");
207
208 // works with repeat
209 cx.set_state("a\nb\nccˇc\n", Mode::Normal);
210 cx.simulate_keystrokes("> 2 k");
211 cx.assert_editor_state(" a\n b\n ccˇc\n");
212 cx.simulate_keystrokes(".");
213 cx.assert_editor_state(" a\n b\n ccˇc\n");
214 cx.simulate_keystrokes("v k <");
215 cx.assert_editor_state(" a\n bˇ\n ccc\n");
216 cx.simulate_keystrokes(".");
217 cx.assert_editor_state(" a\nbˇ\nccc\n");
218}
219
220#[gpui::test]
221async fn test_escape_command_palette(cx: &mut gpui::TestAppContext) {
222 let mut cx = VimTestContext::new(cx, true).await;
223
224 cx.set_state("aˇbc\n", Mode::Normal);
225 cx.simulate_keystrokes("i cmd-shift-p");
226
227 assert!(cx.workspace(|workspace, cx| workspace.active_modal::<CommandPalette>(cx).is_some()));
228 cx.simulate_keystrokes("escape");
229 cx.run_until_parked();
230 assert!(!cx.workspace(|workspace, cx| workspace.active_modal::<CommandPalette>(cx).is_some()));
231 cx.assert_state("aˇbc\n", Mode::Insert);
232}
233
234#[gpui::test]
235async fn test_escape_cancels(cx: &mut gpui::TestAppContext) {
236 let mut cx = VimTestContext::new(cx, true).await;
237
238 cx.set_state("aˇbˇc", Mode::Normal);
239 cx.simulate_keystrokes("escape");
240
241 cx.assert_state("aˇbc", Mode::Normal);
242}
243
244#[gpui::test]
245async fn test_selection_on_search(cx: &mut gpui::TestAppContext) {
246 let mut cx = VimTestContext::new(cx, true).await;
247
248 cx.set_state(indoc! {"aa\nbˇb\ncc\ncc\ncc\n"}, Mode::Normal);
249 cx.simulate_keystrokes("/ c c");
250
251 let search_bar = cx.workspace(|workspace, cx| {
252 workspace
253 .active_pane()
254 .read(cx)
255 .toolbar()
256 .read(cx)
257 .item_of_type::<BufferSearchBar>()
258 .expect("Buffer search bar should be deployed")
259 });
260
261 cx.update_view(search_bar, |bar, cx| {
262 assert_eq!(bar.query(cx), "cc");
263 });
264
265 cx.update_editor(|editor, cx| {
266 let highlights = editor.all_text_background_highlights(cx);
267 assert_eq!(3, highlights.len());
268 assert_eq!(
269 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 2),
270 highlights[0].0
271 )
272 });
273 cx.simulate_keystrokes("enter");
274
275 cx.assert_state(indoc! {"aa\nbb\nˇcc\ncc\ncc\n"}, Mode::Normal);
276 cx.simulate_keystrokes("n");
277 cx.assert_state(indoc! {"aa\nbb\ncc\nˇcc\ncc\n"}, Mode::Normal);
278 cx.simulate_keystrokes("shift-n");
279 cx.assert_state(indoc! {"aa\nbb\nˇcc\ncc\ncc\n"}, Mode::Normal);
280}
281
282#[gpui::test]
283async fn test_word_characters(cx: &mut gpui::TestAppContext) {
284 let mut cx = VimTestContext::new_typescript(cx).await;
285 cx.set_state(
286 indoc! { "
287 class A {
288 #ˇgoop = 99;
289 $ˇgoop () { return this.#gˇoop };
290 };
291 console.log(new A().$gooˇp())
292 "},
293 Mode::Normal,
294 );
295 cx.simulate_keystrokes("v i w");
296 cx.assert_state(
297 indoc! {"
298 class A {
299 «#goopˇ» = 99;
300 «$goopˇ» () { return this.«#goopˇ» };
301 };
302 console.log(new A().«$goopˇ»())
303 "},
304 Mode::Visual,
305 )
306}
307
308#[gpui::test]
309async fn test_join_lines(cx: &mut gpui::TestAppContext) {
310 let mut cx = NeovimBackedTestContext::new(cx).await;
311
312 cx.set_shared_state(indoc! {"
313 ˇone
314 two
315 three
316 four
317 five
318 six
319 "})
320 .await;
321 cx.simulate_shared_keystrokes("shift-j").await;
322 cx.shared_state().await.assert_eq(indoc! {"
323 oneˇ two
324 three
325 four
326 five
327 six
328 "});
329 cx.simulate_shared_keystrokes("3 shift-j").await;
330 cx.shared_state().await.assert_eq(indoc! {"
331 one two threeˇ four
332 five
333 six
334 "});
335
336 cx.set_shared_state(indoc! {"
337 ˇone
338 two
339 three
340 four
341 five
342 six
343 "})
344 .await;
345 cx.simulate_shared_keystrokes("j v 3 j shift-j").await;
346 cx.shared_state().await.assert_eq(indoc! {"
347 one
348 two three fourˇ five
349 six
350 "});
351}
352
353#[cfg(target_os = "macos")]
354#[gpui::test]
355async fn test_wrapped_lines(cx: &mut gpui::TestAppContext) {
356 let mut cx = NeovimBackedTestContext::new(cx).await;
357
358 cx.set_shared_wrap(12).await;
359 // tests line wrap as follows:
360 // 1: twelve char
361 // twelve char
362 // 2: twelve char
363 cx.set_shared_state(indoc! { "
364 tˇwelve char twelve char
365 twelve char
366 "})
367 .await;
368 cx.simulate_shared_keystrokes("j").await;
369 cx.shared_state().await.assert_eq(indoc! {"
370 twelve char twelve char
371 tˇwelve char
372 "});
373 cx.simulate_shared_keystrokes("k").await;
374 cx.shared_state().await.assert_eq(indoc! {"
375 tˇwelve char twelve char
376 twelve char
377 "});
378 cx.simulate_shared_keystrokes("g j").await;
379 cx.shared_state().await.assert_eq(indoc! {"
380 twelve char tˇwelve char
381 twelve char
382 "});
383 cx.simulate_shared_keystrokes("g j").await;
384 cx.shared_state().await.assert_eq(indoc! {"
385 twelve char twelve char
386 tˇwelve char
387 "});
388
389 cx.simulate_shared_keystrokes("g k").await;
390 cx.shared_state().await.assert_eq(indoc! {"
391 twelve char tˇwelve char
392 twelve char
393 "});
394
395 cx.simulate_shared_keystrokes("g ^").await;
396 cx.shared_state().await.assert_eq(indoc! {"
397 twelve char ˇtwelve char
398 twelve char
399 "});
400
401 cx.simulate_shared_keystrokes("^").await;
402 cx.shared_state().await.assert_eq(indoc! {"
403 ˇtwelve char twelve char
404 twelve char
405 "});
406
407 cx.simulate_shared_keystrokes("g $").await;
408 cx.shared_state().await.assert_eq(indoc! {"
409 twelve charˇ twelve char
410 twelve char
411 "});
412 cx.simulate_shared_keystrokes("$").await;
413 cx.shared_state().await.assert_eq(indoc! {"
414 twelve char twelve chaˇr
415 twelve char
416 "});
417
418 cx.set_shared_state(indoc! { "
419 tˇwelve char twelve char
420 twelve char
421 "})
422 .await;
423 cx.simulate_shared_keystrokes("enter").await;
424 cx.shared_state().await.assert_eq(indoc! {"
425 twelve char twelve char
426 ˇtwelve char
427 "});
428
429 cx.set_shared_state(indoc! { "
430 twelve char
431 tˇwelve char twelve char
432 twelve char
433 "})
434 .await;
435 cx.simulate_shared_keystrokes("o o escape").await;
436 cx.shared_state().await.assert_eq(indoc! {"
437 twelve char
438 twelve char twelve char
439 ˇo
440 twelve char
441 "});
442
443 cx.set_shared_state(indoc! { "
444 twelve char
445 tˇwelve char twelve char
446 twelve char
447 "})
448 .await;
449 cx.simulate_shared_keystrokes("shift-a a escape").await;
450 cx.shared_state().await.assert_eq(indoc! {"
451 twelve char
452 twelve char twelve charˇa
453 twelve char
454 "});
455 cx.simulate_shared_keystrokes("shift-i i escape").await;
456 cx.shared_state().await.assert_eq(indoc! {"
457 twelve char
458 ˇitwelve char twelve chara
459 twelve char
460 "});
461 cx.simulate_shared_keystrokes("shift-d").await;
462 cx.shared_state().await.assert_eq(indoc! {"
463 twelve char
464 ˇ
465 twelve char
466 "});
467
468 cx.set_shared_state(indoc! { "
469 twelve char
470 twelve char tˇwelve char
471 twelve char
472 "})
473 .await;
474 cx.simulate_shared_keystrokes("shift-o o escape").await;
475 cx.shared_state().await.assert_eq(indoc! {"
476 twelve char
477 ˇo
478 twelve char twelve char
479 twelve char
480 "});
481
482 // line wraps as:
483 // fourteen ch
484 // ar
485 // fourteen ch
486 // ar
487 cx.set_shared_state(indoc! { "
488 fourteen chaˇr
489 fourteen char
490 "})
491 .await;
492
493 cx.simulate_shared_keystrokes("d i w").await;
494 cx.shared_state().await.assert_eq(indoc! {"
495 fourteenˇ•
496 fourteen char
497 "});
498 cx.simulate_shared_keystrokes("j shift-f e f r").await;
499 cx.shared_state().await.assert_eq(indoc! {"
500 fourteen•
501 fourteen chaˇr
502 "});
503}
504
505#[gpui::test]
506async fn test_folds(cx: &mut gpui::TestAppContext) {
507 let mut cx = NeovimBackedTestContext::new(cx).await;
508 cx.set_neovim_option("foldmethod=manual").await;
509
510 cx.set_shared_state(indoc! { "
511 fn boop() {
512 ˇbarp()
513 bazp()
514 }
515 "})
516 .await;
517 cx.simulate_shared_keystrokes("shift-v j z f").await;
518
519 // visual display is now:
520 // fn boop () {
521 // [FOLDED]
522 // }
523
524 // TODO: this should not be needed but currently zf does not
525 // return to normal mode.
526 cx.simulate_shared_keystrokes("escape").await;
527
528 // skip over fold downward
529 cx.simulate_shared_keystrokes("g g").await;
530 cx.shared_state().await.assert_eq(indoc! {"
531 ˇfn boop() {
532 barp()
533 bazp()
534 }
535 "});
536
537 cx.simulate_shared_keystrokes("j j").await;
538 cx.shared_state().await.assert_eq(indoc! {"
539 fn boop() {
540 barp()
541 bazp()
542 ˇ}
543 "});
544
545 // skip over fold upward
546 cx.simulate_shared_keystrokes("2 k").await;
547 cx.shared_state().await.assert_eq(indoc! {"
548 ˇfn boop() {
549 barp()
550 bazp()
551 }
552 "});
553
554 // yank the fold
555 cx.simulate_shared_keystrokes("down y y").await;
556 cx.shared_clipboard()
557 .await
558 .assert_eq(" barp()\n bazp()\n");
559
560 // re-open
561 cx.simulate_shared_keystrokes("z o").await;
562 cx.shared_state().await.assert_eq(indoc! {"
563 fn boop() {
564 ˇ barp()
565 bazp()
566 }
567 "});
568}
569
570#[gpui::test]
571async fn test_folds_panic(cx: &mut gpui::TestAppContext) {
572 let mut cx = NeovimBackedTestContext::new(cx).await;
573 cx.set_neovim_option("foldmethod=manual").await;
574
575 cx.set_shared_state(indoc! { "
576 fn boop() {
577 ˇbarp()
578 bazp()
579 }
580 "})
581 .await;
582 cx.simulate_shared_keystrokes("shift-v j z f").await;
583 cx.simulate_shared_keystrokes("escape").await;
584 cx.simulate_shared_keystrokes("g g").await;
585 cx.simulate_shared_keystrokes("5 d j").await;
586 cx.shared_state().await.assert_eq("ˇ");
587 cx.set_shared_state(indoc! {"
588 fn boop() {
589 ˇbarp()
590 bazp()
591 }
592 "})
593 .await;
594 cx.simulate_shared_keystrokes("shift-v j j z f").await;
595 cx.simulate_shared_keystrokes("escape").await;
596 cx.simulate_shared_keystrokes("shift-g shift-v").await;
597 cx.shared_state().await.assert_eq(indoc! {"
598 fn boop() {
599 barp()
600 bazp()
601 }
602 ˇ"});
603}
604
605#[gpui::test]
606async fn test_clear_counts(cx: &mut gpui::TestAppContext) {
607 let mut cx = NeovimBackedTestContext::new(cx).await;
608
609 cx.set_shared_state(indoc! {"
610 The quick brown
611 fox juˇmps over
612 the lazy dog"})
613 .await;
614
615 cx.simulate_shared_keystrokes("4 escape 3 d l").await;
616 cx.shared_state().await.assert_eq(indoc! {"
617 The quick brown
618 fox juˇ over
619 the lazy dog"});
620}
621
622#[gpui::test]
623async fn test_zero(cx: &mut gpui::TestAppContext) {
624 let mut cx = NeovimBackedTestContext::new(cx).await;
625
626 cx.set_shared_state(indoc! {"
627 The quˇick brown
628 fox jumps over
629 the lazy dog"})
630 .await;
631
632 cx.simulate_shared_keystrokes("0").await;
633 cx.shared_state().await.assert_eq(indoc! {"
634 ˇThe quick brown
635 fox jumps over
636 the lazy dog"});
637
638 cx.simulate_shared_keystrokes("1 0 l").await;
639 cx.shared_state().await.assert_eq(indoc! {"
640 The quick ˇbrown
641 fox jumps over
642 the lazy dog"});
643}
644
645#[gpui::test]
646async fn test_selection_goal(cx: &mut gpui::TestAppContext) {
647 let mut cx = NeovimBackedTestContext::new(cx).await;
648
649 cx.set_shared_state(indoc! {"
650 ;;ˇ;
651 Lorem Ipsum"})
652 .await;
653
654 cx.simulate_shared_keystrokes("a down up ; down up").await;
655 cx.shared_state().await.assert_eq(indoc! {"
656 ;;;;ˇ
657 Lorem Ipsum"});
658}
659
660#[cfg(target_os = "macos")]
661#[gpui::test]
662async fn test_wrapped_motions(cx: &mut gpui::TestAppContext) {
663 let mut cx = NeovimBackedTestContext::new(cx).await;
664
665 cx.set_shared_wrap(12).await;
666
667 cx.set_shared_state(indoc! {"
668 aaˇaa
669 😃😃"
670 })
671 .await;
672 cx.simulate_shared_keystrokes("j").await;
673 cx.shared_state().await.assert_eq(indoc! {"
674 aaaa
675 😃ˇ😃"
676 });
677
678 cx.set_shared_state(indoc! {"
679 123456789012aaˇaa
680 123456789012😃😃"
681 })
682 .await;
683 cx.simulate_shared_keystrokes("j").await;
684 cx.shared_state().await.assert_eq(indoc! {"
685 123456789012aaaa
686 123456789012😃ˇ😃"
687 });
688
689 cx.set_shared_state(indoc! {"
690 123456789012aaˇaa
691 123456789012😃😃"
692 })
693 .await;
694 cx.simulate_shared_keystrokes("j").await;
695 cx.shared_state().await.assert_eq(indoc! {"
696 123456789012aaaa
697 123456789012😃ˇ😃"
698 });
699
700 cx.set_shared_state(indoc! {"
701 123456789012aaaaˇaaaaaaaa123456789012
702 wow
703 123456789012😃😃😃😃😃😃123456789012"
704 })
705 .await;
706 cx.simulate_shared_keystrokes("j j").await;
707 cx.shared_state().await.assert_eq(indoc! {"
708 123456789012aaaaaaaaaaaa123456789012
709 wow
710 123456789012😃😃ˇ😃😃😃😃123456789012"
711 });
712}
713
714#[gpui::test]
715async fn test_paragraphs_dont_wrap(cx: &mut gpui::TestAppContext) {
716 let mut cx = NeovimBackedTestContext::new(cx).await;
717
718 cx.set_shared_state(indoc! {"
719 one
720 ˇ
721 two"})
722 .await;
723
724 cx.simulate_shared_keystrokes("} }").await;
725 cx.shared_state().await.assert_eq(indoc! {"
726 one
727
728 twˇo"});
729
730 cx.simulate_shared_keystrokes("{ { {").await;
731 cx.shared_state().await.assert_eq(indoc! {"
732 ˇone
733
734 two"});
735}
736
737#[gpui::test]
738async fn test_select_all_issue_2170(cx: &mut gpui::TestAppContext) {
739 let mut cx = VimTestContext::new(cx, true).await;
740
741 cx.set_state(
742 indoc! {"
743 defmodule Test do
744 def test(a, ˇ[_, _] = b), do: IO.puts('hi')
745 end
746 "},
747 Mode::Normal,
748 );
749 cx.simulate_keystrokes("g a");
750 cx.assert_state(
751 indoc! {"
752 defmodule Test do
753 def test(a, «[ˇ»_, _] = b), do: IO.puts('hi')
754 end
755 "},
756 Mode::Visual,
757 );
758}
759
760#[gpui::test]
761async fn test_jk(cx: &mut gpui::TestAppContext) {
762 let mut cx = NeovimBackedTestContext::new(cx).await;
763
764 cx.update(|cx| {
765 cx.bind_keys([KeyBinding::new(
766 "j k",
767 NormalBefore,
768 Some("vim_mode == insert"),
769 )])
770 });
771 cx.neovim.exec("imap jk <esc>").await;
772
773 cx.set_shared_state("ˇhello").await;
774 cx.simulate_shared_keystrokes("i j o j k").await;
775 cx.shared_state().await.assert_eq("jˇohello");
776}
777
778#[gpui::test]
779async fn test_jk_delay(cx: &mut gpui::TestAppContext) {
780 let mut cx = VimTestContext::new(cx, true).await;
781
782 cx.update(|cx| {
783 cx.bind_keys([KeyBinding::new(
784 "j k",
785 NormalBefore,
786 Some("vim_mode == insert"),
787 )])
788 });
789
790 cx.set_state("ˇhello", Mode::Normal);
791 cx.simulate_keystrokes("i j");
792 cx.executor().advance_clock(Duration::from_millis(500));
793 cx.run_until_parked();
794 cx.assert_state("ˇhello", Mode::Insert);
795 cx.executor().advance_clock(Duration::from_millis(500));
796 cx.run_until_parked();
797 cx.assert_state("jˇhello", Mode::Insert);
798 cx.simulate_keystrokes("k j k");
799 cx.assert_state("jˇkhello", Mode::Normal);
800}
801
802#[gpui::test]
803async fn test_comma_w(cx: &mut gpui::TestAppContext) {
804 let mut cx = NeovimBackedTestContext::new(cx).await;
805
806 cx.update(|cx| {
807 cx.bind_keys([KeyBinding::new(
808 ", w",
809 motion::Down {
810 display_lines: false,
811 },
812 Some("vim_mode == normal"),
813 )])
814 });
815 cx.neovim.exec("map ,w j").await;
816
817 cx.set_shared_state("ˇhello hello\nhello hello").await;
818 cx.simulate_shared_keystrokes("f o ; , w").await;
819 cx.shared_state()
820 .await
821 .assert_eq("hello hello\nhello hellˇo");
822
823 cx.set_shared_state("ˇhello hello\nhello hello").await;
824 cx.simulate_shared_keystrokes("f o ; , i").await;
825 cx.shared_state()
826 .await
827 .assert_eq("hellˇo hello\nhello hello");
828}
829
830#[gpui::test]
831async fn test_rename(cx: &mut gpui::TestAppContext) {
832 let mut cx = VimTestContext::new_typescript(cx).await;
833
834 cx.set_state("const beˇfore = 2; console.log(before)", Mode::Normal);
835 let def_range = cx.lsp_range("const «beforeˇ» = 2; console.log(before)");
836 let tgt_range = cx.lsp_range("const before = 2; console.log(«beforeˇ»)");
837 let mut prepare_request =
838 cx.handle_request::<lsp::request::PrepareRenameRequest, _, _>(move |_, _, _| async move {
839 Ok(Some(lsp::PrepareRenameResponse::Range(def_range)))
840 });
841 let mut rename_request =
842 cx.handle_request::<lsp::request::Rename, _, _>(move |url, params, _| async move {
843 Ok(Some(lsp::WorkspaceEdit {
844 changes: Some(
845 [(
846 url.clone(),
847 vec![
848 lsp::TextEdit::new(def_range, params.new_name.clone()),
849 lsp::TextEdit::new(tgt_range, params.new_name),
850 ],
851 )]
852 .into(),
853 ),
854 ..Default::default()
855 }))
856 });
857
858 cx.simulate_keystrokes("c d");
859 prepare_request.next().await.unwrap();
860 cx.simulate_input("after");
861 cx.simulate_keystrokes("enter");
862 rename_request.next().await.unwrap();
863 cx.assert_state("const afterˇ = 2; console.log(after)", Mode::Normal)
864}
865
866// TODO: this test is flaky on our linux CI machines
867#[cfg(target_os = "macos")]
868#[gpui::test]
869async fn test_remap(cx: &mut gpui::TestAppContext) {
870 let mut cx = VimTestContext::new(cx, true).await;
871
872 // test moving the cursor
873 cx.update(|cx| {
874 cx.bind_keys([KeyBinding::new(
875 "g z",
876 workspace::SendKeystrokes("l l l l".to_string()),
877 None,
878 )])
879 });
880 cx.set_state("ˇ123456789", Mode::Normal);
881 cx.simulate_keystrokes("g z");
882 cx.assert_state("1234ˇ56789", Mode::Normal);
883
884 // test switching modes
885 cx.update(|cx| {
886 cx.bind_keys([KeyBinding::new(
887 "g y",
888 workspace::SendKeystrokes("i f o o escape l".to_string()),
889 None,
890 )])
891 });
892 cx.set_state("ˇ123456789", Mode::Normal);
893 cx.simulate_keystrokes("g y");
894 cx.assert_state("fooˇ123456789", Mode::Normal);
895
896 // test recursion
897 cx.update(|cx| {
898 cx.bind_keys([KeyBinding::new(
899 "g x",
900 workspace::SendKeystrokes("g z g y".to_string()),
901 None,
902 )])
903 });
904 cx.set_state("ˇ123456789", Mode::Normal);
905 cx.simulate_keystrokes("g x");
906 cx.assert_state("1234fooˇ56789", Mode::Normal);
907
908 cx.executor().allow_parking();
909
910 // test command
911 cx.update(|cx| {
912 cx.bind_keys([KeyBinding::new(
913 "g w",
914 workspace::SendKeystrokes(": j enter".to_string()),
915 None,
916 )])
917 });
918 cx.set_state("ˇ1234\n56789", Mode::Normal);
919 cx.simulate_keystrokes("g w");
920 cx.assert_state("1234ˇ 56789", Mode::Normal);
921
922 // test leaving command
923 cx.update(|cx| {
924 cx.bind_keys([KeyBinding::new(
925 "g u",
926 workspace::SendKeystrokes("g w g z".to_string()),
927 None,
928 )])
929 });
930 cx.set_state("ˇ1234\n56789", Mode::Normal);
931 cx.simulate_keystrokes("g u");
932 cx.assert_state("1234 567ˇ89", Mode::Normal);
933
934 // test leaving command
935 cx.update(|cx| {
936 cx.bind_keys([KeyBinding::new(
937 "g t",
938 workspace::SendKeystrokes("i space escape".to_string()),
939 None,
940 )])
941 });
942 cx.set_state("12ˇ34", Mode::Normal);
943 cx.simulate_keystrokes("g t");
944 cx.assert_state("12ˇ 34", Mode::Normal);
945}
946
947#[gpui::test]
948async fn test_undo(cx: &mut gpui::TestAppContext) {
949 let mut cx = NeovimBackedTestContext::new(cx).await;
950
951 cx.set_shared_state("hello quˇoel world").await;
952 cx.simulate_shared_keystrokes("v i w s c o escape u").await;
953 cx.shared_state().await.assert_eq("hello ˇquoel world");
954 cx.simulate_shared_keystrokes("ctrl-r").await;
955 cx.shared_state().await.assert_eq("hello ˇco world");
956 cx.simulate_shared_keystrokes("a o right l escape").await;
957 cx.shared_state().await.assert_eq("hello cooˇl world");
958 cx.simulate_shared_keystrokes("u").await;
959 cx.shared_state().await.assert_eq("hello cooˇ world");
960 cx.simulate_shared_keystrokes("u").await;
961 cx.shared_state().await.assert_eq("hello cˇo world");
962 cx.simulate_shared_keystrokes("u").await;
963 cx.shared_state().await.assert_eq("hello ˇquoel world");
964
965 cx.set_shared_state("hello quˇoel world").await;
966 cx.simulate_shared_keystrokes("v i w ~ u").await;
967 cx.shared_state().await.assert_eq("hello ˇquoel world");
968
969 cx.set_shared_state("\nhello quˇoel world\n").await;
970 cx.simulate_shared_keystrokes("shift-v s c escape u").await;
971 cx.shared_state().await.assert_eq("\nˇhello quoel world\n");
972
973 cx.set_shared_state(indoc! {"
974 ˇ1
975 2
976 3"})
977 .await;
978
979 cx.simulate_shared_keystrokes("ctrl-v shift-g ctrl-a").await;
980 cx.shared_state().await.assert_eq(indoc! {"
981 ˇ2
982 3
983 4"});
984
985 cx.simulate_shared_keystrokes("u").await;
986 cx.shared_state().await.assert_eq(indoc! {"
987 ˇ1
988 2
989 3"});
990}
991
992#[gpui::test]
993async fn test_mouse_selection(cx: &mut TestAppContext) {
994 let mut cx = VimTestContext::new(cx, true).await;
995
996 cx.set_state("ˇone two three", Mode::Normal);
997
998 let start_point = cx.pixel_position("one twˇo three");
999 let end_point = cx.pixel_position("one ˇtwo three");
1000
1001 cx.simulate_mouse_down(start_point, MouseButton::Left, Modifiers::none());
1002 cx.simulate_mouse_move(end_point, MouseButton::Left, Modifiers::none());
1003 cx.simulate_mouse_up(end_point, MouseButton::Left, Modifiers::none());
1004
1005 cx.assert_state("one «ˇtwo» three", Mode::Visual)
1006}
1007
1008#[gpui::test]
1009async fn test_lowercase_marks(cx: &mut TestAppContext) {
1010 let mut cx = NeovimBackedTestContext::new(cx).await;
1011
1012 cx.set_shared_state("line one\nline ˇtwo\nline three").await;
1013 cx.simulate_shared_keystrokes("m a l ' a").await;
1014 cx.shared_state()
1015 .await
1016 .assert_eq("line one\nˇline two\nline three");
1017 cx.simulate_shared_keystrokes("` a").await;
1018 cx.shared_state()
1019 .await
1020 .assert_eq("line one\nline ˇtwo\nline three");
1021
1022 cx.simulate_shared_keystrokes("^ d ` a").await;
1023 cx.shared_state()
1024 .await
1025 .assert_eq("line one\nˇtwo\nline three");
1026}
1027
1028#[gpui::test]
1029async fn test_lt_gt_marks(cx: &mut TestAppContext) {
1030 let mut cx = NeovimBackedTestContext::new(cx).await;
1031
1032 cx.set_shared_state(indoc!(
1033 "
1034 Line one
1035 Line two
1036 Line ˇthree
1037 Line four
1038 Line five
1039 "
1040 ))
1041 .await;
1042
1043 cx.simulate_shared_keystrokes("v j escape k k").await;
1044
1045 cx.simulate_shared_keystrokes("' <").await;
1046 cx.shared_state().await.assert_eq(indoc! {"
1047 Line one
1048 Line two
1049 ˇLine three
1050 Line four
1051 Line five
1052 "});
1053
1054 cx.simulate_shared_keystrokes("` <").await;
1055 cx.shared_state().await.assert_eq(indoc! {"
1056 Line one
1057 Line two
1058 Line ˇthree
1059 Line four
1060 Line five
1061 "});
1062
1063 cx.simulate_shared_keystrokes("' >").await;
1064 cx.shared_state().await.assert_eq(indoc! {"
1065 Line one
1066 Line two
1067 Line three
1068 ˇLine four
1069 Line five
1070 "
1071 });
1072
1073 cx.simulate_shared_keystrokes("` >").await;
1074 cx.shared_state().await.assert_eq(indoc! {"
1075 Line one
1076 Line two
1077 Line three
1078 Line ˇfour
1079 Line five
1080 "
1081 });
1082
1083 cx.simulate_shared_keystrokes("v i w o escape").await;
1084 cx.simulate_shared_keystrokes("` >").await;
1085 cx.shared_state().await.assert_eq(indoc! {"
1086 Line one
1087 Line two
1088 Line three
1089 Line fouˇr
1090 Line five
1091 "
1092 });
1093 cx.simulate_shared_keystrokes("` <").await;
1094 cx.shared_state().await.assert_eq(indoc! {"
1095 Line one
1096 Line two
1097 Line three
1098 Line ˇfour
1099 Line five
1100 "
1101 });
1102}
1103
1104#[gpui::test]
1105async fn test_caret_mark(cx: &mut TestAppContext) {
1106 let mut cx = NeovimBackedTestContext::new(cx).await;
1107
1108 cx.set_shared_state(indoc!(
1109 "
1110 Line one
1111 Line two
1112 Line three
1113 ˇLine four
1114 Line five
1115 "
1116 ))
1117 .await;
1118
1119 cx.simulate_shared_keystrokes("c w shift-s t r a i g h t space t h i n g escape j j")
1120 .await;
1121
1122 cx.simulate_shared_keystrokes("' ^").await;
1123 cx.shared_state().await.assert_eq(indoc! {"
1124 Line one
1125 Line two
1126 Line three
1127 ˇStraight thing four
1128 Line five
1129 "
1130 });
1131
1132 cx.simulate_shared_keystrokes("` ^").await;
1133 cx.shared_state().await.assert_eq(indoc! {"
1134 Line one
1135 Line two
1136 Line three
1137 Straight thingˇ four
1138 Line five
1139 "
1140 });
1141
1142 cx.simulate_shared_keystrokes("k a ! escape k g i ?").await;
1143 cx.shared_state().await.assert_eq(indoc! {"
1144 Line one
1145 Line two
1146 Line three!?ˇ
1147 Straight thing four
1148 Line five
1149 "
1150 });
1151}
1152
1153#[cfg(target_os = "macos")]
1154#[gpui::test]
1155async fn test_dw_eol(cx: &mut gpui::TestAppContext) {
1156 let mut cx = NeovimBackedTestContext::new(cx).await;
1157
1158 cx.set_shared_wrap(12).await;
1159 cx.set_shared_state("twelve ˇchar twelve char\ntwelve char")
1160 .await;
1161 cx.simulate_shared_keystrokes("d w").await;
1162 cx.shared_state()
1163 .await
1164 .assert_eq("twelve ˇtwelve char\ntwelve char");
1165}
1166
1167#[gpui::test]
1168async fn test_toggle_comments(cx: &mut gpui::TestAppContext) {
1169 let mut cx = VimTestContext::new(cx, true).await;
1170
1171 let language = std::sync::Arc::new(language::Language::new(
1172 language::LanguageConfig {
1173 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
1174 ..Default::default()
1175 },
1176 Some(language::tree_sitter_rust::language()),
1177 ));
1178 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
1179
1180 // works in normal model
1181 cx.set_state(
1182 indoc! {"
1183 ˇone
1184 two
1185 three
1186 "},
1187 Mode::Normal,
1188 );
1189 cx.simulate_keystrokes("g c c");
1190 cx.assert_state(
1191 indoc! {"
1192 // ˇone
1193 two
1194 three
1195 "},
1196 Mode::Normal,
1197 );
1198
1199 // works in visual mode
1200 cx.simulate_keystrokes("v j g c");
1201 cx.assert_state(
1202 indoc! {"
1203 // // ˇone
1204 // two
1205 three
1206 "},
1207 Mode::Normal,
1208 );
1209
1210 // works in visual line mode
1211 cx.simulate_keystrokes("shift-v j g c");
1212 cx.assert_state(
1213 indoc! {"
1214 // ˇone
1215 two
1216 three
1217 "},
1218 Mode::Normal,
1219 );
1220
1221 // works with count
1222 cx.simulate_keystrokes("g c 2 j");
1223 cx.assert_state(
1224 indoc! {"
1225 // // ˇone
1226 // two
1227 // three
1228 "},
1229 Mode::Normal,
1230 );
1231
1232 // works with motion object
1233 cx.simulate_keystrokes("shift-g");
1234 cx.simulate_keystrokes("g c g g");
1235 cx.assert_state(
1236 indoc! {"
1237 // one
1238 two
1239 three
1240 ˇ"},
1241 Mode::Normal,
1242 );
1243}
1244
1245#[gpui::test]
1246async fn test_find_multibyte(cx: &mut gpui::TestAppContext) {
1247 let mut cx = NeovimBackedTestContext::new(cx).await;
1248
1249 cx.set_shared_state(r#"<label for="guests">ˇPočet hostů</label>"#)
1250 .await;
1251
1252 cx.simulate_shared_keystrokes("c t < o escape").await;
1253 cx.shared_state()
1254 .await
1255 .assert_eq(r#"<label for="guests">ˇo</label>"#);
1256}
1257
1258#[gpui::test]
1259async fn test_plus_minus(cx: &mut gpui::TestAppContext) {
1260 let mut cx = NeovimBackedTestContext::new(cx).await;
1261
1262 cx.set_shared_state(indoc! {
1263 "one
1264 two
1265 thrˇee
1266 "})
1267 .await;
1268
1269 cx.simulate_shared_keystrokes("-").await;
1270 cx.shared_state().await.assert_matches();
1271 cx.simulate_shared_keystrokes("-").await;
1272 cx.shared_state().await.assert_matches();
1273 cx.simulate_shared_keystrokes("+").await;
1274 cx.shared_state().await.assert_matches();
1275}
1276
1277#[gpui::test]
1278async fn test_command_alias(cx: &mut gpui::TestAppContext) {
1279 let mut cx = VimTestContext::new(cx, true).await;
1280 cx.update_global(|store: &mut SettingsStore, cx| {
1281 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
1282 let mut aliases = HashMap::default();
1283 aliases.insert("Q".to_string(), "upper".to_string());
1284 s.command_aliases = Some(aliases)
1285 });
1286 });
1287
1288 cx.set_state("ˇhello world", Mode::Normal);
1289 cx.simulate_keystrokes(": Q");
1290 cx.set_state("ˇHello world", Mode::Normal);
1291}
1292
1293#[gpui::test]
1294async fn test_remap_adjacent_dog_cat(cx: &mut gpui::TestAppContext) {
1295 let mut cx = NeovimBackedTestContext::new(cx).await;
1296 cx.update(|cx| {
1297 cx.bind_keys([
1298 KeyBinding::new(
1299 "d o g",
1300 workspace::SendKeystrokes("🐶".to_string()),
1301 Some("vim_mode == insert"),
1302 ),
1303 KeyBinding::new(
1304 "c a t",
1305 workspace::SendKeystrokes("🐱".to_string()),
1306 Some("vim_mode == insert"),
1307 ),
1308 ])
1309 });
1310 cx.neovim.exec("imap dog 🐶").await;
1311 cx.neovim.exec("imap cat 🐱").await;
1312
1313 cx.set_shared_state("ˇ").await;
1314 cx.simulate_shared_keystrokes("i d o g").await;
1315 cx.shared_state().await.assert_eq("🐶ˇ");
1316
1317 cx.set_shared_state("ˇ").await;
1318 cx.simulate_shared_keystrokes("i d o d o g").await;
1319 cx.shared_state().await.assert_eq("do🐶ˇ");
1320
1321 cx.set_shared_state("ˇ").await;
1322 cx.simulate_shared_keystrokes("i d o c a t").await;
1323 cx.shared_state().await.assert_eq("do🐱ˇ");
1324}
1325
1326#[gpui::test]
1327async fn test_remap_nested_pineapple(cx: &mut gpui::TestAppContext) {
1328 let mut cx = NeovimBackedTestContext::new(cx).await;
1329 cx.update(|cx| {
1330 cx.bind_keys([
1331 KeyBinding::new(
1332 "p i n",
1333 workspace::SendKeystrokes("📌".to_string()),
1334 Some("vim_mode == insert"),
1335 ),
1336 KeyBinding::new(
1337 "p i n e",
1338 workspace::SendKeystrokes("🌲".to_string()),
1339 Some("vim_mode == insert"),
1340 ),
1341 KeyBinding::new(
1342 "p i n e a p p l e",
1343 workspace::SendKeystrokes("🍍".to_string()),
1344 Some("vim_mode == insert"),
1345 ),
1346 ])
1347 });
1348 cx.neovim.exec("imap pin 📌").await;
1349 cx.neovim.exec("imap pine 🌲").await;
1350 cx.neovim.exec("imap pineapple 🍍").await;
1351
1352 cx.set_shared_state("ˇ").await;
1353 cx.simulate_shared_keystrokes("i p i n").await;
1354 cx.executor().advance_clock(Duration::from_millis(1000));
1355 cx.run_until_parked();
1356 cx.shared_state().await.assert_eq("📌ˇ");
1357
1358 cx.set_shared_state("ˇ").await;
1359 cx.simulate_shared_keystrokes("i p i n e").await;
1360 cx.executor().advance_clock(Duration::from_millis(1000));
1361 cx.run_until_parked();
1362 cx.shared_state().await.assert_eq("🌲ˇ");
1363
1364 cx.set_shared_state("ˇ").await;
1365 cx.simulate_shared_keystrokes("i p i n e a p p l e").await;
1366 cx.shared_state().await.assert_eq("🍍ˇ");
1367}
1368
1369#[gpui::test]
1370async fn test_escape_while_waiting(cx: &mut gpui::TestAppContext) {
1371 let mut cx = NeovimBackedTestContext::new(cx).await;
1372 cx.set_shared_state("ˇhi").await;
1373 cx.simulate_shared_keystrokes("\" + escape x").await;
1374 cx.shared_state().await.assert_eq("ˇi");
1375}
1376
1377#[gpui::test]
1378async fn test_ctrl_w_override(cx: &mut gpui::TestAppContext) {
1379 let mut cx = NeovimBackedTestContext::new(cx).await;
1380 cx.update(|cx| {
1381 cx.bind_keys([KeyBinding::new("ctrl-w", DeleteLine, None)]);
1382 });
1383 cx.neovim.exec("map <c-w> D").await;
1384 cx.set_shared_state("ˇhi").await;
1385 cx.simulate_shared_keystrokes("ctrl-w").await;
1386 cx.shared_state().await.assert_eq("ˇ");
1387}
1388
1389#[gpui::test]
1390async fn test_visual_indent_count(cx: &mut gpui::TestAppContext) {
1391 let mut cx = VimTestContext::new(cx, true).await;
1392 cx.set_state("ˇhi", Mode::Normal);
1393 cx.simulate_keystrokes("shift-v 3 >");
1394 cx.assert_state(" ˇhi", Mode::Normal);
1395 cx.simulate_keystrokes("shift-v 2 <");
1396 cx.assert_state(" ˇhi", Mode::Normal);
1397}
1398
1399#[gpui::test]
1400async fn test_record_replay_recursion(cx: &mut gpui::TestAppContext) {
1401 let mut cx = NeovimBackedTestContext::new(cx).await;
1402
1403 cx.set_shared_state("ˇhello world").await;
1404 cx.simulate_shared_keystrokes(">").await;
1405 cx.simulate_shared_keystrokes(".").await;
1406 cx.simulate_shared_keystrokes(".").await;
1407 cx.simulate_shared_keystrokes(".").await;
1408 cx.shared_state().await.assert_eq("ˇhello world"); // takes a _long_ time
1409}