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(¶ms, 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}