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_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_wrapped_lines(cx: &mut gpui::TestAppContext) {
291 let mut cx = NeovimBackedTestContext::new(cx).await;
292
293 cx.set_shared_wrap(12).await;
294 // tests line wrap as follows:
295 // 1: twelve char
296 // twelve char
297 // 2: twelve char
298 cx.set_shared_state(indoc! { "
299 tˇwelve char twelve char
300 twelve char
301 "})
302 .await;
303 cx.simulate_shared_keystrokes(["j"]).await;
304 cx.assert_shared_state(indoc! { "
305 twelve char twelve char
306 tˇwelve char
307 "})
308 .await;
309 cx.simulate_shared_keystrokes(["k"]).await;
310 cx.assert_shared_state(indoc! { "
311 tˇwelve char twelve char
312 twelve char
313 "})
314 .await;
315 cx.simulate_shared_keystrokes(["g", "j"]).await;
316 cx.assert_shared_state(indoc! { "
317 twelve char tˇwelve char
318 twelve char
319 "})
320 .await;
321 cx.simulate_shared_keystrokes(["g", "j"]).await;
322 cx.assert_shared_state(indoc! { "
323 twelve char twelve char
324 tˇwelve char
325 "})
326 .await;
327
328 cx.simulate_shared_keystrokes(["g", "k"]).await;
329 cx.assert_shared_state(indoc! { "
330 twelve char tˇwelve char
331 twelve char
332 "})
333 .await;
334
335 cx.simulate_shared_keystrokes(["g", "^"]).await;
336 cx.assert_shared_state(indoc! { "
337 twelve char ˇtwelve char
338 twelve char
339 "})
340 .await;
341
342 cx.simulate_shared_keystrokes(["^"]).await;
343 cx.assert_shared_state(indoc! { "
344 ˇtwelve char twelve char
345 twelve char
346 "})
347 .await;
348
349 cx.simulate_shared_keystrokes(["g", "$"]).await;
350 cx.assert_shared_state(indoc! { "
351 twelve charˇ twelve char
352 twelve char
353 "})
354 .await;
355 cx.simulate_shared_keystrokes(["$"]).await;
356 cx.assert_shared_state(indoc! { "
357 twelve char twelve chaˇr
358 twelve char
359 "})
360 .await;
361
362 cx.set_shared_state(indoc! { "
363 tˇwelve char twelve char
364 twelve char
365 "})
366 .await;
367 cx.simulate_shared_keystrokes(["enter"]).await;
368 cx.assert_shared_state(indoc! { "
369 twelve char twelve char
370 ˇtwelve char
371 "})
372 .await;
373
374 cx.set_shared_state(indoc! { "
375 twelve char
376 tˇwelve char twelve char
377 twelve char
378 "})
379 .await;
380 cx.simulate_shared_keystrokes(["o", "o", "escape"]).await;
381 cx.assert_shared_state(indoc! { "
382 twelve char
383 twelve char twelve char
384 ˇo
385 twelve char
386 "})
387 .await;
388
389 cx.set_shared_state(indoc! { "
390 twelve char
391 tˇwelve char twelve char
392 twelve char
393 "})
394 .await;
395 cx.simulate_shared_keystrokes(["shift-a", "a", "escape"])
396 .await;
397 cx.assert_shared_state(indoc! { "
398 twelve char
399 twelve char twelve charˇa
400 twelve char
401 "})
402 .await;
403 cx.simulate_shared_keystrokes(["shift-i", "i", "escape"])
404 .await;
405 cx.assert_shared_state(indoc! { "
406 twelve char
407 ˇitwelve char twelve chara
408 twelve char
409 "})
410 .await;
411 cx.simulate_shared_keystrokes(["shift-d"]).await;
412 cx.assert_shared_state(indoc! { "
413 twelve char
414 ˇ
415 twelve char
416 "})
417 .await;
418
419 cx.set_shared_state(indoc! { "
420 twelve char
421 twelve char tˇwelve char
422 twelve char
423 "})
424 .await;
425 cx.simulate_shared_keystrokes(["shift-o", "o", "escape"])
426 .await;
427 cx.assert_shared_state(indoc! { "
428 twelve char
429 ˇo
430 twelve char twelve char
431 twelve char
432 "})
433 .await;
434
435 // line wraps as:
436 // fourteen ch
437 // ar
438 // fourteen ch
439 // ar
440 cx.set_shared_state(indoc! { "
441 fourteen chaˇr
442 fourteen char
443 "})
444 .await;
445
446 cx.simulate_shared_keystrokes(["d", "i", "w"]).await;
447 cx.assert_shared_state(indoc! {"
448 fourteenˇ•
449 fourteen char
450 "})
451 .await;
452}
453
454#[gpui::test]
455async fn test_folds(cx: &mut gpui::TestAppContext) {
456 let mut cx = NeovimBackedTestContext::new(cx).await;
457 cx.set_neovim_option("foldmethod=manual").await;
458
459 cx.set_shared_state(indoc! { "
460 fn boop() {
461 ˇbarp()
462 bazp()
463 }
464 "})
465 .await;
466 cx.simulate_shared_keystrokes(["shift-v", "j", "z", "f"])
467 .await;
468
469 // visual display is now:
470 // fn boop () {
471 // [FOLDED]
472 // }
473
474 // TODO: this should not be needed but currently zf does not
475 // return to normal mode.
476 cx.simulate_shared_keystrokes(["escape"]).await;
477
478 // skip over fold downward
479 cx.simulate_shared_keystrokes(["g", "g"]).await;
480 cx.assert_shared_state(indoc! { "
481 ˇfn boop() {
482 barp()
483 bazp()
484 }
485 "})
486 .await;
487
488 cx.simulate_shared_keystrokes(["j", "j"]).await;
489 cx.assert_shared_state(indoc! { "
490 fn boop() {
491 barp()
492 bazp()
493 ˇ}
494 "})
495 .await;
496
497 // skip over fold upward
498 cx.simulate_shared_keystrokes(["2", "k"]).await;
499 cx.assert_shared_state(indoc! { "
500 ˇfn boop() {
501 barp()
502 bazp()
503 }
504 "})
505 .await;
506
507 // yank the fold
508 cx.simulate_shared_keystrokes(["down", "y", "y"]).await;
509 cx.assert_shared_clipboard(" barp()\n bazp()\n").await;
510
511 // re-open
512 cx.simulate_shared_keystrokes(["z", "o"]).await;
513 cx.assert_shared_state(indoc! { "
514 fn boop() {
515 ˇ barp()
516 bazp()
517 }
518 "})
519 .await;
520}