1mod neovim_backed_binding_test_context;
2mod neovim_backed_test_context;
3mod neovim_connection;
4mod vim_test_context;
5
6use std::sync::Arc;
7
8use command_palette::CommandPalette;
9use editor::DisplayPoint;
10pub use neovim_backed_binding_test_context::*;
11pub use neovim_backed_test_context::*;
12pub use vim_test_context::*;
13
14use indoc::indoc;
15use search::BufferSearchBar;
16
17use crate::{state::Mode, ModeIndicator};
18
19#[gpui::test]
20async fn test_initially_disabled(cx: &mut gpui::TestAppContext) {
21 let mut cx = VimTestContext::new(cx, false).await;
22 cx.simulate_keystrokes(["h", "j", "k", "l"]);
23 cx.assert_editor_state("hjklˇ");
24}
25
26#[gpui::test]
27async fn test_neovim(cx: &mut gpui::TestAppContext) {
28 let mut cx = NeovimBackedTestContext::new(cx).await;
29
30 cx.simulate_shared_keystroke("i").await;
31 cx.assert_state_matches().await;
32 cx.simulate_shared_keystrokes([
33 "shift-T", "e", "s", "t", " ", "t", "e", "s", "t", "escape", "0", "d", "w",
34 ])
35 .await;
36 cx.assert_state_matches().await;
37 cx.assert_editor_state("ˇtest");
38}
39
40#[gpui::test]
41async fn test_toggle_through_settings(cx: &mut gpui::TestAppContext) {
42 let mut cx = VimTestContext::new(cx, true).await;
43
44 cx.simulate_keystroke("i");
45 assert_eq!(cx.mode(), Mode::Insert);
46
47 // Editor acts as though vim is disabled
48 cx.disable_vim();
49 cx.simulate_keystrokes(["h", "j", "k", "l"]);
50 cx.assert_editor_state("hjklˇ");
51
52 // Selections aren't changed if editor is blurred but vim-mode is still disabled.
53 cx.set_state("«hjklˇ»", Mode::Normal);
54 cx.assert_editor_state("«hjklˇ»");
55 cx.update_editor(|_, cx| cx.blur());
56 cx.assert_editor_state("«hjklˇ»");
57 cx.update_editor(|_, cx| cx.focus_self());
58 cx.assert_editor_state("«hjklˇ»");
59
60 // Enabling dynamically sets vim mode again and restores normal mode
61 cx.enable_vim();
62 assert_eq!(cx.mode(), Mode::Normal);
63 cx.simulate_keystrokes(["h", "h", "h", "l"]);
64 assert_eq!(cx.buffer_text(), "hjkl".to_owned());
65 cx.assert_editor_state("hˇjkl");
66 cx.simulate_keystrokes(["i", "T", "e", "s", "t"]);
67 cx.assert_editor_state("hTestˇjkl");
68
69 // Disabling and enabling resets to normal mode
70 assert_eq!(cx.mode(), Mode::Insert);
71 cx.disable_vim();
72 cx.enable_vim();
73 assert_eq!(cx.mode(), Mode::Normal);
74}
75
76#[gpui::test]
77async fn test_buffer_search(cx: &mut gpui::TestAppContext) {
78 let mut cx = VimTestContext::new(cx, true).await;
79
80 cx.set_state(
81 indoc! {"
82 The quick brown
83 fox juˇmps over
84 the lazy dog"},
85 Mode::Normal,
86 );
87 cx.simulate_keystroke("/");
88
89 let search_bar = cx.workspace(|workspace, cx| {
90 workspace
91 .active_pane()
92 .read(cx)
93 .toolbar()
94 .read(cx)
95 .item_of_type::<BufferSearchBar>()
96 .expect("Buffer search bar should be deployed")
97 });
98
99 search_bar.read_with(cx.cx, |bar, cx| {
100 assert_eq!(bar.query(cx), "");
101 })
102}
103
104#[gpui::test]
105async fn test_count_down(cx: &mut gpui::TestAppContext) {
106 let mut cx = VimTestContext::new(cx, true).await;
107
108 cx.set_state(indoc! {"aˇa\nbb\ncc\ndd\nee"}, Mode::Normal);
109 cx.simulate_keystrokes(["2", "down"]);
110 cx.assert_editor_state("aa\nbb\ncˇc\ndd\nee");
111 cx.simulate_keystrokes(["9", "down"]);
112 cx.assert_editor_state("aa\nbb\ncc\ndd\neˇe");
113}
114
115#[gpui::test]
116async fn test_end_of_document_710(cx: &mut gpui::TestAppContext) {
117 let mut cx = VimTestContext::new(cx, true).await;
118
119 // goes to end by default
120 cx.set_state(indoc! {"aˇa\nbb\ncc"}, Mode::Normal);
121 cx.simulate_keystrokes(["shift-g"]);
122 cx.assert_editor_state("aa\nbb\ncˇc");
123
124 // can go to line 1 (https://github.com/zed-industries/community/issues/710)
125 cx.simulate_keystrokes(["1", "shift-g"]);
126 cx.assert_editor_state("aˇa\nbb\ncc");
127}
128
129#[gpui::test]
130async fn test_indent_outdent(cx: &mut gpui::TestAppContext) {
131 let mut cx = VimTestContext::new(cx, true).await;
132
133 // works in normal mode
134 cx.set_state(indoc! {"aa\nbˇb\ncc"}, Mode::Normal);
135 cx.simulate_keystrokes([">", ">"]);
136 cx.assert_editor_state("aa\n bˇb\ncc");
137 cx.simulate_keystrokes(["<", "<"]);
138 cx.assert_editor_state("aa\nbˇb\ncc");
139
140 // works in visuial mode
141 cx.simulate_keystrokes(["shift-v", "down", ">"]);
142 cx.assert_editor_state("aa\n b«b\n ccˇ»");
143}
144
145#[gpui::test]
146async fn test_escape_command_palette(cx: &mut gpui::TestAppContext) {
147 let mut cx = VimTestContext::new(cx, true).await;
148
149 cx.set_state("aˇbc\n", Mode::Normal);
150 cx.simulate_keystrokes(["i", "cmd-shift-p"]);
151
152 assert!(cx.workspace(|workspace, _| workspace.modal::<CommandPalette>().is_some()));
153 cx.simulate_keystroke("escape");
154 assert!(!cx.workspace(|workspace, _| workspace.modal::<CommandPalette>().is_some()));
155 cx.assert_state("aˇbc\n", Mode::Insert);
156}
157
158#[gpui::test]
159async fn test_escape_cancels(cx: &mut gpui::TestAppContext) {
160 let mut cx = VimTestContext::new(cx, true).await;
161
162 cx.set_state("aˇbˇc", Mode::Normal);
163 cx.simulate_keystrokes(["escape"]);
164
165 cx.assert_state("aˇbc", Mode::Normal);
166}
167
168#[gpui::test]
169async fn test_selection_on_search(cx: &mut gpui::TestAppContext) {
170 let mut cx = VimTestContext::new(cx, true).await;
171
172 cx.set_state(indoc! {"aa\nbˇb\ncc\ncc\ncc\n"}, Mode::Normal);
173 cx.simulate_keystrokes(["/", "c", "c"]);
174
175 let search_bar = cx.workspace(|workspace, cx| {
176 workspace
177 .active_pane()
178 .read(cx)
179 .toolbar()
180 .read(cx)
181 .item_of_type::<BufferSearchBar>()
182 .expect("Buffer search bar should be deployed")
183 });
184
185 search_bar.read_with(cx.cx, |bar, cx| {
186 assert_eq!(bar.query(cx), "cc");
187 });
188
189 // wait for the query editor change event to fire.
190 search_bar.next_notification(&cx).await;
191
192 cx.update_editor(|editor, cx| {
193 let highlights = editor.all_text_background_highlights(cx);
194 assert_eq!(3, highlights.len());
195 assert_eq!(
196 DisplayPoint::new(2, 0)..DisplayPoint::new(2, 2),
197 highlights[0].0
198 )
199 });
200 cx.simulate_keystrokes(["enter"]);
201
202 cx.assert_state(indoc! {"aa\nbb\nˇcc\ncc\ncc\n"}, Mode::Normal);
203 cx.simulate_keystrokes(["n"]);
204 cx.assert_state(indoc! {"aa\nbb\ncc\nˇcc\ncc\n"}, Mode::Normal);
205 cx.simulate_keystrokes(["shift-n"]);
206 cx.assert_state(indoc! {"aa\nbb\nˇcc\ncc\ncc\n"}, Mode::Normal);
207}
208
209#[gpui::test]
210async fn test_status_indicator(
211 cx: &mut gpui::TestAppContext,
212 deterministic: Arc<gpui::executor::Deterministic>,
213) {
214 let mut cx = VimTestContext::new(cx, true).await;
215 deterministic.run_until_parked();
216
217 let mode_indicator = cx.workspace(|workspace, cx| {
218 let status_bar = workspace.status_bar().read(cx);
219 let mode_indicator = status_bar.item_of_type::<ModeIndicator>();
220 assert!(mode_indicator.is_some());
221 mode_indicator.unwrap()
222 });
223
224 assert_eq!(
225 cx.workspace(|_, cx| mode_indicator.read(cx).mode),
226 Some(Mode::Normal)
227 );
228
229 // shows the correct mode
230 cx.simulate_keystrokes(["i"]);
231 deterministic.run_until_parked();
232 assert_eq!(
233 cx.workspace(|_, cx| mode_indicator.read(cx).mode),
234 Some(Mode::Insert)
235 );
236
237 // shows even in search
238 cx.simulate_keystrokes(["escape", "v", "/"]);
239 deterministic.run_until_parked();
240 assert_eq!(
241 cx.workspace(|_, cx| mode_indicator.read(cx).mode),
242 Some(Mode::Visual)
243 );
244
245 // hides if vim mode is disabled
246 cx.disable_vim();
247 deterministic.run_until_parked();
248 cx.workspace(|workspace, cx| {
249 let status_bar = workspace.status_bar().read(cx);
250 let mode_indicator = status_bar.item_of_type::<ModeIndicator>().unwrap();
251 assert!(mode_indicator.read(cx).mode.is_none());
252 });
253
254 cx.enable_vim();
255 deterministic.run_until_parked();
256 cx.workspace(|workspace, cx| {
257 let status_bar = workspace.status_bar().read(cx);
258 let mode_indicator = status_bar.item_of_type::<ModeIndicator>().unwrap();
259 assert!(mode_indicator.read(cx).mode.is_some());
260 });
261}
262
263#[gpui::test]
264async fn test_word_characters(cx: &mut gpui::TestAppContext) {
265 let mut cx = VimTestContext::new_typescript(cx).await;
266 cx.set_state(
267 indoc! { "
268 class A {
269 #ˇgoop = 99;
270 $ˇgoop () { return this.#gˇoop };
271 };
272 console.log(new A().$gooˇp())
273 "},
274 Mode::Normal,
275 );
276 cx.simulate_keystrokes(["v", "i", "w"]);
277 cx.assert_state(
278 indoc! {"
279 class A {
280 «#goopˇ» = 99;
281 «$goopˇ» () { return this.«#goopˇ» };
282 };
283 console.log(new A().«$goopˇ»())
284 "},
285 Mode::Visual,
286 )
287}
288
289#[gpui::test]
290async fn test_join_lines(cx: &mut gpui::TestAppContext) {
291 let mut cx = NeovimBackedTestContext::new(cx).await;
292
293 cx.set_shared_state(indoc! {"
294 ˇone
295 two
296 three
297 four
298 five
299 six
300 "})
301 .await;
302 cx.simulate_shared_keystrokes(["shift-j"]).await;
303 cx.assert_shared_state(indoc! {"
304 oneˇ two
305 three
306 four
307 five
308 six
309 "})
310 .await;
311 cx.simulate_shared_keystrokes(["3", "shift-j"]).await;
312 cx.assert_shared_state(indoc! {"
313 one two threeˇ four
314 five
315 six
316 "})
317 .await;
318
319 cx.set_shared_state(indoc! {"
320 ˇone
321 two
322 three
323 four
324 five
325 six
326 "})
327 .await;
328 cx.simulate_shared_keystrokes(["j", "v", "3", "j", "shift-j"])
329 .await;
330 cx.assert_shared_state(indoc! {"
331 one
332 two three fourˇ five
333 six
334 "})
335 .await;
336}
337
338#[gpui::test]
339async fn test_wrapped_lines(cx: &mut gpui::TestAppContext) {
340 let mut cx = NeovimBackedTestContext::new(cx).await;
341
342 cx.set_shared_wrap(12).await;
343 // tests line wrap as follows:
344 // 1: twelve char
345 // twelve char
346 // 2: twelve char
347 cx.set_shared_state(indoc! { "
348 tˇwelve char twelve char
349 twelve char
350 "})
351 .await;
352 cx.simulate_shared_keystrokes(["j"]).await;
353 cx.assert_shared_state(indoc! { "
354 twelve char twelve char
355 tˇwelve char
356 "})
357 .await;
358 cx.simulate_shared_keystrokes(["k"]).await;
359 cx.assert_shared_state(indoc! { "
360 tˇwelve char twelve char
361 twelve char
362 "})
363 .await;
364 cx.simulate_shared_keystrokes(["g", "j"]).await;
365 cx.assert_shared_state(indoc! { "
366 twelve char tˇwelve char
367 twelve char
368 "})
369 .await;
370 cx.simulate_shared_keystrokes(["g", "j"]).await;
371 cx.assert_shared_state(indoc! { "
372 twelve char twelve char
373 tˇwelve char
374 "})
375 .await;
376
377 cx.simulate_shared_keystrokes(["g", "k"]).await;
378 cx.assert_shared_state(indoc! { "
379 twelve char tˇwelve char
380 twelve char
381 "})
382 .await;
383
384 cx.simulate_shared_keystrokes(["g", "^"]).await;
385 cx.assert_shared_state(indoc! { "
386 twelve char ˇtwelve char
387 twelve char
388 "})
389 .await;
390
391 cx.simulate_shared_keystrokes(["^"]).await;
392 cx.assert_shared_state(indoc! { "
393 ˇtwelve char twelve char
394 twelve char
395 "})
396 .await;
397
398 cx.simulate_shared_keystrokes(["g", "$"]).await;
399 cx.assert_shared_state(indoc! { "
400 twelve charˇ twelve char
401 twelve char
402 "})
403 .await;
404 cx.simulate_shared_keystrokes(["$"]).await;
405 cx.assert_shared_state(indoc! { "
406 twelve char twelve chaˇr
407 twelve char
408 "})
409 .await;
410
411 cx.set_shared_state(indoc! { "
412 tˇwelve char twelve char
413 twelve char
414 "})
415 .await;
416 cx.simulate_shared_keystrokes(["enter"]).await;
417 cx.assert_shared_state(indoc! { "
418 twelve char twelve char
419 ˇtwelve char
420 "})
421 .await;
422
423 cx.set_shared_state(indoc! { "
424 twelve char
425 tˇwelve char twelve char
426 twelve char
427 "})
428 .await;
429 cx.simulate_shared_keystrokes(["o", "o", "escape"]).await;
430 cx.assert_shared_state(indoc! { "
431 twelve char
432 twelve char twelve char
433 ˇo
434 twelve char
435 "})
436 .await;
437
438 cx.set_shared_state(indoc! { "
439 twelve char
440 tˇwelve char twelve char
441 twelve char
442 "})
443 .await;
444 cx.simulate_shared_keystrokes(["shift-a", "a", "escape"])
445 .await;
446 cx.assert_shared_state(indoc! { "
447 twelve char
448 twelve char twelve charˇa
449 twelve char
450 "})
451 .await;
452 cx.simulate_shared_keystrokes(["shift-i", "i", "escape"])
453 .await;
454 cx.assert_shared_state(indoc! { "
455 twelve char
456 ˇitwelve char twelve chara
457 twelve char
458 "})
459 .await;
460 cx.simulate_shared_keystrokes(["shift-d"]).await;
461 cx.assert_shared_state(indoc! { "
462 twelve char
463 ˇ
464 twelve char
465 "})
466 .await;
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"])
475 .await;
476 cx.assert_shared_state(indoc! { "
477 twelve char
478 ˇo
479 twelve char twelve char
480 twelve char
481 "})
482 .await;
483
484 // line wraps as:
485 // fourteen ch
486 // ar
487 // fourteen ch
488 // ar
489 cx.set_shared_state(indoc! { "
490 fourteen chaˇr
491 fourteen char
492 "})
493 .await;
494
495 cx.simulate_shared_keystrokes(["d", "i", "w"]).await;
496 cx.assert_shared_state(indoc! {"
497 fourteenˇ•
498 fourteen char
499 "})
500 .await;
501 cx.simulate_shared_keystrokes(["j", "shift-f", "e", "f", "r"])
502 .await;
503 cx.assert_shared_state(indoc! {"
504 fourteen•
505 fourteen chaˇr
506 "})
507 .await;
508}
509
510#[gpui::test]
511async fn test_folds(cx: &mut gpui::TestAppContext) {
512 let mut cx = NeovimBackedTestContext::new(cx).await;
513 cx.set_neovim_option("foldmethod=manual").await;
514
515 cx.set_shared_state(indoc! { "
516 fn boop() {
517 ˇbarp()
518 bazp()
519 }
520 "})
521 .await;
522 cx.simulate_shared_keystrokes(["shift-v", "j", "z", "f"])
523 .await;
524
525 // visual display is now:
526 // fn boop () {
527 // [FOLDED]
528 // }
529
530 // TODO: this should not be needed but currently zf does not
531 // return to normal mode.
532 cx.simulate_shared_keystrokes(["escape"]).await;
533
534 // skip over fold downward
535 cx.simulate_shared_keystrokes(["g", "g"]).await;
536 cx.assert_shared_state(indoc! { "
537 ˇfn boop() {
538 barp()
539 bazp()
540 }
541 "})
542 .await;
543
544 cx.simulate_shared_keystrokes(["j", "j"]).await;
545 cx.assert_shared_state(indoc! { "
546 fn boop() {
547 barp()
548 bazp()
549 ˇ}
550 "})
551 .await;
552
553 // skip over fold upward
554 cx.simulate_shared_keystrokes(["2", "k"]).await;
555 cx.assert_shared_state(indoc! { "
556 ˇfn boop() {
557 barp()
558 bazp()
559 }
560 "})
561 .await;
562
563 // yank the fold
564 cx.simulate_shared_keystrokes(["down", "y", "y"]).await;
565 cx.assert_shared_clipboard(" barp()\n bazp()\n").await;
566
567 // re-open
568 cx.simulate_shared_keystrokes(["z", "o"]).await;
569 cx.assert_shared_state(indoc! { "
570 fn boop() {
571 ˇ barp()
572 bazp()
573 }
574 "})
575 .await;
576}
577
578#[gpui::test]
579async fn test_clear_counts(cx: &mut gpui::TestAppContext) {
580 let mut cx = NeovimBackedTestContext::new(cx).await;
581
582 cx.set_shared_state(indoc! {"
583 The quick brown
584 fox juˇmps over
585 the lazy dog"})
586 .await;
587
588 cx.simulate_shared_keystrokes(["4", "escape", "3", "d", "l"])
589 .await;
590 cx.assert_shared_state(indoc! {"
591 The quick brown
592 fox juˇ over
593 the lazy dog"})
594 .await;
595}
596
597#[gpui::test]
598async fn test_zero(cx: &mut gpui::TestAppContext) {
599 let mut cx = NeovimBackedTestContext::new(cx).await;
600
601 cx.set_shared_state(indoc! {"
602 The quˇick brown
603 fox jumps over
604 the lazy dog"})
605 .await;
606
607 cx.simulate_shared_keystrokes(["0"]).await;
608 cx.assert_shared_state(indoc! {"
609 ˇThe quick brown
610 fox jumps over
611 the lazy dog"})
612 .await;
613
614 cx.simulate_shared_keystrokes(["1", "0", "l"]).await;
615 cx.assert_shared_state(indoc! {"
616 The quick ˇbrown
617 fox jumps over
618 the lazy dog"})
619 .await;
620}