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}