1use std::ops::{Deref, DerefMut};
2
3use collections::{HashMap, HashSet};
4use gpui::ContextHandle;
5use language::{OffsetRangeExt, Point};
6use util::test::marked_text_offsets;
7
8use super::{neovim_connection::NeovimConnection, NeovimBackedBindingTestContext, VimTestContext};
9use crate::state::Mode;
10
11pub const SUPPORTED_FEATURES: &[ExemptionFeatures] = &[];
12
13/// Enum representing features we have tests for but which don't work, yet. Used
14/// to add exemptions and automatically
15#[derive(PartialEq, Eq)]
16pub enum ExemptionFeatures {
17 // MOTIONS
18 // Deletions on empty lines miss some newlines
19 DeletionOnEmptyLine,
20 // When a motion fails, it should should not apply linewise operations
21 OperatorAbortsOnFailedMotion,
22
23 // OBJECTS
24 // Resulting position after the operation is slightly incorrect for unintuitive reasons.
25 IncorrectLandingPosition,
26 // Operator around the text object at the end of the line doesn't remove whitespace.
27 AroundObjectLeavesWhitespaceAtEndOfLine,
28 // Sentence object on empty lines
29 SentenceOnEmptyLines,
30 // Whitespace isn't included with text objects at the start of the line
31 SentenceAtStartOfLineWithWhitespace,
32 // Whitespace around sentences is slightly incorrect when starting between sentences
33 AroundSentenceStartingBetweenIncludesWrongWhitespace,
34 // Non empty selection with text objects in visual mode
35 NonEmptyVisualTextObjects,
36 // Quote style surrounding text objects don't seek forward properly
37 QuotesSeekForward,
38 // Neovim freezes up for some reason with angle brackets
39 AngleBracketsFreezeNeovim,
40 // Sentence Doesn't backtrack when its at the end of the file
41 SentenceAfterPunctuationAtEndOfFile,
42}
43
44impl ExemptionFeatures {
45 pub fn supported(&self) -> bool {
46 SUPPORTED_FEATURES.contains(self)
47 }
48}
49
50pub struct NeovimBackedTestContext<'a> {
51 cx: VimTestContext<'a>,
52 // Lookup for exempted assertions. Keyed by the insertion text, and with a value indicating which
53 // bindings are exempted. If None, all bindings are ignored for that insertion text.
54 exemptions: HashMap<String, Option<HashSet<String>>>,
55 neovim: NeovimConnection,
56}
57
58impl<'a> NeovimBackedTestContext<'a> {
59 pub async fn new(cx: &'a mut gpui::TestAppContext) -> NeovimBackedTestContext<'a> {
60 let function_name = cx.function_name.clone();
61 let cx = VimTestContext::new(cx, true).await;
62 Self {
63 cx,
64 exemptions: Default::default(),
65 neovim: NeovimConnection::new(function_name).await,
66 }
67 }
68
69 pub fn add_initial_state_exemptions(
70 &mut self,
71 marked_positions: &str,
72 missing_feature: ExemptionFeatures, // Feature required to support this exempted test case
73 ) {
74 if !missing_feature.supported() {
75 let (unmarked_text, cursor_offsets) = marked_text_offsets(marked_positions);
76
77 for cursor_offset in cursor_offsets.iter() {
78 let mut marked_text = unmarked_text.clone();
79 marked_text.insert(*cursor_offset, 'ˇ');
80
81 // None represents all keybindings being exempted for that initial state
82 self.exemptions.insert(marked_text, None);
83 }
84 }
85 }
86
87 pub async fn simulate_shared_keystroke(&mut self, keystroke_text: &str) -> ContextHandle {
88 self.neovim.send_keystroke(keystroke_text).await;
89 self.simulate_keystroke(keystroke_text)
90 }
91
92 pub async fn simulate_shared_keystrokes<const COUNT: usize>(
93 &mut self,
94 keystroke_texts: [&str; COUNT],
95 ) -> ContextHandle {
96 for keystroke_text in keystroke_texts.into_iter() {
97 self.neovim.send_keystroke(keystroke_text).await;
98 }
99 self.simulate_keystrokes(keystroke_texts)
100 }
101
102 pub async fn set_shared_state(&mut self, marked_text: &str) -> ContextHandle {
103 let context_handle = self.set_state(marked_text, Mode::Normal);
104
105 let selection = self.editor(|editor, cx| editor.selections.newest::<Point>(cx));
106 let text = self.buffer_text();
107 self.neovim.set_state(selection, &text).await;
108
109 context_handle
110 }
111
112 pub async fn assert_state_matches(&mut self) {
113 assert_eq!(
114 self.neovim.text().await,
115 self.buffer_text(),
116 "{}",
117 self.assertion_context()
118 );
119
120 let mut neovim_selection = self.neovim.selection().await;
121 // Zed selections adjust themselves to make the end point visually make sense
122 if neovim_selection.start > neovim_selection.end {
123 neovim_selection.start.column += 1;
124 }
125 let neovim_selection = neovim_selection.to_offset(&self.buffer_snapshot());
126 self.assert_editor_selections(vec![neovim_selection]);
127
128 if let Some(neovim_mode) = self.neovim.mode().await {
129 assert_eq!(neovim_mode, self.mode(), "{}", self.assertion_context(),);
130 }
131 }
132
133 pub async fn assert_binding_matches<const COUNT: usize>(
134 &mut self,
135 keystrokes: [&str; COUNT],
136 initial_state: &str,
137 ) -> Option<(ContextHandle, ContextHandle)> {
138 if let Some(possible_exempted_keystrokes) = self.exemptions.get(initial_state) {
139 match possible_exempted_keystrokes {
140 Some(exempted_keystrokes) => {
141 if exempted_keystrokes.contains(&format!("{keystrokes:?}")) {
142 // This keystroke was exempted for this insertion text
143 return None;
144 }
145 }
146 None => {
147 // All keystrokes for this insertion text are exempted
148 return None;
149 }
150 }
151 }
152
153 let _state_context = self.set_shared_state(initial_state).await;
154 let _keystroke_context = self.simulate_shared_keystrokes(keystrokes).await;
155 self.assert_state_matches().await;
156 Some((_state_context, _keystroke_context))
157 }
158
159 pub async fn assert_binding_matches_all<const COUNT: usize>(
160 &mut self,
161 keystrokes: [&str; COUNT],
162 marked_positions: &str,
163 ) {
164 let (unmarked_text, cursor_offsets) = marked_text_offsets(marked_positions);
165
166 for cursor_offset in cursor_offsets.iter() {
167 let mut marked_text = unmarked_text.clone();
168 marked_text.insert(*cursor_offset, 'ˇ');
169
170 self.assert_binding_matches(keystrokes, &marked_text).await;
171 }
172 }
173
174 pub async fn assert_binding_matches_all_exempted<const COUNT: usize>(
175 &mut self,
176 keystrokes: [&str; COUNT],
177 marked_positions: &str,
178 feature: ExemptionFeatures,
179 ) {
180 if SUPPORTED_FEATURES.contains(&feature) {
181 self.assert_binding_matches_all(keystrokes, marked_positions)
182 .await
183 }
184 }
185
186 pub fn binding<const COUNT: usize>(
187 self,
188 keystrokes: [&'static str; COUNT],
189 ) -> NeovimBackedBindingTestContext<'a, COUNT> {
190 NeovimBackedBindingTestContext::new(keystrokes, self)
191 }
192}
193
194impl<'a> Deref for NeovimBackedTestContext<'a> {
195 type Target = VimTestContext<'a>;
196
197 fn deref(&self) -> &Self::Target {
198 &self.cx
199 }
200}
201
202impl<'a> DerefMut for NeovimBackedTestContext<'a> {
203 fn deref_mut(&mut self) -> &mut Self::Target {
204 &mut self.cx
205 }
206}
207
208#[cfg(test)]
209mod test {
210 use gpui::TestAppContext;
211
212 use crate::test::NeovimBackedTestContext;
213
214 #[gpui::test]
215 async fn neovim_backed_test_context_works(cx: &mut TestAppContext) {
216 let mut cx = NeovimBackedTestContext::new(cx).await;
217 cx.assert_state_matches().await;
218 cx.set_shared_state("This is a tesˇt").await;
219 cx.assert_state_matches().await;
220 }
221}