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, ModeIndicator};
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.set_state("«hjklˇ»", Mode::Normal);
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_status_indicator(cx: &mut gpui::TestAppContext) {
284 let mut cx = VimTestContext::new(cx, true).await;
285
286 let mode_indicator = cx.workspace(|workspace, cx| {
287 let status_bar = workspace.status_bar().read(cx);
288 let mode_indicator = status_bar.item_of_type::<ModeIndicator>();
289 assert!(mode_indicator.is_some());
290 mode_indicator.unwrap()
291 });
292
293 assert_eq!(
294 cx.workspace(|_, cx| mode_indicator.read(cx).mode),
295 Some(Mode::Normal)
296 );
297
298 // shows the correct mode
299 cx.simulate_keystrokes("i");
300 assert_eq!(
301 cx.workspace(|_, cx| mode_indicator.read(cx).mode),
302 Some(Mode::Insert)
303 );
304 cx.simulate_keystrokes("escape shift-r");
305 assert_eq!(
306 cx.workspace(|_, cx| mode_indicator.read(cx).mode),
307 Some(Mode::Replace)
308 );
309
310 // shows even in search
311 cx.simulate_keystrokes("escape v /");
312 assert_eq!(
313 cx.workspace(|_, cx| mode_indicator.read(cx).mode),
314 Some(Mode::Visual)
315 );
316
317 // hides if vim mode is disabled
318 cx.disable_vim();
319 cx.run_until_parked();
320 cx.workspace(|workspace, cx| {
321 let status_bar = workspace.status_bar().read(cx);
322 let mode_indicator = status_bar.item_of_type::<ModeIndicator>().unwrap();
323 assert!(mode_indicator.read(cx).mode.is_none());
324 });
325
326 cx.enable_vim();
327 cx.run_until_parked();
328 cx.workspace(|workspace, cx| {
329 let status_bar = workspace.status_bar().read(cx);
330 let mode_indicator = status_bar.item_of_type::<ModeIndicator>().unwrap();
331 assert!(mode_indicator.read(cx).mode.is_some());
332 });
333}
334
335#[gpui::test]
336async fn test_word_characters(cx: &mut gpui::TestAppContext) {
337 let mut cx = VimTestContext::new_typescript(cx).await;
338 cx.set_state(
339 indoc! { "
340 class A {
341 #ˇgoop = 99;
342 $ˇgoop () { return this.#gˇoop };
343 };
344 console.log(new A().$gooˇp())
345 "},
346 Mode::Normal,
347 );
348 cx.simulate_keystrokes("v i w");
349 cx.assert_state(
350 indoc! {"
351 class A {
352 «#goopˇ» = 99;
353 «$goopˇ» () { return this.«#goopˇ» };
354 };
355 console.log(new A().«$goopˇ»())
356 "},
357 Mode::Visual,
358 )
359}
360
361#[gpui::test]
362async fn test_join_lines(cx: &mut gpui::TestAppContext) {
363 let mut cx = NeovimBackedTestContext::new(cx).await;
364
365 cx.set_shared_state(indoc! {"
366 ˇone
367 two
368 three
369 four
370 five
371 six
372 "})
373 .await;
374 cx.simulate_shared_keystrokes("shift-j").await;
375 cx.shared_state().await.assert_eq(indoc! {"
376 oneˇ two
377 three
378 four
379 five
380 six
381 "});
382 cx.simulate_shared_keystrokes("3 shift-j").await;
383 cx.shared_state().await.assert_eq(indoc! {"
384 one two threeˇ four
385 five
386 six
387 "});
388
389 cx.set_shared_state(indoc! {"
390 ˇone
391 two
392 three
393 four
394 five
395 six
396 "})
397 .await;
398 cx.simulate_shared_keystrokes("j v 3 j shift-j").await;
399 cx.shared_state().await.assert_eq(indoc! {"
400 one
401 two three fourˇ five
402 six
403 "});
404}
405
406#[cfg(target_os = "macos")]
407#[gpui::test]
408async fn test_wrapped_lines(cx: &mut gpui::TestAppContext) {
409 let mut cx = NeovimBackedTestContext::new(cx).await;
410
411 cx.set_shared_wrap(12).await;
412 // tests line wrap as follows:
413 // 1: twelve char
414 // twelve char
415 // 2: twelve char
416 cx.set_shared_state(indoc! { "
417 tˇwelve char twelve char
418 twelve char
419 "})
420 .await;
421 cx.simulate_shared_keystrokes("j").await;
422 cx.shared_state().await.assert_eq(indoc! {"
423 twelve char twelve char
424 tˇwelve char
425 "});
426 cx.simulate_shared_keystrokes("k").await;
427 cx.shared_state().await.assert_eq(indoc! {"
428 tˇwelve char twelve char
429 twelve char
430 "});
431 cx.simulate_shared_keystrokes("g j").await;
432 cx.shared_state().await.assert_eq(indoc! {"
433 twelve char tˇwelve char
434 twelve char
435 "});
436 cx.simulate_shared_keystrokes("g j").await;
437 cx.shared_state().await.assert_eq(indoc! {"
438 twelve char twelve char
439 tˇwelve char
440 "});
441
442 cx.simulate_shared_keystrokes("g k").await;
443 cx.shared_state().await.assert_eq(indoc! {"
444 twelve char tˇwelve char
445 twelve char
446 "});
447
448 cx.simulate_shared_keystrokes("g ^").await;
449 cx.shared_state().await.assert_eq(indoc! {"
450 twelve char ˇtwelve char
451 twelve char
452 "});
453
454 cx.simulate_shared_keystrokes("^").await;
455 cx.shared_state().await.assert_eq(indoc! {"
456 ˇtwelve char twelve char
457 twelve char
458 "});
459
460 cx.simulate_shared_keystrokes("g $").await;
461 cx.shared_state().await.assert_eq(indoc! {"
462 twelve charˇ twelve char
463 twelve char
464 "});
465 cx.simulate_shared_keystrokes("$").await;
466 cx.shared_state().await.assert_eq(indoc! {"
467 twelve char twelve chaˇr
468 twelve char
469 "});
470
471 cx.set_shared_state(indoc! { "
472 tˇwelve char twelve char
473 twelve char
474 "})
475 .await;
476 cx.simulate_shared_keystrokes("enter").await;
477 cx.shared_state().await.assert_eq(indoc! {"
478 twelve char twelve char
479 ˇtwelve char
480 "});
481
482 cx.set_shared_state(indoc! { "
483 twelve char
484 tˇwelve char twelve char
485 twelve char
486 "})
487 .await;
488 cx.simulate_shared_keystrokes("o o escape").await;
489 cx.shared_state().await.assert_eq(indoc! {"
490 twelve char
491 twelve char twelve char
492 ˇo
493 twelve char
494 "});
495
496 cx.set_shared_state(indoc! { "
497 twelve char
498 tˇwelve char twelve char
499 twelve char
500 "})
501 .await;
502 cx.simulate_shared_keystrokes("shift-a a escape").await;
503 cx.shared_state().await.assert_eq(indoc! {"
504 twelve char
505 twelve char twelve charˇa
506 twelve char
507 "});
508 cx.simulate_shared_keystrokes("shift-i i escape").await;
509 cx.shared_state().await.assert_eq(indoc! {"
510 twelve char
511 ˇitwelve char twelve chara
512 twelve char
513 "});
514 cx.simulate_shared_keystrokes("shift-d").await;
515 cx.shared_state().await.assert_eq(indoc! {"
516 twelve char
517 ˇ
518 twelve char
519 "});
520
521 cx.set_shared_state(indoc! { "
522 twelve char
523 twelve char tˇwelve char
524 twelve char
525 "})
526 .await;
527 cx.simulate_shared_keystrokes("shift-o o escape").await;
528 cx.shared_state().await.assert_eq(indoc! {"
529 twelve char
530 ˇo
531 twelve char twelve char
532 twelve char
533 "});
534
535 // line wraps as:
536 // fourteen ch
537 // ar
538 // fourteen ch
539 // ar
540 cx.set_shared_state(indoc! { "
541 fourteen chaˇr
542 fourteen char
543 "})
544 .await;
545
546 cx.simulate_shared_keystrokes("d i w").await;
547 cx.shared_state().await.assert_eq(indoc! {"
548 fourteenˇ•
549 fourteen char
550 "});
551 cx.simulate_shared_keystrokes("j shift-f e f r").await;
552 cx.shared_state().await.assert_eq(indoc! {"
553 fourteen•
554 fourteen chaˇr
555 "});
556}
557
558#[gpui::test]
559async fn test_folds(cx: &mut gpui::TestAppContext) {
560 let mut cx = NeovimBackedTestContext::new(cx).await;
561 cx.set_neovim_option("foldmethod=manual").await;
562
563 cx.set_shared_state(indoc! { "
564 fn boop() {
565 ˇbarp()
566 bazp()
567 }
568 "})
569 .await;
570 cx.simulate_shared_keystrokes("shift-v j z f").await;
571
572 // visual display is now:
573 // fn boop () {
574 // [FOLDED]
575 // }
576
577 // TODO: this should not be needed but currently zf does not
578 // return to normal mode.
579 cx.simulate_shared_keystrokes("escape").await;
580
581 // skip over fold downward
582 cx.simulate_shared_keystrokes("g g").await;
583 cx.shared_state().await.assert_eq(indoc! {"
584 ˇfn boop() {
585 barp()
586 bazp()
587 }
588 "});
589
590 cx.simulate_shared_keystrokes("j j").await;
591 cx.shared_state().await.assert_eq(indoc! {"
592 fn boop() {
593 barp()
594 bazp()
595 ˇ}
596 "});
597
598 // skip over fold upward
599 cx.simulate_shared_keystrokes("2 k").await;
600 cx.shared_state().await.assert_eq(indoc! {"
601 ˇfn boop() {
602 barp()
603 bazp()
604 }
605 "});
606
607 // yank the fold
608 cx.simulate_shared_keystrokes("down y y").await;
609 cx.shared_clipboard()
610 .await
611 .assert_eq(" barp()\n bazp()\n");
612
613 // re-open
614 cx.simulate_shared_keystrokes("z o").await;
615 cx.shared_state().await.assert_eq(indoc! {"
616 fn boop() {
617 ˇ barp()
618 bazp()
619 }
620 "});
621}
622
623#[gpui::test]
624async fn test_folds_panic(cx: &mut gpui::TestAppContext) {
625 let mut cx = NeovimBackedTestContext::new(cx).await;
626 cx.set_neovim_option("foldmethod=manual").await;
627
628 cx.set_shared_state(indoc! { "
629 fn boop() {
630 ˇbarp()
631 bazp()
632 }
633 "})
634 .await;
635 cx.simulate_shared_keystrokes("shift-v j z f").await;
636 cx.simulate_shared_keystrokes("escape").await;
637 cx.simulate_shared_keystrokes("g g").await;
638 cx.simulate_shared_keystrokes("5 d j").await;
639 cx.shared_state().await.assert_eq("ˇ");
640 cx.set_shared_state(indoc! {"
641 fn boop() {
642 ˇbarp()
643 bazp()
644 }
645 "})
646 .await;
647 cx.simulate_shared_keystrokes("shift-v j j z f").await;
648 cx.simulate_shared_keystrokes("escape").await;
649 cx.simulate_shared_keystrokes("shift-g shift-v").await;
650 cx.shared_state().await.assert_eq(indoc! {"
651 fn boop() {
652 barp()
653 bazp()
654 }
655 ˇ"});
656}
657
658#[gpui::test]
659async fn test_clear_counts(cx: &mut gpui::TestAppContext) {
660 let mut cx = NeovimBackedTestContext::new(cx).await;
661
662 cx.set_shared_state(indoc! {"
663 The quick brown
664 fox juˇmps over
665 the lazy dog"})
666 .await;
667
668 cx.simulate_shared_keystrokes("4 escape 3 d l").await;
669 cx.shared_state().await.assert_eq(indoc! {"
670 The quick brown
671 fox juˇ over
672 the lazy dog"});
673}
674
675#[gpui::test]
676async fn test_zero(cx: &mut gpui::TestAppContext) {
677 let mut cx = NeovimBackedTestContext::new(cx).await;
678
679 cx.set_shared_state(indoc! {"
680 The quˇick brown
681 fox jumps over
682 the lazy dog"})
683 .await;
684
685 cx.simulate_shared_keystrokes("0").await;
686 cx.shared_state().await.assert_eq(indoc! {"
687 ˇThe quick brown
688 fox jumps over
689 the lazy dog"});
690
691 cx.simulate_shared_keystrokes("1 0 l").await;
692 cx.shared_state().await.assert_eq(indoc! {"
693 The quick ˇbrown
694 fox jumps over
695 the lazy dog"});
696}
697
698#[gpui::test]
699async fn test_selection_goal(cx: &mut gpui::TestAppContext) {
700 let mut cx = NeovimBackedTestContext::new(cx).await;
701
702 cx.set_shared_state(indoc! {"
703 ;;ˇ;
704 Lorem Ipsum"})
705 .await;
706
707 cx.simulate_shared_keystrokes("a down up ; down up").await;
708 cx.shared_state().await.assert_eq(indoc! {"
709 ;;;;ˇ
710 Lorem Ipsum"});
711}
712
713#[cfg(target_os = "macos")]
714#[gpui::test]
715async fn test_wrapped_motions(cx: &mut gpui::TestAppContext) {
716 let mut cx = NeovimBackedTestContext::new(cx).await;
717
718 cx.set_shared_wrap(12).await;
719
720 cx.set_shared_state(indoc! {"
721 aaˇaa
722 😃😃"
723 })
724 .await;
725 cx.simulate_shared_keystrokes("j").await;
726 cx.shared_state().await.assert_eq(indoc! {"
727 aaaa
728 😃ˇ😃"
729 });
730
731 cx.set_shared_state(indoc! {"
732 123456789012aaˇaa
733 123456789012😃😃"
734 })
735 .await;
736 cx.simulate_shared_keystrokes("j").await;
737 cx.shared_state().await.assert_eq(indoc! {"
738 123456789012aaaa
739 123456789012😃ˇ😃"
740 });
741
742 cx.set_shared_state(indoc! {"
743 123456789012aaˇaa
744 123456789012😃😃"
745 })
746 .await;
747 cx.simulate_shared_keystrokes("j").await;
748 cx.shared_state().await.assert_eq(indoc! {"
749 123456789012aaaa
750 123456789012😃ˇ😃"
751 });
752
753 cx.set_shared_state(indoc! {"
754 123456789012aaaaˇaaaaaaaa123456789012
755 wow
756 123456789012😃😃😃😃😃😃123456789012"
757 })
758 .await;
759 cx.simulate_shared_keystrokes("j j").await;
760 cx.shared_state().await.assert_eq(indoc! {"
761 123456789012aaaaaaaaaaaa123456789012
762 wow
763 123456789012😃😃ˇ😃😃😃😃123456789012"
764 });
765}
766
767#[gpui::test]
768async fn test_paragraphs_dont_wrap(cx: &mut gpui::TestAppContext) {
769 let mut cx = NeovimBackedTestContext::new(cx).await;
770
771 cx.set_shared_state(indoc! {"
772 one
773 ˇ
774 two"})
775 .await;
776
777 cx.simulate_shared_keystrokes("} }").await;
778 cx.shared_state().await.assert_eq(indoc! {"
779 one
780
781 twˇo"});
782
783 cx.simulate_shared_keystrokes("{ { {").await;
784 cx.shared_state().await.assert_eq(indoc! {"
785 ˇone
786
787 two"});
788}
789
790#[gpui::test]
791async fn test_select_all_issue_2170(cx: &mut gpui::TestAppContext) {
792 let mut cx = VimTestContext::new(cx, true).await;
793
794 cx.set_state(
795 indoc! {"
796 defmodule Test do
797 def test(a, ˇ[_, _] = b), do: IO.puts('hi')
798 end
799 "},
800 Mode::Normal,
801 );
802 cx.simulate_keystrokes("g a");
803 cx.assert_state(
804 indoc! {"
805 defmodule Test do
806 def test(a, «[ˇ»_, _] = b), do: IO.puts('hi')
807 end
808 "},
809 Mode::Visual,
810 );
811}
812
813#[gpui::test]
814async fn test_jk(cx: &mut gpui::TestAppContext) {
815 let mut cx = NeovimBackedTestContext::new(cx).await;
816
817 cx.update(|cx| {
818 cx.bind_keys([KeyBinding::new(
819 "j k",
820 NormalBefore,
821 Some("vim_mode == insert"),
822 )])
823 });
824 cx.neovim.exec("imap jk <esc>").await;
825
826 cx.set_shared_state("ˇhello").await;
827 cx.simulate_shared_keystrokes("i j o j k").await;
828 cx.shared_state().await.assert_eq("jˇohello");
829}
830
831#[gpui::test]
832async fn test_jk_delay(cx: &mut gpui::TestAppContext) {
833 let mut cx = VimTestContext::new(cx, true).await;
834
835 cx.update(|cx| {
836 cx.bind_keys([KeyBinding::new(
837 "j k",
838 NormalBefore,
839 Some("vim_mode == insert"),
840 )])
841 });
842
843 cx.set_state("ˇhello", Mode::Normal);
844 cx.simulate_keystrokes("i j");
845 cx.executor().advance_clock(Duration::from_millis(500));
846 cx.run_until_parked();
847 cx.assert_state("ˇhello", Mode::Insert);
848 cx.executor().advance_clock(Duration::from_millis(500));
849 cx.run_until_parked();
850 cx.assert_state("jˇhello", Mode::Insert);
851 cx.simulate_keystrokes("k j k");
852 cx.assert_state("jˇkhello", Mode::Normal);
853}
854
855#[gpui::test]
856async fn test_comma_w(cx: &mut gpui::TestAppContext) {
857 let mut cx = NeovimBackedTestContext::new(cx).await;
858
859 cx.update(|cx| {
860 cx.bind_keys([KeyBinding::new(
861 ", w",
862 motion::Down {
863 display_lines: false,
864 },
865 Some("vim_mode == normal"),
866 )])
867 });
868 cx.neovim.exec("map ,w j").await;
869
870 cx.set_shared_state("ˇhello hello\nhello hello").await;
871 cx.simulate_shared_keystrokes("f o ; , w").await;
872 cx.shared_state()
873 .await
874 .assert_eq("hello hello\nhello hellˇo");
875
876 cx.set_shared_state("ˇhello hello\nhello hello").await;
877 cx.simulate_shared_keystrokes("f o ; , i").await;
878 cx.shared_state()
879 .await
880 .assert_eq("hellˇo hello\nhello hello");
881}
882
883#[gpui::test]
884async fn test_rename(cx: &mut gpui::TestAppContext) {
885 let mut cx = VimTestContext::new_typescript(cx).await;
886
887 cx.set_state("const beˇfore = 2; console.log(before)", Mode::Normal);
888 let def_range = cx.lsp_range("const «beforeˇ» = 2; console.log(before)");
889 let tgt_range = cx.lsp_range("const before = 2; console.log(«beforeˇ»)");
890 let mut prepare_request =
891 cx.handle_request::<lsp::request::PrepareRenameRequest, _, _>(move |_, _, _| async move {
892 Ok(Some(lsp::PrepareRenameResponse::Range(def_range)))
893 });
894 let mut rename_request =
895 cx.handle_request::<lsp::request::Rename, _, _>(move |url, params, _| async move {
896 Ok(Some(lsp::WorkspaceEdit {
897 changes: Some(
898 [(
899 url.clone(),
900 vec![
901 lsp::TextEdit::new(def_range, params.new_name.clone()),
902 lsp::TextEdit::new(tgt_range, params.new_name),
903 ],
904 )]
905 .into(),
906 ),
907 ..Default::default()
908 }))
909 });
910
911 cx.simulate_keystrokes("c d");
912 prepare_request.next().await.unwrap();
913 cx.simulate_input("after");
914 cx.simulate_keystrokes("enter");
915 rename_request.next().await.unwrap();
916 cx.assert_state("const afterˇ = 2; console.log(after)", Mode::Normal)
917}
918
919#[gpui::test]
920async fn test_remap(cx: &mut gpui::TestAppContext) {
921 let mut cx = VimTestContext::new(cx, true).await;
922
923 // test moving the cursor
924 cx.update(|cx| {
925 cx.bind_keys([KeyBinding::new(
926 "g z",
927 workspace::SendKeystrokes("l l l l".to_string()),
928 None,
929 )])
930 });
931 cx.set_state("ˇ123456789", Mode::Normal);
932 cx.simulate_keystrokes("g z");
933 cx.assert_state("1234ˇ56789", Mode::Normal);
934
935 // test switching modes
936 cx.update(|cx| {
937 cx.bind_keys([KeyBinding::new(
938 "g y",
939 workspace::SendKeystrokes("i f o o escape l".to_string()),
940 None,
941 )])
942 });
943 cx.set_state("ˇ123456789", Mode::Normal);
944 cx.simulate_keystrokes("g y");
945 cx.assert_state("fooˇ123456789", Mode::Normal);
946
947 // test recursion
948 cx.update(|cx| {
949 cx.bind_keys([KeyBinding::new(
950 "g x",
951 workspace::SendKeystrokes("g z g y".to_string()),
952 None,
953 )])
954 });
955 cx.set_state("ˇ123456789", Mode::Normal);
956 cx.simulate_keystrokes("g x");
957 cx.assert_state("1234fooˇ56789", Mode::Normal);
958
959 // test command
960 cx.update(|cx| {
961 cx.bind_keys([KeyBinding::new(
962 "g w",
963 workspace::SendKeystrokes(": j enter".to_string()),
964 None,
965 )])
966 });
967 cx.set_state("ˇ1234\n56789", Mode::Normal);
968 cx.simulate_keystrokes("g w");
969 cx.assert_state("1234ˇ 56789", Mode::Normal);
970
971 // test leaving command
972 cx.update(|cx| {
973 cx.bind_keys([KeyBinding::new(
974 "g u",
975 workspace::SendKeystrokes("g w g z".to_string()),
976 None,
977 )])
978 });
979 cx.set_state("ˇ1234\n56789", Mode::Normal);
980 cx.simulate_keystrokes("g u");
981 cx.assert_state("1234 567ˇ89", Mode::Normal);
982
983 // test leaving command
984 cx.update(|cx| {
985 cx.bind_keys([KeyBinding::new(
986 "g t",
987 workspace::SendKeystrokes("i space escape".to_string()),
988 None,
989 )])
990 });
991 cx.set_state("12ˇ34", Mode::Normal);
992 cx.simulate_keystrokes("g t");
993 cx.assert_state("12ˇ 34", Mode::Normal);
994}
995
996#[gpui::test]
997async fn test_undo(cx: &mut gpui::TestAppContext) {
998 let mut cx = NeovimBackedTestContext::new(cx).await;
999
1000 cx.set_shared_state("hello quˇoel world").await;
1001 cx.simulate_shared_keystrokes("v i w s c o escape u").await;
1002 cx.shared_state().await.assert_eq("hello ˇquoel world");
1003 cx.simulate_shared_keystrokes("ctrl-r").await;
1004 cx.shared_state().await.assert_eq("hello ˇco world");
1005 cx.simulate_shared_keystrokes("a o right l escape").await;
1006 cx.shared_state().await.assert_eq("hello cooˇl world");
1007 cx.simulate_shared_keystrokes("u").await;
1008 cx.shared_state().await.assert_eq("hello cooˇ world");
1009 cx.simulate_shared_keystrokes("u").await;
1010 cx.shared_state().await.assert_eq("hello cˇo world");
1011 cx.simulate_shared_keystrokes("u").await;
1012 cx.shared_state().await.assert_eq("hello ˇquoel world");
1013
1014 cx.set_shared_state("hello quˇoel world").await;
1015 cx.simulate_shared_keystrokes("v i w ~ u").await;
1016 cx.shared_state().await.assert_eq("hello ˇquoel world");
1017
1018 cx.set_shared_state("\nhello quˇoel world\n").await;
1019 cx.simulate_shared_keystrokes("shift-v s c escape u").await;
1020 cx.shared_state().await.assert_eq("\nˇhello quoel world\n");
1021
1022 cx.set_shared_state(indoc! {"
1023 ˇ1
1024 2
1025 3"})
1026 .await;
1027
1028 cx.simulate_shared_keystrokes("ctrl-v shift-g ctrl-a").await;
1029 cx.shared_state().await.assert_eq(indoc! {"
1030 ˇ2
1031 3
1032 4"});
1033
1034 cx.simulate_shared_keystrokes("u").await;
1035 cx.shared_state().await.assert_eq(indoc! {"
1036 ˇ1
1037 2
1038 3"});
1039}
1040
1041#[gpui::test]
1042async fn test_mouse_selection(cx: &mut TestAppContext) {
1043 let mut cx = VimTestContext::new(cx, true).await;
1044
1045 cx.set_state("ˇone two three", Mode::Normal);
1046
1047 let start_point = cx.pixel_position("one twˇo three");
1048 let end_point = cx.pixel_position("one ˇtwo three");
1049
1050 cx.simulate_mouse_down(start_point, MouseButton::Left, Modifiers::none());
1051 cx.simulate_mouse_move(end_point, MouseButton::Left, Modifiers::none());
1052 cx.simulate_mouse_up(end_point, MouseButton::Left, Modifiers::none());
1053
1054 cx.assert_state("one «ˇtwo» three", Mode::Visual)
1055}
1056
1057#[gpui::test]
1058async fn test_lowercase_marks(cx: &mut TestAppContext) {
1059 let mut cx = NeovimBackedTestContext::new(cx).await;
1060
1061 cx.set_shared_state("line one\nline ˇtwo\nline three").await;
1062 cx.simulate_shared_keystrokes("m a l ' a").await;
1063 cx.shared_state()
1064 .await
1065 .assert_eq("line one\nˇline two\nline three");
1066 cx.simulate_shared_keystrokes("` a").await;
1067 cx.shared_state()
1068 .await
1069 .assert_eq("line one\nline ˇtwo\nline three");
1070
1071 cx.simulate_shared_keystrokes("^ d ` a").await;
1072 cx.shared_state()
1073 .await
1074 .assert_eq("line one\nˇtwo\nline three");
1075}
1076
1077#[gpui::test]
1078async fn test_lt_gt_marks(cx: &mut TestAppContext) {
1079 let mut cx = NeovimBackedTestContext::new(cx).await;
1080
1081 cx.set_shared_state(indoc!(
1082 "
1083 Line one
1084 Line two
1085 Line ˇthree
1086 Line four
1087 Line five
1088 "
1089 ))
1090 .await;
1091
1092 cx.simulate_shared_keystrokes("v j escape k k").await;
1093
1094 cx.simulate_shared_keystrokes("' <").await;
1095 cx.shared_state().await.assert_eq(indoc! {"
1096 Line one
1097 Line two
1098 ˇLine three
1099 Line four
1100 Line five
1101 "});
1102
1103 cx.simulate_shared_keystrokes("` <").await;
1104 cx.shared_state().await.assert_eq(indoc! {"
1105 Line one
1106 Line two
1107 Line ˇthree
1108 Line four
1109 Line five
1110 "});
1111
1112 cx.simulate_shared_keystrokes("' >").await;
1113 cx.shared_state().await.assert_eq(indoc! {"
1114 Line one
1115 Line two
1116 Line three
1117 ˇLine four
1118 Line five
1119 "
1120 });
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 Line ˇfour
1128 Line five
1129 "
1130 });
1131
1132 cx.simulate_shared_keystrokes("v i w o escape").await;
1133 cx.simulate_shared_keystrokes("` >").await;
1134 cx.shared_state().await.assert_eq(indoc! {"
1135 Line one
1136 Line two
1137 Line three
1138 Line fouˇr
1139 Line five
1140 "
1141 });
1142 cx.simulate_shared_keystrokes("` <").await;
1143 cx.shared_state().await.assert_eq(indoc! {"
1144 Line one
1145 Line two
1146 Line three
1147 Line ˇfour
1148 Line five
1149 "
1150 });
1151}
1152
1153#[gpui::test]
1154async fn test_caret_mark(cx: &mut TestAppContext) {
1155 let mut cx = NeovimBackedTestContext::new(cx).await;
1156
1157 cx.set_shared_state(indoc!(
1158 "
1159 Line one
1160 Line two
1161 Line three
1162 ˇLine four
1163 Line five
1164 "
1165 ))
1166 .await;
1167
1168 cx.simulate_shared_keystrokes("c w shift-s t r a i g h t space t h i n g escape j j")
1169 .await;
1170
1171 cx.simulate_shared_keystrokes("' ^").await;
1172 cx.shared_state().await.assert_eq(indoc! {"
1173 Line one
1174 Line two
1175 Line three
1176 ˇStraight thing four
1177 Line five
1178 "
1179 });
1180
1181 cx.simulate_shared_keystrokes("` ^").await;
1182 cx.shared_state().await.assert_eq(indoc! {"
1183 Line one
1184 Line two
1185 Line three
1186 Straight thingˇ four
1187 Line five
1188 "
1189 });
1190
1191 cx.simulate_shared_keystrokes("k a ! escape k g i ?").await;
1192 cx.shared_state().await.assert_eq(indoc! {"
1193 Line one
1194 Line two
1195 Line three!?ˇ
1196 Straight thing four
1197 Line five
1198 "
1199 });
1200}
1201
1202#[cfg(target_os = "macos")]
1203#[gpui::test]
1204async fn test_dw_eol(cx: &mut gpui::TestAppContext) {
1205 let mut cx = NeovimBackedTestContext::new(cx).await;
1206
1207 cx.set_shared_wrap(12).await;
1208 cx.set_shared_state("twelve ˇchar twelve char\ntwelve char")
1209 .await;
1210 cx.simulate_shared_keystrokes("d w").await;
1211 cx.shared_state()
1212 .await
1213 .assert_eq("twelve ˇtwelve char\ntwelve char");
1214}
1215
1216#[gpui::test]
1217async fn test_toggle_comments(cx: &mut gpui::TestAppContext) {
1218 let mut cx = VimTestContext::new(cx, true).await;
1219
1220 let language = std::sync::Arc::new(language::Language::new(
1221 language::LanguageConfig {
1222 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
1223 ..Default::default()
1224 },
1225 Some(language::tree_sitter_rust::language()),
1226 ));
1227 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
1228
1229 // works in normal model
1230 cx.set_state(
1231 indoc! {"
1232 ˇone
1233 two
1234 three
1235 "},
1236 Mode::Normal,
1237 );
1238 cx.simulate_keystrokes("g c c");
1239 cx.assert_state(
1240 indoc! {"
1241 // ˇone
1242 two
1243 three
1244 "},
1245 Mode::Normal,
1246 );
1247
1248 // works in visual mode
1249 cx.simulate_keystrokes("v j g c");
1250 cx.assert_state(
1251 indoc! {"
1252 // // ˇone
1253 // two
1254 three
1255 "},
1256 Mode::Normal,
1257 );
1258
1259 // works in visual line mode
1260 cx.simulate_keystrokes("shift-v j g c");
1261 cx.assert_state(
1262 indoc! {"
1263 // ˇone
1264 two
1265 three
1266 "},
1267 Mode::Normal,
1268 );
1269
1270 // works with count
1271 cx.simulate_keystrokes("g c 2 j");
1272 cx.assert_state(
1273 indoc! {"
1274 // // ˇone
1275 // two
1276 // three
1277 "},
1278 Mode::Normal,
1279 );
1280
1281 // works with motion object
1282 cx.simulate_keystrokes("shift-g");
1283 cx.simulate_keystrokes("g c g g");
1284 cx.assert_state(
1285 indoc! {"
1286 // one
1287 two
1288 three
1289 ˇ"},
1290 Mode::Normal,
1291 );
1292}
1293
1294#[gpui::test]
1295async fn test_find_multibyte(cx: &mut gpui::TestAppContext) {
1296 let mut cx = NeovimBackedTestContext::new(cx).await;
1297
1298 cx.set_shared_state(r#"<label for="guests">ˇPočet hostů</label>"#)
1299 .await;
1300
1301 cx.simulate_shared_keystrokes("c t < o escape").await;
1302 cx.shared_state()
1303 .await
1304 .assert_eq(r#"<label for="guests">ˇo</label>"#);
1305}
1306
1307#[gpui::test]
1308async fn test_plus_minus(cx: &mut gpui::TestAppContext) {
1309 let mut cx = NeovimBackedTestContext::new(cx).await;
1310
1311 cx.set_shared_state(indoc! {
1312 "one
1313 two
1314 thrˇee
1315 "})
1316 .await;
1317
1318 cx.simulate_shared_keystrokes("-").await;
1319 cx.shared_state().await.assert_matches();
1320 cx.simulate_shared_keystrokes("-").await;
1321 cx.shared_state().await.assert_matches();
1322 cx.simulate_shared_keystrokes("+").await;
1323 cx.shared_state().await.assert_matches();
1324}
1325
1326#[gpui::test]
1327async fn test_command_alias(cx: &mut gpui::TestAppContext) {
1328 let mut cx = VimTestContext::new(cx, true).await;
1329 cx.update_global(|store: &mut SettingsStore, cx| {
1330 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
1331 let mut aliases = HashMap::default();
1332 aliases.insert("Q".to_string(), "upper".to_string());
1333 s.command_aliases = Some(aliases)
1334 });
1335 });
1336
1337 cx.set_state("ˇhello world", Mode::Normal);
1338 cx.simulate_keystrokes(": Q");
1339 cx.set_state("ˇHello world", Mode::Normal);
1340}
1341
1342#[gpui::test]
1343async fn test_remap_adjacent_dog_cat(cx: &mut gpui::TestAppContext) {
1344 let mut cx = NeovimBackedTestContext::new(cx).await;
1345 cx.update(|cx| {
1346 cx.bind_keys([
1347 KeyBinding::new(
1348 "d o g",
1349 workspace::SendKeystrokes("🐶".to_string()),
1350 Some("vim_mode == insert"),
1351 ),
1352 KeyBinding::new(
1353 "c a t",
1354 workspace::SendKeystrokes("🐱".to_string()),
1355 Some("vim_mode == insert"),
1356 ),
1357 ])
1358 });
1359 cx.neovim.exec("imap dog 🐶").await;
1360 cx.neovim.exec("imap cat 🐱").await;
1361
1362 cx.set_shared_state("ˇ").await;
1363 cx.simulate_shared_keystrokes("i d o g").await;
1364 cx.shared_state().await.assert_eq("🐶ˇ");
1365
1366 cx.set_shared_state("ˇ").await;
1367 cx.simulate_shared_keystrokes("i d o d o g").await;
1368 cx.shared_state().await.assert_eq("do🐶ˇ");
1369
1370 cx.set_shared_state("ˇ").await;
1371 cx.simulate_shared_keystrokes("i d o c a t").await;
1372 cx.shared_state().await.assert_eq("do🐱ˇ");
1373}
1374
1375#[gpui::test]
1376async fn test_remap_nested_pineapple(cx: &mut gpui::TestAppContext) {
1377 let mut cx = NeovimBackedTestContext::new(cx).await;
1378 cx.update(|cx| {
1379 cx.bind_keys([
1380 KeyBinding::new(
1381 "p i n",
1382 workspace::SendKeystrokes("📌".to_string()),
1383 Some("vim_mode == insert"),
1384 ),
1385 KeyBinding::new(
1386 "p i n e",
1387 workspace::SendKeystrokes("🌲".to_string()),
1388 Some("vim_mode == insert"),
1389 ),
1390 KeyBinding::new(
1391 "p i n e a p p l e",
1392 workspace::SendKeystrokes("🍍".to_string()),
1393 Some("vim_mode == insert"),
1394 ),
1395 ])
1396 });
1397 cx.neovim.exec("imap pin 📌").await;
1398 cx.neovim.exec("imap pine 🌲").await;
1399 cx.neovim.exec("imap pineapple 🍍").await;
1400
1401 cx.set_shared_state("ˇ").await;
1402 cx.simulate_shared_keystrokes("i p i n").await;
1403 cx.executor().advance_clock(Duration::from_millis(1000));
1404 cx.run_until_parked();
1405 cx.shared_state().await.assert_eq("📌ˇ");
1406
1407 cx.set_shared_state("ˇ").await;
1408 cx.simulate_shared_keystrokes("i p i n e").await;
1409 cx.executor().advance_clock(Duration::from_millis(1000));
1410 cx.run_until_parked();
1411 cx.shared_state().await.assert_eq("🌲ˇ");
1412
1413 cx.set_shared_state("ˇ").await;
1414 cx.simulate_shared_keystrokes("i p i n e a p p l e").await;
1415 cx.shared_state().await.assert_eq("🍍ˇ");
1416}
1417
1418#[gpui::test]
1419async fn test_escape_while_waiting(cx: &mut gpui::TestAppContext) {
1420 let mut cx = NeovimBackedTestContext::new(cx).await;
1421 cx.set_shared_state("ˇhi").await;
1422 cx.simulate_shared_keystrokes("\" + escape x").await;
1423 cx.shared_state().await.assert_eq("ˇi");
1424}
1425
1426#[gpui::test]
1427async fn test_ctrl_w_override(cx: &mut gpui::TestAppContext) {
1428 let mut cx = NeovimBackedTestContext::new(cx).await;
1429 cx.update(|cx| {
1430 cx.bind_keys([KeyBinding::new("ctrl-w", DeleteLine, None)]);
1431 });
1432 cx.neovim.exec("map <c-w> D").await;
1433 cx.set_shared_state("ˇhi").await;
1434 cx.simulate_shared_keystrokes("ctrl-w").await;
1435 cx.shared_state().await.assert_eq("ˇ");
1436}