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, true, "").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, true, "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, true, "").await;
 85
 86    cx.simulate_keystroke("i");
 87    assert_eq!(cx.mode(), Mode::Insert);
 88
 89    // Editor acts as though vim is disabled
 90    cx.disable_vim();
 91    cx.simulate_keystrokes(&["h", "j", "k", "l"]);
 92    cx.assert_newest_selection_head("hjkl|");
 93
 94    // Enabling dynamically sets vim mode again and restores normal mode
 95    cx.enable_vim();
 96    assert_eq!(cx.mode(), Mode::Normal);
 97    cx.simulate_keystrokes(&["h", "h", "h", "l"]);
 98    assert_eq!(cx.editor_text(), "hjkl".to_owned());
 99    cx.assert_newest_selection_head("hj|kl");
100    cx.simulate_keystrokes(&["i", "T", "e", "s", "t"]);
101    cx.assert_newest_selection_head("hjTest|kl");
102
103    // Disabling and enabling resets to normal mode
104    assert_eq!(cx.mode(), Mode::Insert);
105    cx.disable_vim();
106    assert_eq!(cx.mode(), Mode::Insert);
107    cx.enable_vim();
108    assert_eq!(cx.mode(), Mode::Normal);
109}
110
111#[gpui::test]
112async fn test_initially_disabled(cx: &mut gpui::TestAppContext) {
113    let mut cx = VimTestAppContext::new(cx, false, "").await;
114    cx.simulate_keystrokes(&["h", "j", "k", "l"]);
115    cx.assert_newest_selection_head("hjkl|");
116}
117
118struct VimTestAppContext<'a> {
119    cx: &'a mut gpui::TestAppContext,
120    window_id: usize,
121    editor: ViewHandle<Editor>,
122}
123
124impl<'a> VimTestAppContext<'a> {
125    async fn new(
126        cx: &'a mut gpui::TestAppContext,
127        enabled: bool,
128        initial_editor_text: &str,
129    ) -> VimTestAppContext<'a> {
130        cx.update(|cx| {
131            editor::init(cx);
132            crate::init(cx);
133        });
134        let params = cx.update(WorkspaceParams::test);
135
136        cx.update(|cx| {
137            cx.update_global(|settings: &mut Settings, _| {
138                settings.vim_mode = enabled;
139            });
140        });
141
142        params
143            .fs
144            .as_fake()
145            .insert_tree(
146                "/root",
147                json!({ "dir": { "test.txt": initial_editor_text } }),
148            )
149            .await;
150
151        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
152        params
153            .project
154            .update(cx, |project, cx| {
155                project.find_or_create_local_worktree("/root", true, cx)
156            })
157            .await
158            .unwrap();
159        cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
160            .await;
161
162        let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
163        let item = workspace
164            .update(cx, |workspace, cx| workspace.open_path(file, cx))
165            .await
166            .expect("Could not open test file");
167
168        let editor = cx.update(|cx| {
169            item.act_as::<Editor>(cx)
170                .expect("Opened test file wasn't an editor")
171        });
172        editor.update(cx, |_, cx| cx.focus_self());
173
174        Self {
175            cx,
176            window_id,
177            editor,
178        }
179    }
180
181    fn enable_vim(&mut self) {
182        self.cx.update(|cx| {
183            cx.update_global(|settings: &mut Settings, _| {
184                settings.vim_mode = true;
185            });
186        })
187    }
188
189    fn disable_vim(&mut self) {
190        self.cx.update(|cx| {
191            cx.update_global(|settings: &mut Settings, _| {
192                settings.vim_mode = false;
193            });
194        })
195    }
196
197    fn newest_selection(&mut self) -> Selection<DisplayPoint> {
198        self.editor.update(self.cx, |editor, cx| {
199            let snapshot = editor.snapshot(cx);
200            editor
201                .newest_selection::<Point>(cx)
202                .map(|point| point.to_display_point(&snapshot.display_snapshot))
203        })
204    }
205
206    fn mode(&mut self) -> Mode {
207        self.cx.update(|cx| cx.global::<VimState>().mode)
208    }
209
210    fn editor_text(&mut self) -> String {
211        self.editor
212            .update(self.cx, |editor, cx| editor.snapshot(cx).text())
213    }
214
215    fn simulate_keystroke(&mut self, keystroke_text: &str) {
216        let keystroke = Keystroke::parse(keystroke_text).unwrap();
217        let input = if keystroke.modified() {
218            None
219        } else {
220            Some(keystroke.key.clone())
221        };
222        self.cx
223            .dispatch_keystroke(self.window_id, keystroke, input, false);
224    }
225
226    fn simulate_keystrokes(&mut self, keystroke_texts: &[&str]) {
227        for keystroke_text in keystroke_texts.into_iter() {
228            self.simulate_keystroke(keystroke_text);
229        }
230    }
231
232    fn assert_newest_selection_head(&mut self, text: &str) {
233        let (unmarked_text, markers) = marked_text(&text);
234        assert_eq!(
235            self.editor_text(),
236            unmarked_text,
237            "Unmarked text doesn't match editor text"
238        );
239        let newest_selection = self.newest_selection();
240        let expected_head = self.editor.update(self.cx, |editor, cx| {
241            markers[0].to_display_point(&editor.snapshot(cx))
242        });
243        assert_eq!(newest_selection.head(), expected_head)
244    }
245}
246
247impl<'a> Deref for VimTestAppContext<'a> {
248    type Target = gpui::TestAppContext;
249
250    fn deref(&self) -> &Self::Target {
251        self.cx
252    }
253}