1use std::ops::Deref;
2
3use editor::{display_map::ToDisplayPoint, Bias, DisplayPoint};
4use gpui::{json::json, keymap::Keystroke, ViewHandle};
5use language::{Point, Selection};
6use util::test::marked_text;
7use workspace::{WorkspaceHandle, WorkspaceParams};
8
9use crate::{state::Operator, *};
10
11pub struct VimTestContext<'a> {
12 cx: &'a mut gpui::TestAppContext,
13 window_id: usize,
14 editor: ViewHandle<Editor>,
15}
16
17impl<'a> VimTestContext<'a> {
18 pub async fn new(cx: &'a mut gpui::TestAppContext, enabled: bool) -> VimTestContext<'a> {
19 cx.update(|cx| {
20 editor::init(cx);
21 crate::init(cx);
22
23 settings::KeymapFileContent::load("keymaps/vim.json", cx).unwrap();
24 });
25
26 let params = cx.update(WorkspaceParams::test);
27
28 cx.update(|cx| {
29 cx.update_global(|settings: &mut Settings, _| {
30 settings.vim_mode = enabled;
31 });
32 });
33
34 params
35 .fs
36 .as_fake()
37 .insert_tree("/root", json!({ "dir": { "test.txt": "" } }))
38 .await;
39
40 let (window_id, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx));
41 params
42 .project
43 .update(cx, |project, cx| {
44 project.find_or_create_local_worktree("/root", true, cx)
45 })
46 .await
47 .unwrap();
48 cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
49 .await;
50
51 let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
52 let item = workspace
53 .update(cx, |workspace, cx| workspace.open_path(file, cx))
54 .await
55 .expect("Could not open test file");
56
57 let editor = cx.update(|cx| {
58 item.act_as::<Editor>(cx)
59 .expect("Opened test file wasn't an editor")
60 });
61 editor.update(cx, |_, cx| cx.focus_self());
62
63 Self {
64 cx,
65 window_id,
66 editor,
67 }
68 }
69
70 pub fn enable_vim(&mut self) {
71 self.cx.update(|cx| {
72 cx.update_global(|settings: &mut Settings, _| {
73 settings.vim_mode = true;
74 });
75 })
76 }
77
78 pub fn disable_vim(&mut self) {
79 self.cx.update(|cx| {
80 cx.update_global(|settings: &mut Settings, _| {
81 settings.vim_mode = false;
82 });
83 })
84 }
85
86 pub fn newest_selection(&mut self) -> Selection<DisplayPoint> {
87 self.editor.update(self.cx, |editor, cx| {
88 let snapshot = editor.snapshot(cx);
89 editor
90 .newest_selection::<Point>(cx)
91 .map(|point| point.to_display_point(&snapshot.display_snapshot))
92 })
93 }
94
95 pub fn mode(&mut self) -> Mode {
96 self.cx.read(|cx| cx.global::<Vim>().state.mode)
97 }
98
99 pub fn active_operator(&mut self) -> Option<Operator> {
100 self.cx
101 .read(|cx| cx.global::<Vim>().state.operator_stack.last().copied())
102 }
103
104 pub fn editor_text(&mut self) -> String {
105 self.editor
106 .update(self.cx, |editor, cx| editor.snapshot(cx).text())
107 }
108
109 pub fn simulate_keystroke(&mut self, keystroke_text: &str) {
110 let keystroke = Keystroke::parse(keystroke_text).unwrap();
111 let input = if keystroke.modified() {
112 None
113 } else {
114 Some(keystroke.key.clone())
115 };
116 self.cx
117 .dispatch_keystroke(self.window_id, keystroke, input, false);
118 }
119
120 pub fn simulate_keystrokes<const COUNT: usize>(&mut self, keystroke_texts: [&str; COUNT]) {
121 for keystroke_text in keystroke_texts.into_iter() {
122 self.simulate_keystroke(keystroke_text);
123 }
124 }
125
126 pub fn set_state(&mut self, text: &str, mode: Mode) {
127 self.cx
128 .update(|cx| Vim::update(cx, |vim, cx| vim.switch_mode(mode, cx)));
129 self.editor.update(self.cx, |editor, cx| {
130 let (unmarked_text, markers) = marked_text(&text);
131 editor.set_text(unmarked_text, cx);
132 let cursor_offset = markers[0];
133 editor.replace_selections_with(cx, |map| cursor_offset.to_display_point(map));
134 })
135 }
136
137 pub fn assert_newest_selection_head_offset(&mut self, expected_offset: usize) {
138 let actual_head = self.newest_selection().head();
139 let (actual_offset, expected_head) = self.editor.update(self.cx, |editor, cx| {
140 let snapshot = editor.snapshot(cx);
141 (
142 actual_head.to_offset(&snapshot, Bias::Left),
143 expected_offset.to_display_point(&snapshot),
144 )
145 });
146 let mut actual_position_text = self.editor_text();
147 let mut expected_position_text = actual_position_text.clone();
148 actual_position_text.insert(actual_offset, '|');
149 expected_position_text.insert(expected_offset, '|');
150 assert_eq!(
151 actual_head, expected_head,
152 "\nActual Position: {}\nExpected Position: {}",
153 actual_position_text, expected_position_text
154 )
155 }
156
157 pub fn assert_editor_state(&mut self, text: &str) {
158 let (unmarked_text, markers) = marked_text(&text);
159 let editor_text = self.editor_text();
160 assert_eq!(
161 editor_text, unmarked_text,
162 "Unmarked text doesn't match editor text"
163 );
164 let expected_offset = markers[0];
165 let actual_head = self.newest_selection().head();
166 let (actual_offset, expected_head) = self.editor.update(self.cx, |editor, cx| {
167 let snapshot = editor.snapshot(cx);
168 (
169 actual_head.to_offset(&snapshot, Bias::Left),
170 expected_offset.to_display_point(&snapshot),
171 )
172 });
173 let mut actual_position_text = self.editor_text();
174 let mut expected_position_text = actual_position_text.clone();
175 actual_position_text.insert(actual_offset, '|');
176 expected_position_text.insert(expected_offset, '|');
177 assert_eq!(
178 actual_head, expected_head,
179 "\nActual Position: {}\nExpected Position: {}",
180 actual_position_text, expected_position_text
181 )
182 }
183
184 pub fn assert_binding<const COUNT: usize>(
185 &mut self,
186 keystrokes: [&str; COUNT],
187 initial_state: &str,
188 initial_mode: Mode,
189 state_after: &str,
190 mode_after: Mode,
191 ) {
192 self.set_state(initial_state, initial_mode);
193 self.simulate_keystrokes(keystrokes);
194 self.assert_editor_state(state_after);
195 assert_eq!(self.mode(), mode_after);
196 assert_eq!(self.active_operator(), None);
197 }
198
199 pub fn binding<const COUNT: usize>(
200 mut self,
201 keystrokes: [&'static str; COUNT],
202 ) -> VimBindingTestContext<'a, COUNT> {
203 let mode = self.mode();
204 VimBindingTestContext::new(keystrokes, mode, mode, self)
205 }
206}
207
208impl<'a> Deref for VimTestContext<'a> {
209 type Target = gpui::TestAppContext;
210
211 fn deref(&self) -> &Self::Target {
212 self.cx
213 }
214}
215
216pub struct VimBindingTestContext<'a, const COUNT: usize> {
217 cx: VimTestContext<'a>,
218 keystrokes_under_test: [&'static str; COUNT],
219 initial_mode: Mode,
220 mode_after: Mode,
221}
222
223impl<'a, const COUNT: usize> VimBindingTestContext<'a, COUNT> {
224 pub fn new(
225 keystrokes_under_test: [&'static str; COUNT],
226 initial_mode: Mode,
227 mode_after: Mode,
228 cx: VimTestContext<'a>,
229 ) -> Self {
230 Self {
231 cx,
232 keystrokes_under_test,
233 initial_mode,
234 mode_after,
235 }
236 }
237
238 pub fn binding<const NEW_COUNT: usize>(
239 self,
240 keystrokes_under_test: [&'static str; NEW_COUNT],
241 ) -> VimBindingTestContext<'a, NEW_COUNT> {
242 VimBindingTestContext {
243 keystrokes_under_test,
244 cx: self.cx,
245 initial_mode: self.initial_mode,
246 mode_after: self.mode_after,
247 }
248 }
249
250 pub fn mode_after(mut self, mode_after: Mode) -> Self {
251 self.mode_after = mode_after;
252 self
253 }
254
255 pub fn assert(&mut self, initial_state: &str, state_after: &str) {
256 self.cx.assert_binding(
257 self.keystrokes_under_test,
258 initial_state,
259 self.initial_mode,
260 state_after,
261 self.mode_after,
262 )
263 }
264}
265
266impl<'a, const COUNT: usize> Deref for VimBindingTestContext<'a, COUNT> {
267 type Target = VimTestContext<'a>;
268
269 fn deref(&self) -> &Self::Target {
270 &self.cx
271 }
272}