1use std::ops::{Deref, DerefMut, Range};
2
3use collections::BTreeMap;
4use itertools::{Either, Itertools};
5
6use editor::{display_map::ToDisplayPoint, Autoscroll};
7use gpui::{json::json, keymap::Keystroke, ViewHandle};
8use indoc::indoc;
9use language::Selection;
10use project::Project;
11use util::{
12 set_eq,
13 test::{marked_text, marked_text_ranges_by, SetEqError},
14};
15use workspace::{pane, AppState, WorkspaceHandle};
16
17use crate::{state::Operator, *};
18
19pub struct VimTestContext<'a> {
20 cx: &'a mut gpui::TestAppContext,
21 window_id: usize,
22 editor: ViewHandle<Editor>,
23}
24
25impl<'a> VimTestContext<'a> {
26 pub async fn new(cx: &'a mut gpui::TestAppContext, enabled: bool) -> VimTestContext<'a> {
27 cx.update(|cx| {
28 editor::init(cx);
29 pane::init(cx);
30 crate::init(cx);
31
32 settings::KeymapFileContent::load("keymaps/vim.json", cx).unwrap();
33 });
34
35 let params = cx.update(AppState::test);
36 let project = Project::test(params.fs.clone(), [], cx).await;
37
38 cx.update(|cx| {
39 cx.update_global(|settings: &mut Settings, _| {
40 settings.vim_mode = enabled;
41 });
42 });
43
44 params
45 .fs
46 .as_fake()
47 .insert_tree("/root", json!({ "dir": { "test.txt": "" } }))
48 .await;
49
50 let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
51 project
52 .update(cx, |project, cx| {
53 project.find_or_create_local_worktree("/root", true, cx)
54 })
55 .await
56 .unwrap();
57 cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
58 .await;
59
60 let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
61 let item = workspace
62 .update(cx, |workspace, cx| workspace.open_path(file, true, cx))
63 .await
64 .expect("Could not open test file");
65
66 let editor = cx.update(|cx| {
67 item.act_as::<Editor>(cx)
68 .expect("Opened test file wasn't an editor")
69 });
70 editor.update(cx, |_, cx| cx.focus_self());
71
72 Self {
73 cx,
74 window_id,
75 editor,
76 }
77 }
78
79 pub fn enable_vim(&mut self) {
80 self.cx.update(|cx| {
81 cx.update_global(|settings: &mut Settings, _| {
82 settings.vim_mode = true;
83 });
84 })
85 }
86
87 pub fn disable_vim(&mut self) {
88 self.cx.update(|cx| {
89 cx.update_global(|settings: &mut Settings, _| {
90 settings.vim_mode = false;
91 });
92 })
93 }
94
95 pub fn mode(&mut self) -> Mode {
96 self.cx.read(|cx| cx.global::<Vim>().state.mode)
97 }
98
99 pub fn active_operator(&mut self) -> Option<Operator> {
100 self.cx
101 .read(|cx| cx.global::<Vim>().state.operator_stack.last().copied())
102 }
103
104 pub fn editor_text(&mut self) -> String {
105 self.editor
106 .update(self.cx, |editor, cx| editor.snapshot(cx).text())
107 }
108
109 pub fn simulate_keystroke(&mut self, keystroke_text: &str) {
110 let keystroke = Keystroke::parse(keystroke_text).unwrap();
111 let input = if keystroke.modified() {
112 None
113 } else {
114 Some(keystroke.key.clone())
115 };
116 self.cx
117 .dispatch_keystroke(self.window_id, keystroke, input, false);
118 }
119
120 pub fn simulate_keystrokes<const COUNT: usize>(&mut self, keystroke_texts: [&str; COUNT]) {
121 for keystroke_text in keystroke_texts.into_iter() {
122 self.simulate_keystroke(keystroke_text);
123 }
124 }
125
126 pub fn set_state(&mut self, text: &str, mode: Mode) {
127 self.cx
128 .update(|cx| Vim::update(cx, |vim, cx| vim.switch_mode(mode, cx)));
129 self.editor.update(self.cx, |editor, cx| {
130 let (unmarked_text, markers) = marked_text(&text);
131 editor.set_text(unmarked_text, cx);
132 let cursor_offset = markers[0];
133 editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
134 s.replace_cursors_with(|map| vec![cursor_offset.to_display_point(map)])
135 });
136 })
137 }
138
139 // Asserts the editor state via a marked string.
140 // `|` characters represent empty selections
141 // `[` to `}` represents a non empty selection with the head at `}`
142 // `{` to `]` represents a non empty selection with the head at `{`
143 pub fn assert_editor_state(&mut self, text: &str) {
144 let (text_with_ranges, expected_empty_selections) = marked_text(&text);
145 let (unmarked_text, mut selection_ranges) =
146 marked_text_ranges_by(&text_with_ranges, vec![('[', '}'), ('{', ']')]);
147 let editor_text = self.editor_text();
148 assert_eq!(
149 editor_text, unmarked_text,
150 "Unmarked text doesn't match editor text"
151 );
152
153 let expected_reverse_selections = selection_ranges.remove(&('{', ']')).unwrap_or_default();
154 let expected_forward_selections = selection_ranges.remove(&('[', '}')).unwrap_or_default();
155
156 self.assert_selections(
157 expected_empty_selections,
158 expected_reverse_selections,
159 expected_forward_selections,
160 Some(text.to_string()),
161 )
162 }
163
164 pub fn assert_editor_selections(&mut self, expected_selections: Vec<Selection<usize>>) {
165 let (expected_empty_selections, expected_non_empty_selections): (Vec<_>, Vec<_>) =
166 expected_selections.into_iter().partition_map(|selection| {
167 if selection.is_empty() {
168 Either::Left(selection.head())
169 } else {
170 Either::Right(selection)
171 }
172 });
173
174 let (expected_reverse_selections, expected_forward_selections): (Vec<_>, Vec<_>) =
175 expected_non_empty_selections
176 .into_iter()
177 .partition_map(|selection| {
178 let range = selection.start..selection.end;
179 if selection.reversed {
180 Either::Left(range)
181 } else {
182 Either::Right(range)
183 }
184 });
185
186 self.assert_selections(
187 expected_empty_selections,
188 expected_reverse_selections,
189 expected_forward_selections,
190 None,
191 )
192 }
193
194 fn assert_selections(
195 &mut self,
196 expected_empty_selections: Vec<usize>,
197 expected_reverse_selections: Vec<Range<usize>>,
198 expected_forward_selections: Vec<Range<usize>>,
199 asserted_text: Option<String>,
200 ) {
201 let (empty_selections, reverse_selections, forward_selections) =
202 self.editor.read_with(self.cx, |editor, cx| {
203 let (empty_selections, non_empty_selections): (Vec<_>, Vec<_>) = editor
204 .selections
205 .all::<usize>(cx)
206 .into_iter()
207 .partition_map(|selection| {
208 if selection.is_empty() {
209 Either::Left(selection.head())
210 } else {
211 Either::Right(selection)
212 }
213 });
214
215 let (reverse_selections, forward_selections): (Vec<_>, Vec<_>) =
216 non_empty_selections.into_iter().partition_map(|selection| {
217 let range = selection.start..selection.end;
218 if selection.reversed {
219 Either::Left(range)
220 } else {
221 Either::Right(range)
222 }
223 });
224 (empty_selections, reverse_selections, forward_selections)
225 });
226
227 let asserted_selections = asserted_text.unwrap_or_else(|| {
228 self.insert_markers(
229 &expected_empty_selections,
230 &expected_reverse_selections,
231 &expected_forward_selections,
232 )
233 });
234 let actual_selections =
235 self.insert_markers(&empty_selections, &reverse_selections, &forward_selections);
236
237 let unmarked_text = self.editor_text();
238 let all_eq: Result<(), SetEqError<String>> =
239 set_eq!(expected_empty_selections, empty_selections)
240 .map_err(|err| {
241 err.map(|missing| {
242 let mut error_text = unmarked_text.clone();
243 error_text.insert(missing, '|');
244 error_text
245 })
246 })
247 .and_then(|_| {
248 set_eq!(expected_reverse_selections, reverse_selections).map_err(|err| {
249 err.map(|missing| {
250 let mut error_text = unmarked_text.clone();
251 error_text.insert(missing.start, '{');
252 error_text.insert(missing.end, ']');
253 error_text
254 })
255 })
256 })
257 .and_then(|_| {
258 set_eq!(expected_forward_selections, forward_selections).map_err(|err| {
259 err.map(|missing| {
260 let mut error_text = unmarked_text.clone();
261 error_text.insert(missing.start, '[');
262 error_text.insert(missing.end, '}');
263 error_text
264 })
265 })
266 });
267
268 match all_eq {
269 Err(SetEqError::LeftMissing(location_text)) => {
270 panic!(
271 indoc! {"
272 Editor has extra selection
273 Extra Selection Location:
274 {}
275 Asserted selections:
276 {}
277 Actual selections:
278 {}"},
279 location_text, asserted_selections, actual_selections,
280 );
281 }
282 Err(SetEqError::RightMissing(location_text)) => {
283 panic!(
284 indoc! {"
285 Editor is missing empty selection
286 Missing Selection Location:
287 {}
288 Asserted selections:
289 {}
290 Actual selections:
291 {}"},
292 location_text, asserted_selections, actual_selections,
293 );
294 }
295 _ => {}
296 }
297 }
298
299 fn insert_markers(
300 &mut self,
301 empty_selections: &Vec<usize>,
302 reverse_selections: &Vec<Range<usize>>,
303 forward_selections: &Vec<Range<usize>>,
304 ) -> String {
305 let mut editor_text_with_selections = self.editor_text();
306 let mut selection_marks = BTreeMap::new();
307 for offset in empty_selections {
308 selection_marks.insert(offset, '|');
309 }
310 for range in reverse_selections {
311 selection_marks.insert(&range.start, '{');
312 selection_marks.insert(&range.end, ']');
313 }
314 for range in forward_selections {
315 selection_marks.insert(&range.start, '[');
316 selection_marks.insert(&range.end, '}');
317 }
318 for (offset, mark) in selection_marks.into_iter().rev() {
319 editor_text_with_selections.insert(*offset, mark);
320 }
321
322 editor_text_with_selections
323 }
324
325 pub fn assert_binding<const COUNT: usize>(
326 &mut self,
327 keystrokes: [&str; COUNT],
328 initial_state: &str,
329 initial_mode: Mode,
330 state_after: &str,
331 mode_after: Mode,
332 ) {
333 self.set_state(initial_state, initial_mode);
334 self.simulate_keystrokes(keystrokes);
335 self.assert_editor_state(state_after);
336 assert_eq!(self.mode(), mode_after);
337 assert_eq!(self.active_operator(), None);
338 }
339
340 pub fn binding<const COUNT: usize>(
341 mut self,
342 keystrokes: [&'static str; COUNT],
343 ) -> VimBindingTestContext<'a, COUNT> {
344 let mode = self.mode();
345 VimBindingTestContext::new(keystrokes, mode, mode, self)
346 }
347
348 pub fn assert_clipboard_content(&mut self, expected_content: Option<&str>) {
349 self.cx.update(|cx| {
350 let actual_content = cx.read_from_clipboard().map(|item| item.text().to_owned());
351 let expected_content = expected_content.map(|content| content.to_owned());
352 assert_eq!(actual_content, expected_content);
353 })
354 }
355}
356
357impl<'a> Deref for VimTestContext<'a> {
358 type Target = gpui::TestAppContext;
359
360 fn deref(&self) -> &Self::Target {
361 self.cx
362 }
363}
364
365pub struct VimBindingTestContext<'a, const COUNT: usize> {
366 cx: VimTestContext<'a>,
367 keystrokes_under_test: [&'static str; COUNT],
368 mode_before: Mode,
369 mode_after: Mode,
370}
371
372impl<'a, const COUNT: usize> VimBindingTestContext<'a, COUNT> {
373 pub fn new(
374 keystrokes_under_test: [&'static str; COUNT],
375 mode_before: Mode,
376 mode_after: Mode,
377 cx: VimTestContext<'a>,
378 ) -> Self {
379 Self {
380 cx,
381 keystrokes_under_test,
382 mode_before,
383 mode_after,
384 }
385 }
386
387 pub fn binding<const NEW_COUNT: usize>(
388 self,
389 keystrokes_under_test: [&'static str; NEW_COUNT],
390 ) -> VimBindingTestContext<'a, NEW_COUNT> {
391 VimBindingTestContext {
392 keystrokes_under_test,
393 cx: self.cx,
394 mode_before: self.mode_before,
395 mode_after: self.mode_after,
396 }
397 }
398
399 pub fn mode_after(mut self, mode_after: Mode) -> Self {
400 self.mode_after = mode_after;
401 self
402 }
403
404 pub fn assert(&mut self, initial_state: &str, state_after: &str) {
405 self.cx.assert_binding(
406 self.keystrokes_under_test,
407 initial_state,
408 self.mode_before,
409 state_after,
410 self.mode_after,
411 )
412 }
413}
414
415impl<'a, const COUNT: usize> Deref for VimBindingTestContext<'a, COUNT> {
416 type Target = VimTestContext<'a>;
417
418 fn deref(&self) -> &Self::Target {
419 &self.cx
420 }
421}
422
423impl<'a, const COUNT: usize> DerefMut for VimBindingTestContext<'a, COUNT> {
424 fn deref_mut(&mut self) -> &mut Self::Target {
425 &mut self.cx
426 }
427}