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.replace_selections_with(cx, |map| cursor_offset.to_display_point(map));
132 })
133 }
134
135 // Asserts the editor state via a marked string.
136 // `|` characters represent empty selections
137 // `[` to `}` represents a non empty selection with the head at `}`
138 // `{` to `]` represents a non empty selection with the head at `{`
139 pub fn assert_editor_state(&mut self, text: &str) {
140 let (text_with_ranges, expected_empty_selections) = marked_text(&text);
141 let (unmarked_text, mut selection_ranges) =
142 marked_text_ranges_by(&text_with_ranges, vec![('[', '}'), ('{', ']')]);
143 let editor_text = self.editor_text();
144 assert_eq!(
145 editor_text, unmarked_text,
146 "Unmarked text doesn't match editor text"
147 );
148
149 let expected_reverse_selections = selection_ranges.remove(&('{', ']')).unwrap_or_default();
150 let expected_forward_selections = selection_ranges.remove(&('[', '}')).unwrap_or_default();
151
152 self.assert_selections(
153 expected_empty_selections,
154 expected_reverse_selections,
155 expected_forward_selections,
156 Some(text.to_string()),
157 )
158 }
159
160 pub fn assert_editor_selections(&mut self, expected_selections: Vec<Selection<usize>>) {
161 let (expected_empty_selections, expected_non_empty_selections): (Vec<_>, Vec<_>) =
162 expected_selections.into_iter().partition_map(|selection| {
163 if selection.is_empty() {
164 Either::Left(selection.head())
165 } else {
166 Either::Right(selection)
167 }
168 });
169
170 let (expected_reverse_selections, expected_forward_selections): (Vec<_>, Vec<_>) =
171 expected_non_empty_selections
172 .into_iter()
173 .partition_map(|selection| {
174 let range = selection.start..selection.end;
175 if selection.reversed {
176 Either::Left(range)
177 } else {
178 Either::Right(range)
179 }
180 });
181
182 self.assert_selections(
183 expected_empty_selections,
184 expected_reverse_selections,
185 expected_forward_selections,
186 None,
187 )
188 }
189
190 fn assert_selections(
191 &mut self,
192 expected_empty_selections: Vec<usize>,
193 expected_reverse_selections: Vec<Range<usize>>,
194 expected_forward_selections: Vec<Range<usize>>,
195 asserted_text: Option<String>,
196 ) {
197 let (empty_selections, reverse_selections, forward_selections) =
198 self.editor.read_with(self.cx, |editor, cx| {
199 let (empty_selections, non_empty_selections): (Vec<_>, Vec<_>) = editor
200 .local_selections::<usize>(cx)
201 .into_iter()
202 .partition_map(|selection| {
203 if selection.is_empty() {
204 Either::Left(selection.head())
205 } else {
206 Either::Right(selection)
207 }
208 });
209
210 let (reverse_selections, forward_selections): (Vec<_>, Vec<_>) =
211 non_empty_selections.into_iter().partition_map(|selection| {
212 let range = selection.start..selection.end;
213 if selection.reversed {
214 Either::Left(range)
215 } else {
216 Either::Right(range)
217 }
218 });
219 (empty_selections, reverse_selections, forward_selections)
220 });
221
222 let asserted_selections = asserted_text.unwrap_or_else(|| {
223 self.insert_markers(
224 &expected_empty_selections,
225 &expected_reverse_selections,
226 &expected_forward_selections,
227 )
228 });
229 let actual_selections =
230 self.insert_markers(&empty_selections, &reverse_selections, &forward_selections);
231
232 let unmarked_text = self.editor_text();
233 let all_eq: Result<(), SetEqError<String>> =
234 set_eq!(expected_empty_selections, empty_selections)
235 .map_err(|err| {
236 err.map(|missing| {
237 let mut error_text = unmarked_text.clone();
238 error_text.insert(missing, '|');
239 error_text
240 })
241 })
242 .and_then(|_| {
243 set_eq!(expected_reverse_selections, reverse_selections).map_err(|err| {
244 err.map(|missing| {
245 let mut error_text = unmarked_text.clone();
246 error_text.insert(missing.start, '{');
247 error_text.insert(missing.end, ']');
248 error_text
249 })
250 })
251 })
252 .and_then(|_| {
253 set_eq!(expected_forward_selections, forward_selections).map_err(|err| {
254 err.map(|missing| {
255 let mut error_text = unmarked_text.clone();
256 error_text.insert(missing.start, '[');
257 error_text.insert(missing.end, '}');
258 error_text
259 })
260 })
261 });
262
263 match all_eq {
264 Err(SetEqError::LeftMissing(location_text)) => {
265 panic!(
266 indoc! {"
267 Editor has extra selection
268 Extra Selection Location: {}
269 Asserted selections: {}
270 Actual selections: {}"},
271 location_text, asserted_selections, actual_selections,
272 );
273 }
274 Err(SetEqError::RightMissing(location_text)) => {
275 panic!(
276 indoc! {"
277 Editor is missing empty selection
278 Missing Selection Location: {}
279 Asserted selections: {}
280 Actual selections: {}"},
281 location_text, asserted_selections, actual_selections,
282 );
283 }
284 _ => {}
285 }
286 }
287
288 fn insert_markers(
289 &mut self,
290 empty_selections: &Vec<usize>,
291 reverse_selections: &Vec<Range<usize>>,
292 forward_selections: &Vec<Range<usize>>,
293 ) -> String {
294 let mut editor_text_with_selections = self.editor_text();
295 let mut selection_marks = BTreeMap::new();
296 for offset in empty_selections {
297 selection_marks.insert(offset, '|');
298 }
299 for range in reverse_selections {
300 selection_marks.insert(&range.start, '{');
301 selection_marks.insert(&range.end, ']');
302 }
303 for range in forward_selections {
304 selection_marks.insert(&range.start, '[');
305 selection_marks.insert(&range.end, '}');
306 }
307 for (offset, mark) in selection_marks.into_iter().rev() {
308 editor_text_with_selections.insert(*offset, mark);
309 }
310
311 editor_text_with_selections
312 }
313
314 pub fn assert_binding<const COUNT: usize>(
315 &mut self,
316 keystrokes: [&str; COUNT],
317 initial_state: &str,
318 initial_mode: Mode,
319 state_after: &str,
320 mode_after: Mode,
321 ) {
322 self.set_state(initial_state, initial_mode);
323 self.simulate_keystrokes(keystrokes);
324 self.assert_editor_state(state_after);
325 assert_eq!(self.mode(), mode_after);
326 assert_eq!(self.active_operator(), None);
327 }
328
329 pub fn binding<const COUNT: usize>(
330 mut self,
331 keystrokes: [&'static str; COUNT],
332 ) -> VimBindingTestContext<'a, COUNT> {
333 let mode = self.mode();
334 VimBindingTestContext::new(keystrokes, mode, mode, self)
335 }
336}
337
338impl<'a> Deref for VimTestContext<'a> {
339 type Target = gpui::TestAppContext;
340
341 fn deref(&self) -> &Self::Target {
342 self.cx
343 }
344}
345
346pub struct VimBindingTestContext<'a, const COUNT: usize> {
347 cx: VimTestContext<'a>,
348 keystrokes_under_test: [&'static str; COUNT],
349 mode_before: Mode,
350 mode_after: Mode,
351}
352
353impl<'a, const COUNT: usize> VimBindingTestContext<'a, COUNT> {
354 pub fn new(
355 keystrokes_under_test: [&'static str; COUNT],
356 mode_before: Mode,
357 mode_after: Mode,
358 cx: VimTestContext<'a>,
359 ) -> Self {
360 Self {
361 cx,
362 keystrokes_under_test,
363 mode_before,
364 mode_after,
365 }
366 }
367
368 pub fn binding<const NEW_COUNT: usize>(
369 self,
370 keystrokes_under_test: [&'static str; NEW_COUNT],
371 ) -> VimBindingTestContext<'a, NEW_COUNT> {
372 VimBindingTestContext {
373 keystrokes_under_test,
374 cx: self.cx,
375 mode_before: self.mode_before,
376 mode_after: self.mode_after,
377 }
378 }
379
380 pub fn mode_after(mut self, mode_after: Mode) -> Self {
381 self.mode_after = mode_after;
382 self
383 }
384
385 pub fn assert(&mut self, initial_state: &str, state_after: &str) {
386 self.cx.assert_binding(
387 self.keystrokes_under_test,
388 initial_state,
389 self.mode_before,
390 state_after,
391 self.mode_after,
392 )
393 }
394}
395
396impl<'a, const COUNT: usize> Deref for VimBindingTestContext<'a, COUNT> {
397 type Target = VimTestContext<'a>;
398
399 fn deref(&self) -> &Self::Target {
400 &self.cx
401 }
402}