1use std::ops::{Deref, DerefMut};
2
3use editor::test::editor_lsp_test_context::EditorLspTestContext;
4use gpui::{Context, Entity, UpdateGlobal};
5use search::{BufferSearchBar, project_search::ProjectSearchBar};
6use semver::Version;
7
8use crate::{state::Operator, *};
9
10pub struct VimTestContext {
11 cx: EditorLspTestContext,
12}
13
14impl VimTestContext {
15 pub fn init(cx: &mut gpui::TestAppContext) {
16 if cx.has_global::<VimGlobals>() {
17 return;
18 }
19 env_logger::try_init().ok();
20 cx.update(|cx| {
21 let settings = SettingsStore::test(cx);
22 cx.set_global(settings);
23 release_channel::init(Version::new(0, 0, 0), cx);
24 command_palette::init(cx);
25 project_panel::init(cx);
26 outline_panel::init(cx);
27 git_ui::init(cx);
28 crate::init(cx);
29 search::init(cx);
30 theme::init(theme::LoadThemes::JustBase, cx);
31 settings_ui::init(cx);
32 markdown_preview::init(cx);
33 });
34 }
35
36 pub async fn new(cx: &mut gpui::TestAppContext, enabled: bool) -> VimTestContext {
37 Self::init(cx);
38 let lsp = EditorLspTestContext::new_rust(Default::default(), cx).await;
39 Self::new_with_lsp(lsp, enabled)
40 }
41
42 pub async fn new_html(cx: &mut gpui::TestAppContext) -> VimTestContext {
43 Self::init(cx);
44 Self::new_with_lsp(EditorLspTestContext::new_html(cx).await, true)
45 }
46
47 pub async fn new_markdown_with_rust(cx: &mut gpui::TestAppContext) -> VimTestContext {
48 Self::init(cx);
49 Self::new_with_lsp(EditorLspTestContext::new_markdown_with_rust(cx).await, true)
50 }
51
52 pub async fn new_typescript(cx: &mut gpui::TestAppContext) -> VimTestContext {
53 Self::init(cx);
54 Self::new_with_lsp(
55 EditorLspTestContext::new_typescript(
56 lsp::ServerCapabilities {
57 completion_provider: Some(lsp::CompletionOptions {
58 trigger_characters: Some(vec![".".to_string()]),
59 ..Default::default()
60 }),
61 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
62 prepare_provider: Some(true),
63 work_done_progress_options: Default::default(),
64 })),
65 definition_provider: Some(lsp::OneOf::Left(true)),
66 ..Default::default()
67 },
68 cx,
69 )
70 .await,
71 true,
72 )
73 }
74
75 pub async fn new_tsx(cx: &mut gpui::TestAppContext) -> VimTestContext {
76 Self::init(cx);
77 Self::new_with_lsp(
78 EditorLspTestContext::new_tsx(
79 lsp::ServerCapabilities {
80 completion_provider: Some(lsp::CompletionOptions {
81 trigger_characters: Some(vec![".".to_string()]),
82 ..Default::default()
83 }),
84 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
85 prepare_provider: Some(true),
86 work_done_progress_options: Default::default(),
87 })),
88 ..Default::default()
89 },
90 cx,
91 )
92 .await,
93 true,
94 )
95 }
96
97 pub fn init_keybindings(enabled: bool, cx: &mut App) {
98 SettingsStore::update_global(cx, |store, cx| {
99 store.update_user_settings(cx, |s| s.vim_mode = Some(enabled));
100 });
101 let mut default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
102 "keymaps/default-macos.json",
103 cx,
104 )
105 .unwrap();
106 for key_binding in &mut default_key_bindings {
107 key_binding.set_meta(settings::KeybindSource::Default.meta());
108 }
109 cx.bind_keys(default_key_bindings);
110 if enabled {
111 let vim_key_bindings = settings::KeymapFile::load_asset(
112 "keymaps/vim.json",
113 Some(settings::KeybindSource::Vim),
114 cx,
115 )
116 .unwrap();
117 cx.bind_keys(vim_key_bindings);
118 }
119 }
120
121 pub fn new_with_lsp(mut cx: EditorLspTestContext, enabled: bool) -> VimTestContext {
122 cx.update(|_, cx| {
123 Self::init_keybindings(enabled, cx);
124 });
125
126 // Setup search toolbars and keypress hook
127 cx.update_workspace(|workspace, window, cx| {
128 workspace.active_pane().update(cx, |pane, cx| {
129 pane.toolbar().update(cx, |toolbar, cx| {
130 let buffer_search_bar = cx.new(|cx| BufferSearchBar::new(None, window, cx));
131 toolbar.add_item(buffer_search_bar, window, cx);
132
133 let project_search_bar = cx.new(|_| ProjectSearchBar::new());
134 toolbar.add_item(project_search_bar, window, cx);
135 })
136 });
137 workspace.status_bar().update(cx, |status_bar, cx| {
138 let vim_mode_indicator = cx.new(|cx| ModeIndicator::new(window, cx));
139 status_bar.add_right_item(vim_mode_indicator, window, cx);
140 });
141 });
142
143 Self { cx }
144 }
145
146 pub fn update_entity<F, T, R>(&mut self, entity: Entity<T>, update: F) -> R
147 where
148 T: 'static,
149 F: FnOnce(&mut T, &mut Window, &mut Context<T>) -> R + 'static,
150 {
151 let window = self.window;
152 self.update_window(window, move |_, window, cx| {
153 entity.update(cx, |t, cx| update(t, window, cx))
154 })
155 .unwrap()
156 }
157
158 pub fn workspace<F, T>(&mut self, update: F) -> T
159 where
160 F: FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) -> T,
161 {
162 self.cx.update_workspace(update)
163 }
164
165 pub fn enable_vim(&mut self) {
166 self.cx.update(|_, cx| {
167 SettingsStore::update_global(cx, |store, cx| {
168 store.update_user_settings(cx, |s| s.vim_mode = Some(true));
169 });
170 })
171 }
172
173 pub fn disable_vim(&mut self) {
174 self.cx.update(|_, cx| {
175 SettingsStore::update_global(cx, |store, cx| {
176 store.update_user_settings(cx, |s| s.vim_mode = Some(false));
177 });
178 })
179 }
180
181 pub fn enable_helix(&mut self) {
182 self.cx.update(|_, cx| {
183 SettingsStore::update_global(cx, |store, cx| {
184 store.update_user_settings(cx, |s| s.helix_mode = Some(true));
185 });
186 })
187 }
188
189 pub fn mode(&mut self) -> Mode {
190 self.update_editor(|editor, _, cx| editor.addon::<VimAddon>().unwrap().entity.read(cx).mode)
191 }
192
193 pub fn forced_motion(&mut self) -> bool {
194 self.update_editor(|_, _, cx| cx.global::<VimGlobals>().forced_motion)
195 }
196
197 pub fn active_operator(&mut self) -> Option<Operator> {
198 self.update_editor(|editor, _, cx| {
199 editor
200 .addon::<VimAddon>()
201 .unwrap()
202 .entity
203 .read(cx)
204 .operator_stack
205 .last()
206 .cloned()
207 })
208 }
209
210 pub fn set_state(&mut self, text: &str, mode: Mode) {
211 self.cx.set_state(text);
212 let vim =
213 self.update_editor(|editor, _window, _cx| editor.addon::<VimAddon>().cloned().unwrap());
214
215 self.update(|window, cx| {
216 vim.entity.update(cx, |vim, cx| {
217 vim.switch_mode(mode, true, window, cx);
218 });
219 });
220 self.cx.cx.cx.run_until_parked();
221 }
222
223 #[track_caller]
224 pub fn assert_state(&mut self, text: &str, mode: Mode) {
225 self.assert_editor_state(text);
226 assert_eq!(self.mode(), mode, "{}", self.assertion_context());
227 }
228
229 pub fn assert_binding(
230 &mut self,
231 keystrokes: &str,
232 initial_state: &str,
233 initial_mode: Mode,
234 state_after: &str,
235 mode_after: Mode,
236 ) {
237 self.set_state(initial_state, initial_mode);
238 self.cx.simulate_keystrokes(keystrokes);
239 self.cx.assert_editor_state(state_after);
240 assert_eq!(self.mode(), mode_after, "{}", self.assertion_context());
241 assert_eq!(self.active_operator(), None, "{}", self.assertion_context());
242 }
243
244 pub fn assert_binding_normal(
245 &mut self,
246 keystrokes: &str,
247 initial_state: &str,
248 state_after: &str,
249 ) {
250 self.set_state(initial_state, Mode::Normal);
251 self.cx.simulate_keystrokes(keystrokes);
252 self.cx.assert_editor_state(state_after);
253 assert_eq!(self.mode(), Mode::Normal, "{}", self.assertion_context());
254 assert_eq!(self.active_operator(), None, "{}", self.assertion_context());
255 }
256
257 pub fn shared_clipboard(&mut self) -> VimClipboard {
258 VimClipboard {
259 editor: self
260 .read_from_clipboard()
261 .map(|item| item.text().unwrap())
262 .unwrap_or_default(),
263 }
264 }
265}
266
267pub struct VimClipboard {
268 editor: String,
269}
270
271impl VimClipboard {
272 #[track_caller]
273 pub fn assert_eq(&self, expected: &str) {
274 assert_eq!(self.editor, expected);
275 }
276}
277
278impl Deref for VimTestContext {
279 type Target = EditorLspTestContext;
280
281 fn deref(&self) -> &Self::Target {
282 &self.cx
283 }
284}
285
286impl DerefMut for VimTestContext {
287 fn deref_mut(&mut self) -> &mut Self::Target {
288 &mut self.cx
289 }
290}