vim_test_context.rs

  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(&params, 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}