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    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 keybindings 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
112        let selection = self.editor(|editor, cx| editor.selections.newest::<Point>(cx));
113        let text = self.buffer_text();
114        self.neovim.set_state(selection, &text).await;
115
116        context_handle
117    }
118
119    pub async fn assert_state_matches(&mut self) {
120        assert_eq!(
121            self.neovim.text().await,
122            self.buffer_text(),
123            "{}",
124            self.assertion_context()
125        );
126
127        let mut neovim_selection = self.neovim.selection().await;
128        // Zed selections adjust themselves to make the end point visually make sense
129        if neovim_selection.start > neovim_selection.end {
130            neovim_selection.start.column += 1;
131        }
132        let neovim_selection = neovim_selection.to_offset(&self.buffer_snapshot());
133        self.assert_editor_selections(vec![neovim_selection]);
134
135        if let Some(neovim_mode) = self.neovim.mode().await {
136            assert_eq!(neovim_mode, self.mode(), "{}", self.assertion_context(),);
137        }
138    }
139
140    pub async fn assert_binding_matches<const COUNT: usize>(
141        &mut self,
142        keystrokes: [&str; COUNT],
143        initial_state: &str,
144    ) -> Option<(ContextHandle, ContextHandle)> {
145        if let Some(possible_exempted_keystrokes) = self.exemptions.get(initial_state) {
146            match possible_exempted_keystrokes {
147                Some(exempted_keystrokes) => {
148                    if exempted_keystrokes.contains(&format!("{keystrokes:?}")) {
149                        // This keystroke was exempted for this insertion text
150                        return None;
151                    }
152                }
153                None => {
154                    // All keystrokes for this insertion text are exempted
155                    return None;
156                }
157            }
158        }
159
160        let _state_context = self.set_shared_state(initial_state).await;
161        let _keystroke_context = self.simulate_shared_keystrokes(keystrokes).await;
162        self.assert_state_matches().await;
163        Some((_state_context, _keystroke_context))
164    }
165
166    pub async fn assert_binding_matches_all<const COUNT: usize>(
167        &mut self,
168        keystrokes: [&str; COUNT],
169        marked_positions: &str,
170    ) {
171        let (unmarked_text, cursor_offsets) = marked_text_offsets(marked_positions);
172
173        for cursor_offset in cursor_offsets.iter() {
174            let mut marked_text = unmarked_text.clone();
175            marked_text.insert(*cursor_offset, 'ˇ');
176
177            self.assert_binding_matches(keystrokes, &marked_text).await;
178        }
179    }
180
181    pub async fn assert_binding_matches_all_exempted<const COUNT: usize>(
182        &mut self,
183        keystrokes: [&str; COUNT],
184        marked_positions: &str,
185        feature: ExemptionFeatures,
186    ) {
187        if SUPPORTED_FEATURES.contains(&feature) {
188            self.assert_binding_matches_all(keystrokes, marked_positions)
189                .await
190        }
191    }
192
193    pub fn binding<const COUNT: usize>(
194        self,
195        keystrokes: [&'static str; COUNT],
196    ) -> NeovimBackedBindingTestContext<'a, COUNT> {
197        NeovimBackedBindingTestContext::new(keystrokes, self)
198    }
199}
200
201impl<'a> Deref for NeovimBackedTestContext<'a> {
202    type Target = VimTestContext<'a>;
203
204    fn deref(&self) -> &Self::Target {
205        &self.cx
206    }
207}
208
209impl<'a> DerefMut for NeovimBackedTestContext<'a> {
210    fn deref_mut(&mut self) -> &mut Self::Target {
211        &mut self.cx
212    }
213}
214
215#[cfg(test)]
216mod test {
217    use gpui::TestAppContext;
218
219    use crate::test::NeovimBackedTestContext;
220
221    #[gpui::test]
222    async fn neovim_backed_test_context_works(cx: &mut TestAppContext) {
223        let mut cx = NeovimBackedTestContext::new(cx).await;
224        cx.assert_state_matches().await;
225        cx.set_shared_state("This is a tesˇt").await;
226        cx.assert_state_matches().await;
227    }
228}