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