vim_test_context.rs

  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}