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