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 struct NeovimBackedTestContext<'a> {
 12    cx: VimTestContext<'a>,
 13    // Lookup for exempted assertions. Keyed by the insertion text, and with a value indicating which
 14    // bindings are exempted. If None, all bindings are ignored for that insertion text.
 15    exemptions: HashMap<String, Option<HashSet<String>>>,
 16    neovim: NeovimConnection,
 17}
 18
 19impl<'a> NeovimBackedTestContext<'a> {
 20    pub async fn new(cx: &'a mut gpui::TestAppContext) -> NeovimBackedTestContext<'a> {
 21        let function_name = cx.function_name.clone();
 22        let cx = VimTestContext::new(cx, true).await;
 23        Self {
 24            cx,
 25            exemptions: Default::default(),
 26            neovim: NeovimConnection::new(function_name).await,
 27        }
 28    }
 29
 30    pub fn add_initial_state_exemption(&mut self, initial_state: &str) {
 31        let initial_state = initial_state.to_string();
 32        // None represents all keybindings being exempted for that initial state
 33        self.exemptions.insert(initial_state, None);
 34    }
 35
 36    pub async fn simulate_shared_keystroke(&mut self, keystroke_text: &str) -> ContextHandle {
 37        self.neovim.send_keystroke(keystroke_text).await;
 38        self.simulate_keystroke(keystroke_text)
 39    }
 40
 41    pub async fn simulate_shared_keystrokes<const COUNT: usize>(
 42        &mut self,
 43        keystroke_texts: [&str; COUNT],
 44    ) -> ContextHandle {
 45        for keystroke_text in keystroke_texts.into_iter() {
 46            self.neovim.send_keystroke(keystroke_text).await;
 47        }
 48        self.simulate_keystrokes(keystroke_texts)
 49    }
 50
 51    pub async fn set_shared_state(&mut self, marked_text: &str) -> ContextHandle {
 52        let context_handle = self.set_state(marked_text, Mode::Normal);
 53
 54        let selection = self.editor(|editor, cx| editor.selections.newest::<Point>(cx));
 55        let text = self.buffer_text();
 56        self.neovim.set_state(selection, &text).await;
 57
 58        context_handle
 59    }
 60
 61    pub async fn assert_state_matches(&mut self) {
 62        assert_eq!(
 63            self.neovim.text().await,
 64            self.buffer_text(),
 65            "{}",
 66            self.assertion_context()
 67        );
 68
 69        let mut neovim_selection = self.neovim.selection().await;
 70        // Zed selections adjust themselves to make the end point visually make sense
 71        if neovim_selection.start > neovim_selection.end {
 72            neovim_selection.start.column += 1;
 73        }
 74        let neovim_selection = neovim_selection.to_offset(&self.buffer_snapshot());
 75        self.assert_editor_selections(vec![neovim_selection]);
 76
 77        if let Some(neovim_mode) = self.neovim.mode().await {
 78            assert_eq!(neovim_mode, self.mode(), "{}", self.assertion_context(),);
 79        }
 80    }
 81
 82    pub async fn assert_binding_matches<const COUNT: usize>(
 83        &mut self,
 84        keystrokes: [&str; COUNT],
 85        initial_state: &str,
 86    ) -> Option<(ContextHandle, ContextHandle)> {
 87        if let Some(possible_exempted_keystrokes) = self.exemptions.get(initial_state) {
 88            match possible_exempted_keystrokes {
 89                Some(exempted_keystrokes) => {
 90                    if exempted_keystrokes.contains(&format!("{keystrokes:?}")) {
 91                        // This keystroke was exempted for this insertion text
 92                        return None;
 93                    }
 94                }
 95                None => {
 96                    // All keystrokes for this insertion text are exempted
 97                    return None;
 98                }
 99            }
100        }
101
102        let _state_context = self.set_shared_state(initial_state).await;
103        let _keystroke_context = self.simulate_shared_keystrokes(keystrokes).await;
104        self.assert_state_matches().await;
105        Some((_state_context, _keystroke_context))
106    }
107
108    pub async fn assert_binding_matches_all<const COUNT: usize>(
109        &mut self,
110        keystrokes: [&str; COUNT],
111        marked_positions: &str,
112    ) {
113        let (unmarked_text, cursor_offsets) = marked_text_offsets(marked_positions);
114
115        for cursor_offset in cursor_offsets.iter() {
116            let mut marked_text = unmarked_text.clone();
117            marked_text.insert(*cursor_offset, 'ˇ');
118
119            self.assert_binding_matches(keystrokes, &marked_text).await;
120        }
121    }
122
123    pub fn binding<const COUNT: usize>(
124        self,
125        keystrokes: [&'static str; COUNT],
126    ) -> NeovimBackedBindingTestContext<'a, COUNT> {
127        NeovimBackedBindingTestContext::new(keystrokes, self)
128    }
129}
130
131impl<'a> Deref for NeovimBackedTestContext<'a> {
132    type Target = VimTestContext<'a>;
133
134    fn deref(&self) -> &Self::Target {
135        &self.cx
136    }
137}
138
139impl<'a> DerefMut for NeovimBackedTestContext<'a> {
140    fn deref_mut(&mut self) -> &mut Self::Target {
141        &mut self.cx
142    }
143}
144
145#[cfg(test)]
146mod test {
147    use gpui::TestAppContext;
148
149    use crate::test::NeovimBackedTestContext;
150
151    #[gpui::test]
152    async fn neovim_backed_test_context_works(cx: &mut TestAppContext) {
153        let mut cx = NeovimBackedTestContext::new(cx).await;
154        cx.assert_state_matches().await;
155        cx.set_shared_state("This is a tesˇt").await;
156        cx.assert_state_matches().await;
157    }
158}