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