vim_tests.rs

  1use indoc::indoc;
  2use std::ops::Deref;
  3
  4use editor::{display_map::ToDisplayPoint, DisplayPoint};
  5use gpui::{json::json, keymap::Keystroke, ViewHandle};
  6use language::{Point, Selection};
  7use util::test::marked_text;
  8use workspace::{WorkspaceHandle, WorkspaceParams};
  9
 10use crate::*;
 11
 12#[gpui::test]
 13async fn test_insert_mode(cx: &mut gpui::TestAppContext) {
 14    let mut cx = VimTestAppContext::new(cx, "").await;
 15    cx.simulate_keystroke("i");
 16    assert_eq!(cx.mode(), Mode::Insert);
 17    cx.simulate_keystrokes(&["T", "e", "s", "t"]);
 18    cx.assert_newest_selection_head("Test|");
 19    cx.simulate_keystroke("escape");
 20    assert_eq!(cx.mode(), Mode::Normal);
 21    cx.assert_newest_selection_head("Tes|t");
 22}
 23
 24#[gpui::test]
 25async fn test_normal_hjkl(cx: &mut gpui::TestAppContext) {
 26    let mut cx = VimTestAppContext::new(cx, "Test\nTestTest\nTest").await;
 27    cx.simulate_keystroke("l");
 28    cx.assert_newest_selection_head(indoc! {"
 29        T|est
 30        TestTest
 31        Test"});
 32    cx.simulate_keystroke("h");
 33    cx.assert_newest_selection_head(indoc! {"
 34        |Test
 35        TestTest
 36        Test"});
 37    cx.simulate_keystroke("j");
 38    cx.assert_newest_selection_head(indoc! {"
 39        Test
 40        |TestTest
 41        Test"});
 42    cx.simulate_keystroke("k");
 43    cx.assert_newest_selection_head(indoc! {"
 44        |Test
 45        TestTest
 46        Test"});
 47    cx.simulate_keystroke("j");
 48    cx.assert_newest_selection_head(indoc! {"
 49        Test
 50        |TestTest
 51        Test"});
 52
 53    // When moving left, cursor does not wrap to the previous line
 54    cx.simulate_keystroke("h");
 55    cx.assert_newest_selection_head(indoc! {"
 56        Test
 57        |TestTest
 58        Test"});
 59
 60    // When moving right, cursor does not reach the line end or wrap to the next line
 61    for _ in 0..9 {
 62        cx.simulate_keystroke("l");
 63    }
 64    cx.assert_newest_selection_head(indoc! {"
 65        Test
 66        TestTes|t
 67        Test"});
 68
 69    // Goal column respects the inability to reach the end of the line
 70    cx.simulate_keystroke("k");
 71    cx.assert_newest_selection_head(indoc! {"
 72        Tes|t
 73        TestTest
 74        Test"});
 75    cx.simulate_keystroke("j");
 76    cx.assert_newest_selection_head(indoc! {"
 77        Test
 78        TestTes|t
 79        Test"});
 80}
 81
 82#[gpui::test]
 83async fn test_toggle_through_settings(cx: &mut gpui::TestAppContext) {
 84    let mut cx = VimTestAppContext::new(cx, "").await;
 85
 86    // Editor acts as though vim is disabled
 87    cx.disable_vim();
 88    assert_eq!(cx.mode(), Mode::Insert);
 89    cx.simulate_keystrokes(&["h", "j", "k", "l"]);
 90    cx.assert_newest_selection_head("hjkl|");
 91
 92    // Enabling dynamically sets vim mode again
 93    cx.enable_vim();
 94    assert_eq!(cx.mode(), Mode::Normal);
 95    cx.simulate_keystrokes(&["h", "h", "h", "l"]);
 96    assert_eq!(cx.editor_text(), "hjkl".to_owned());
 97    cx.assert_newest_selection_head("hj|kl");
 98    cx.simulate_keystrokes(&["i", "T", "e", "s", "t"]);
 99    cx.assert_newest_selection_head("hjTest|kl");
100
101    // Disabling and enabling resets to normal mode
102    assert_eq!(cx.mode(), Mode::Insert);
103    cx.disable_vim();
104    assert_eq!(cx.mode(), Mode::Insert);
105    cx.enable_vim();
106    assert_eq!(cx.mode(), Mode::Normal);
107}
108
109struct VimTestAppContext<'a> {
110    cx: &'a mut gpui::TestAppContext,
111    window_id: usize,
112    editor: ViewHandle<Editor>,
113}
114
115impl<'a> VimTestAppContext<'a> {
116    async fn new(
117        cx: &'a mut gpui::TestAppContext,
118        initial_editor_text: &str,
119    ) -> VimTestAppContext<'a> {
120        cx.update(|cx| {
121            editor::init(cx);
122            crate::init(cx);
123        });
124        let params = cx.update(WorkspaceParams::test);
125
126        cx.update(|cx| {
127            cx.update_global(|settings: &mut Settings, _| {
128                settings.vim_mode = true;
129            });
130        });
131
132        params
133            .fs
134            .as_fake()
135            .insert_tree(
136                "/root",
137                json!({ "dir": { "test.txt": initial_editor_text } }),
138            )
139            .await;
140
141        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
142        params
143            .project
144            .update(cx, |project, cx| {
145                project.find_or_create_local_worktree("/root", true, cx)
146            })
147            .await
148            .unwrap();
149        cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
150            .await;
151
152        let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
153        let item = workspace
154            .update(cx, |workspace, cx| workspace.open_path(file, cx))
155            .await
156            .expect("Could not open test file");
157
158        let editor = cx.update(|cx| {
159            item.act_as::<Editor>(cx)
160                .expect("Opened test file wasn't an editor")
161        });
162        editor.update(cx, |_, cx| cx.focus_self());
163
164        Self {
165            cx,
166            window_id,
167            editor,
168        }
169    }
170
171    fn enable_vim(&mut self) {
172        self.cx.update(|cx| {
173            cx.update_global(|settings: &mut Settings, _| {
174                settings.vim_mode = true;
175            });
176        })
177    }
178
179    fn disable_vim(&mut self) {
180        self.cx.update(|cx| {
181            cx.update_global(|settings: &mut Settings, _| {
182                settings.vim_mode = false;
183            });
184        })
185    }
186
187    fn newest_selection(&mut self) -> Selection<DisplayPoint> {
188        self.editor.update(self.cx, |editor, cx| {
189            let snapshot = editor.snapshot(cx);
190            editor
191                .newest_selection::<Point>(cx)
192                .map(|point| point.to_display_point(&snapshot.display_snapshot))
193        })
194    }
195
196    fn mode(&mut self) -> Mode {
197        self.cx.update(|cx| cx.global::<VimState>().mode)
198    }
199
200    fn editor_text(&mut self) -> String {
201        self.editor
202            .update(self.cx, |editor, cx| editor.snapshot(cx).text())
203    }
204
205    fn simulate_keystroke(&mut self, keystroke_text: &str) {
206        let keystroke = Keystroke::parse(keystroke_text).unwrap();
207        let input = if keystroke.modified() {
208            None
209        } else {
210            Some(keystroke.key.clone())
211        };
212        self.cx
213            .dispatch_keystroke(self.window_id, keystroke, input, false);
214    }
215
216    fn simulate_keystrokes(&mut self, keystroke_texts: &[&str]) {
217        for keystroke_text in keystroke_texts.into_iter() {
218            self.simulate_keystroke(keystroke_text);
219        }
220    }
221
222    fn assert_newest_selection_head(&mut self, text: &str) {
223        let (unmarked_text, markers) = marked_text(&text);
224        assert_eq!(
225            self.editor_text(),
226            unmarked_text,
227            "Unmarked text doesn't match editor text"
228        );
229        let newest_selection = self.newest_selection();
230        let expected_head = self.editor.update(self.cx, |editor, cx| {
231            markers[0].to_display_point(&editor.snapshot(cx))
232        });
233        assert_eq!(newest_selection.head(), expected_head)
234    }
235}
236
237impl<'a> Deref for VimTestAppContext<'a> {
238    type Target = gpui::TestAppContext;
239
240    fn deref(&self) -> &Self::Target {
241        self.cx
242    }
243}