1use indoc::indoc;
2use std::ops::Deref;
3
4use editor::{display_map::ToDisplayPoint, DisplayPoint};
5use gpui::{json::json, keymap::Keystroke, ViewHandle};
6use language::{Point, Selection};
7use util::test::marked_text;
8use workspace::{WorkspaceHandle, WorkspaceParams};
9
10use crate::*;
11
12#[gpui::test]
13async fn test_insert_mode(cx: &mut gpui::TestAppContext) {
14 let mut cx = VimTestAppContext::new(cx, true, "").await;
15 cx.simulate_keystroke("i");
16 assert_eq!(cx.mode(), Mode::Insert);
17 cx.simulate_keystrokes(&["T", "e", "s", "t"]);
18 cx.assert_newest_selection_head("Test|");
19 cx.simulate_keystroke("escape");
20 assert_eq!(cx.mode(), Mode::Normal);
21 cx.assert_newest_selection_head("Tes|t");
22}
23
24#[gpui::test]
25async fn test_normal_hjkl(cx: &mut gpui::TestAppContext) {
26 let mut cx = VimTestAppContext::new(cx, true, "Test\nTestTest\nTest").await;
27 cx.simulate_keystroke("l");
28 cx.assert_newest_selection_head(indoc! {"
29 T|est
30 TestTest
31 Test"});
32 cx.simulate_keystroke("h");
33 cx.assert_newest_selection_head(indoc! {"
34 |Test
35 TestTest
36 Test"});
37 cx.simulate_keystroke("j");
38 cx.assert_newest_selection_head(indoc! {"
39 Test
40 |TestTest
41 Test"});
42 cx.simulate_keystroke("k");
43 cx.assert_newest_selection_head(indoc! {"
44 |Test
45 TestTest
46 Test"});
47 cx.simulate_keystroke("j");
48 cx.assert_newest_selection_head(indoc! {"
49 Test
50 |TestTest
51 Test"});
52
53 // When moving left, cursor does not wrap to the previous line
54 cx.simulate_keystroke("h");
55 cx.assert_newest_selection_head(indoc! {"
56 Test
57 |TestTest
58 Test"});
59
60 // When moving right, cursor does not reach the line end or wrap to the next line
61 for _ in 0..9 {
62 cx.simulate_keystroke("l");
63 }
64 cx.assert_newest_selection_head(indoc! {"
65 Test
66 TestTes|t
67 Test"});
68
69 // Goal column respects the inability to reach the end of the line
70 cx.simulate_keystroke("k");
71 cx.assert_newest_selection_head(indoc! {"
72 Tes|t
73 TestTest
74 Test"});
75 cx.simulate_keystroke("j");
76 cx.assert_newest_selection_head(indoc! {"
77 Test
78 TestTes|t
79 Test"});
80}
81
82#[gpui::test]
83async fn test_toggle_through_settings(cx: &mut gpui::TestAppContext) {
84 let mut cx = VimTestAppContext::new(cx, true, "").await;
85
86 cx.simulate_keystroke("i");
87 assert_eq!(cx.mode(), Mode::Insert);
88
89 // Editor acts as though vim is disabled
90 cx.disable_vim();
91 cx.simulate_keystrokes(&["h", "j", "k", "l"]);
92 cx.assert_newest_selection_head("hjkl|");
93
94 // Enabling dynamically sets vim mode again and restores normal mode
95 cx.enable_vim();
96 assert_eq!(cx.mode(), Mode::Normal);
97 cx.simulate_keystrokes(&["h", "h", "h", "l"]);
98 assert_eq!(cx.editor_text(), "hjkl".to_owned());
99 cx.assert_newest_selection_head("hj|kl");
100 cx.simulate_keystrokes(&["i", "T", "e", "s", "t"]);
101 cx.assert_newest_selection_head("hjTest|kl");
102
103 // Disabling and enabling resets to normal mode
104 assert_eq!(cx.mode(), Mode::Insert);
105 cx.disable_vim();
106 assert_eq!(cx.mode(), Mode::Insert);
107 cx.enable_vim();
108 assert_eq!(cx.mode(), Mode::Normal);
109}
110
111#[gpui::test]
112async fn test_initially_disabled(cx: &mut gpui::TestAppContext) {
113 let mut cx = VimTestAppContext::new(cx, false, "").await;
114 cx.simulate_keystrokes(&["h", "j", "k", "l"]);
115 cx.assert_newest_selection_head("hjkl|");
116}
117
118struct VimTestAppContext<'a> {
119 cx: &'a mut gpui::TestAppContext,
120 window_id: usize,
121 editor: ViewHandle<Editor>,
122}
123
124impl<'a> VimTestAppContext<'a> {
125 async fn new(
126 cx: &'a mut gpui::TestAppContext,
127 enabled: bool,
128 initial_editor_text: &str,
129 ) -> VimTestAppContext<'a> {
130 cx.update(|cx| {
131 editor::init(cx);
132 crate::init(cx);
133 });
134 let params = cx.update(WorkspaceParams::test);
135
136 cx.update(|cx| {
137 cx.update_global(|settings: &mut Settings, _| {
138 settings.vim_mode = enabled;
139 });
140 });
141
142 params
143 .fs
144 .as_fake()
145 .insert_tree(
146 "/root",
147 json!({ "dir": { "test.txt": initial_editor_text } }),
148 )
149 .await;
150
151 let (window_id, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx));
152 params
153 .project
154 .update(cx, |project, cx| {
155 project.find_or_create_local_worktree("/root", true, cx)
156 })
157 .await
158 .unwrap();
159 cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
160 .await;
161
162 let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
163 let item = workspace
164 .update(cx, |workspace, cx| workspace.open_path(file, cx))
165 .await
166 .expect("Could not open test file");
167
168 let editor = cx.update(|cx| {
169 item.act_as::<Editor>(cx)
170 .expect("Opened test file wasn't an editor")
171 });
172 editor.update(cx, |_, cx| cx.focus_self());
173
174 Self {
175 cx,
176 window_id,
177 editor,
178 }
179 }
180
181 fn enable_vim(&mut self) {
182 self.cx.update(|cx| {
183 cx.update_global(|settings: &mut Settings, _| {
184 settings.vim_mode = true;
185 });
186 })
187 }
188
189 fn disable_vim(&mut self) {
190 self.cx.update(|cx| {
191 cx.update_global(|settings: &mut Settings, _| {
192 settings.vim_mode = false;
193 });
194 })
195 }
196
197 fn newest_selection(&mut self) -> Selection<DisplayPoint> {
198 self.editor.update(self.cx, |editor, cx| {
199 let snapshot = editor.snapshot(cx);
200 editor
201 .newest_selection::<Point>(cx)
202 .map(|point| point.to_display_point(&snapshot.display_snapshot))
203 })
204 }
205
206 fn mode(&mut self) -> Mode {
207 self.cx.update(|cx| cx.global::<VimState>().mode)
208 }
209
210 fn editor_text(&mut self) -> String {
211 self.editor
212 .update(self.cx, |editor, cx| editor.snapshot(cx).text())
213 }
214
215 fn simulate_keystroke(&mut self, keystroke_text: &str) {
216 let keystroke = Keystroke::parse(keystroke_text).unwrap();
217 let input = if keystroke.modified() {
218 None
219 } else {
220 Some(keystroke.key.clone())
221 };
222 self.cx
223 .dispatch_keystroke(self.window_id, keystroke, input, false);
224 }
225
226 fn simulate_keystrokes(&mut self, keystroke_texts: &[&str]) {
227 for keystroke_text in keystroke_texts.into_iter() {
228 self.simulate_keystroke(keystroke_text);
229 }
230 }
231
232 fn assert_newest_selection_head(&mut self, text: &str) {
233 let (unmarked_text, markers) = marked_text(&text);
234 assert_eq!(
235 self.editor_text(),
236 unmarked_text,
237 "Unmarked text doesn't match editor text"
238 );
239 let newest_selection = self.newest_selection();
240 let expected_head = self.editor.update(self.cx, |editor, cx| {
241 markers[0].to_display_point(&editor.snapshot(cx))
242 });
243 assert_eq!(newest_selection.head(), expected_head)
244 }
245}
246
247impl<'a> Deref for VimTestAppContext<'a> {
248 type Target = gpui::TestAppContext;
249
250 fn deref(&self) -> &Self::Target {
251 self.cx
252 }
253}