neovim_backed_test_context.rs

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