1#![allow(unused)]
2// todo!()
3
4use editor::{scroll::VERTICAL_SCROLL_MARGIN, test::editor_test_context::ContextHandle};
5use indoc::indoc;
6use settings::SettingsStore;
7use std::{
8 ops::{Deref, DerefMut},
9 panic, thread,
10};
11
12use collections::{HashMap, HashSet};
13use language::language_settings::{AllLanguageSettings, SoftWrap};
14use util::test::marked_text_offsets;
15
16use super::{neovim_connection::NeovimConnection, NeovimBackedBindingTestContext, VimTestContext};
17use crate::state::Mode;
18
19pub const SUPPORTED_FEATURES: &[ExemptionFeatures] = &[];
20
21/// Enum representing features we have tests for but which don't work, yet. Used
22/// to add exemptions and automatically
23#[derive(PartialEq, Eq)]
24pub enum ExemptionFeatures {
25 // MOTIONS
26 // When an operator completes at the end of the file, an extra newline is left
27 OperatorLastNewlineRemains,
28
29 // OBJECTS
30 // Resulting position after the operation is slightly incorrect for unintuitive reasons.
31 IncorrectLandingPosition,
32 // Operator around the text object at the end of the line doesn't remove whitespace.
33 AroundObjectLeavesWhitespaceAtEndOfLine,
34 // Sentence object on empty lines
35 SentenceOnEmptyLines,
36 // Whitespace isn't included with text objects at the start of the line
37 SentenceAtStartOfLineWithWhitespace,
38 // Whitespace around sentences is slightly incorrect when starting between sentences
39 AroundSentenceStartingBetweenIncludesWrongWhitespace,
40 // Non empty selection with text objects in visual mode
41 NonEmptyVisualTextObjects,
42 // Sentence Doesn't backtrack when its at the end of the file
43 SentenceAfterPunctuationAtEndOfFile,
44}
45
46impl ExemptionFeatures {
47 pub fn supported(&self) -> bool {
48 SUPPORTED_FEATURES.contains(self)
49 }
50}
51
52pub struct NeovimBackedTestContext<'a> {
53 cx: VimTestContext<'a>,
54 // Lookup for exempted assertions. Keyed by the insertion text, and with a value indicating which
55 // bindings are exempted. If None, all bindings are ignored for that insertion text.
56 exemptions: HashMap<String, Option<HashSet<String>>>,
57 neovim: NeovimConnection,
58
59 last_set_state: Option<String>,
60 recent_keystrokes: Vec<String>,
61
62 is_dirty: bool,
63}
64
65impl<'a> NeovimBackedTestContext<'a> {
66 pub async fn new(cx: &'a mut gpui::TestAppContext) -> NeovimBackedTestContext<'a> {
67 // rust stores the name of the test on the current thread.
68 // We use this to automatically name a file that will store
69 // the neovim connection's requests/responses so that we can
70 // run without neovim on CI.
71 let thread = thread::current();
72 let test_name = thread
73 .name()
74 .expect("thread is not named")
75 .split(":")
76 .last()
77 .unwrap()
78 .to_string();
79 Self {
80 cx: VimTestContext::new(cx, true).await,
81 exemptions: Default::default(),
82 neovim: NeovimConnection::new(test_name).await,
83
84 last_set_state: None,
85 recent_keystrokes: Default::default(),
86 is_dirty: false,
87 }
88 }
89
90 pub fn add_initial_state_exemptions(
91 &mut self,
92 marked_positions: &str,
93 missing_feature: ExemptionFeatures, // Feature required to support this exempted test case
94 ) {
95 if !missing_feature.supported() {
96 let (unmarked_text, cursor_offsets) = marked_text_offsets(marked_positions);
97
98 for cursor_offset in cursor_offsets.iter() {
99 let mut marked_text = unmarked_text.clone();
100 marked_text.insert(*cursor_offset, 'ˇ');
101
102 // None represents all key bindings being exempted for that initial state
103 self.exemptions.insert(marked_text, None);
104 }
105 }
106 }
107
108 pub async fn simulate_shared_keystroke(&mut self, keystroke_text: &str) -> ContextHandle {
109 self.neovim.send_keystroke(keystroke_text).await;
110 self.simulate_keystroke(keystroke_text)
111 }
112
113 pub async fn simulate_shared_keystrokes<const COUNT: usize>(
114 &mut self,
115 keystroke_texts: [&str; COUNT],
116 ) {
117 for keystroke_text in keystroke_texts.into_iter() {
118 self.recent_keystrokes.push(keystroke_text.to_string());
119 self.neovim.send_keystroke(keystroke_text).await;
120 }
121 self.simulate_keystrokes(keystroke_texts);
122 }
123
124 pub async fn set_shared_state(&mut self, marked_text: &str) {
125 let mode = if marked_text.contains("»") {
126 Mode::Visual
127 } else {
128 Mode::Normal
129 };
130 self.set_state(marked_text, mode);
131 self.last_set_state = Some(marked_text.to_string());
132 self.recent_keystrokes = Vec::new();
133 self.neovim.set_state(marked_text).await;
134 self.is_dirty = true;
135 }
136
137 pub async fn set_shared_wrap(&mut self, columns: u32) {
138 if columns < 12 {
139 panic!("nvim doesn't support columns < 12")
140 }
141 self.neovim.set_option("wrap").await;
142 self.neovim
143 .set_option(&format!("columns={}", columns))
144 .await;
145
146 self.update(|cx| {
147 cx.update_global(|settings: &mut SettingsStore, cx| {
148 settings.update_user_settings::<AllLanguageSettings>(cx, |settings| {
149 settings.defaults.soft_wrap = Some(SoftWrap::PreferredLineLength);
150 settings.defaults.preferred_line_length = Some(columns);
151 });
152 })
153 })
154 }
155
156 // todo!()
157 // pub async fn set_scroll_height(&mut self, rows: u32) {
158 // // match Zed's scrolling behavior
159 // self.neovim
160 // .set_option(&format!("scrolloff={}", VERTICAL_SCROLL_MARGIN))
161 // .await;
162 // // +2 to account for the vim command UI at the bottom.
163 // self.neovim.set_option(&format!("lines={}", rows + 2)).await;
164 // let window = self.window;
165 // let line_height =
166 // self.editor(|editor, cx| editor.style().text.line_height(cx.font_cache()));
167
168 // window.simulate_resize(vec2f(1000., (rows as f32) * line_height), &mut self.cx);
169 // }
170
171 pub async fn set_neovim_option(&mut self, option: &str) {
172 self.neovim.set_option(option).await;
173 }
174
175 pub async fn assert_shared_state(&mut self, marked_text: &str) {
176 self.is_dirty = false;
177 let marked_text = marked_text.replace("•", " ");
178 let neovim = self.neovim_state().await;
179 let editor = self.editor_state();
180 if neovim == marked_text && neovim == editor {
181 return;
182 }
183 let initial_state = self
184 .last_set_state
185 .as_ref()
186 .unwrap_or(&"N/A".to_string())
187 .clone();
188
189 let message = if neovim != marked_text {
190 "Test is incorrect (currently expected != neovim_state)"
191 } else {
192 "Editor does not match nvim behaviour"
193 };
194 panic!(
195 indoc! {"{}
196 # initial state:
197 {}
198 # keystrokes:
199 {}
200 # currently expected:
201 {}
202 # neovim state:
203 {}
204 # zed state:
205 {}"},
206 message,
207 initial_state,
208 self.recent_keystrokes.join(" "),
209 marked_text.replace(" \n", "•\n"),
210 neovim.replace(" \n", "•\n"),
211 editor.replace(" \n", "•\n")
212 )
213 }
214
215 pub async fn assert_shared_clipboard(&mut self, text: &str) {
216 let neovim = self.neovim.read_register('"').await;
217 let editor = self.read_from_clipboard().unwrap().text().clone();
218
219 if text == neovim && text == editor {
220 return;
221 }
222
223 let message = if neovim != text {
224 "Test is incorrect (currently expected != neovim)"
225 } else {
226 "Editor does not match nvim behaviour"
227 };
228
229 let initial_state = self
230 .last_set_state
231 .as_ref()
232 .unwrap_or(&"N/A".to_string())
233 .clone();
234
235 panic!(
236 indoc! {"{}
237 # initial state:
238 {}
239 # keystrokes:
240 {}
241 # currently expected:
242 {}
243 # neovim clipboard:
244 {}
245 # zed clipboard:
246 {}"},
247 message,
248 initial_state,
249 self.recent_keystrokes.join(" "),
250 text,
251 neovim,
252 editor
253 )
254 }
255
256 pub async fn neovim_state(&mut self) -> String {
257 self.neovim.marked_text().await
258 }
259
260 pub async fn neovim_mode(&mut self) -> Mode {
261 self.neovim.mode().await.unwrap()
262 }
263
264 pub async fn assert_state_matches(&mut self) {
265 self.is_dirty = false;
266 let neovim = self.neovim_state().await;
267 let editor = self.editor_state();
268 let initial_state = self
269 .last_set_state
270 .as_ref()
271 .unwrap_or(&"N/A".to_string())
272 .clone();
273
274 if neovim != editor {
275 panic!(
276 indoc! {"Test failed (zed does not match nvim behaviour)
277 # initial state:
278 {}
279 # keystrokes:
280 {}
281 # neovim state:
282 {}
283 # zed state:
284 {}"},
285 initial_state,
286 self.recent_keystrokes.join(" "),
287 neovim,
288 editor,
289 )
290 }
291 }
292
293 pub async fn assert_binding_matches<const COUNT: usize>(
294 &mut self,
295 keystrokes: [&str; COUNT],
296 initial_state: &str,
297 ) {
298 if let Some(possible_exempted_keystrokes) = self.exemptions.get(initial_state) {
299 match possible_exempted_keystrokes {
300 Some(exempted_keystrokes) => {
301 if exempted_keystrokes.contains(&format!("{keystrokes:?}")) {
302 // This keystroke was exempted for this insertion text
303 return;
304 }
305 }
306 None => {
307 // All keystrokes for this insertion text are exempted
308 return;
309 }
310 }
311 }
312
313 let _state_context = self.set_shared_state(initial_state).await;
314 let _keystroke_context = self.simulate_shared_keystrokes(keystrokes).await;
315 self.assert_state_matches().await;
316 }
317
318 pub async fn assert_binding_matches_all<const COUNT: usize>(
319 &mut self,
320 keystrokes: [&str; COUNT],
321 marked_positions: &str,
322 ) {
323 let (unmarked_text, cursor_offsets) = marked_text_offsets(marked_positions);
324
325 for cursor_offset in cursor_offsets.iter() {
326 let mut marked_text = unmarked_text.clone();
327 marked_text.insert(*cursor_offset, 'ˇ');
328
329 self.assert_binding_matches(keystrokes, &marked_text).await;
330 }
331 }
332
333 pub fn each_marked_position(&self, marked_positions: &str) -> Vec<String> {
334 let (unmarked_text, cursor_offsets) = marked_text_offsets(marked_positions);
335 let mut ret = Vec::with_capacity(cursor_offsets.len());
336
337 for cursor_offset in cursor_offsets.iter() {
338 let mut marked_text = unmarked_text.clone();
339 marked_text.insert(*cursor_offset, 'ˇ');
340 ret.push(marked_text)
341 }
342
343 ret
344 }
345
346 pub async fn assert_neovim_compatible<const COUNT: usize>(
347 &mut self,
348 marked_positions: &str,
349 keystrokes: [&str; COUNT],
350 ) {
351 self.set_shared_state(&marked_positions).await;
352 self.simulate_shared_keystrokes(keystrokes).await;
353 self.assert_state_matches().await;
354 }
355
356 pub async fn assert_matches_neovim<const COUNT: usize>(
357 &mut self,
358 marked_positions: &str,
359 keystrokes: [&str; COUNT],
360 result: &str,
361 ) {
362 self.set_shared_state(marked_positions).await;
363 self.simulate_shared_keystrokes(keystrokes).await;
364 self.assert_shared_state(result).await;
365 }
366
367 pub async fn assert_binding_matches_all_exempted<const COUNT: usize>(
368 &mut self,
369 keystrokes: [&str; COUNT],
370 marked_positions: &str,
371 feature: ExemptionFeatures,
372 ) {
373 if SUPPORTED_FEATURES.contains(&feature) {
374 self.assert_binding_matches_all(keystrokes, marked_positions)
375 .await
376 }
377 }
378
379 pub fn binding<const COUNT: usize>(
380 self,
381 keystrokes: [&'static str; COUNT],
382 ) -> NeovimBackedBindingTestContext<'a, COUNT> {
383 NeovimBackedBindingTestContext::new(keystrokes, self)
384 }
385}
386
387impl<'a> Deref for NeovimBackedTestContext<'a> {
388 type Target = VimTestContext<'a>;
389
390 fn deref(&self) -> &Self::Target {
391 &self.cx
392 }
393}
394
395impl<'a> DerefMut for NeovimBackedTestContext<'a> {
396 fn deref_mut(&mut self) -> &mut Self::Target {
397 &mut self.cx
398 }
399}
400
401// a common mistake in tests is to call set_shared_state when
402// you mean asswert_shared_state. This notices that and lets
403// you know.
404impl<'a> Drop for NeovimBackedTestContext<'a> {
405 fn drop(&mut self) {
406 if self.is_dirty {
407 panic!("Test context was dropped after set_shared_state before assert_shared_state")
408 }
409 }
410}
411
412#[cfg(test)]
413mod test {
414 use gpui::TestAppContext;
415
416 use crate::test::NeovimBackedTestContext;
417
418 #[gpui::test]
419 async fn neovim_backed_test_context_works(cx: &mut TestAppContext) {
420 let mut cx = NeovimBackedTestContext::new(cx).await;
421 cx.assert_state_matches().await;
422 cx.set_shared_state("This is a tesˇt").await;
423 cx.assert_state_matches().await;
424 }
425}