neovim_backed_test_context.rs

  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}