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.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}