1use crate::{
2 AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels,
3 DummyKeyboardMapper, ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay,
4 PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, PromptButton,
5 ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, SourceMetadata, Task,
6 TestDisplay, TestWindow, ThermalState, WindowAppearance, WindowParams, size,
7};
8use anyhow::Result;
9use collections::VecDeque;
10use futures::channel::oneshot;
11use parking_lot::Mutex;
12use std::{
13 cell::RefCell,
14 path::{Path, PathBuf},
15 rc::{Rc, Weak},
16 sync::Arc,
17};
18
19/// TestPlatform implements the Platform trait for use in tests.
20pub(crate) struct TestPlatform {
21 background_executor: BackgroundExecutor,
22 foreground_executor: ForegroundExecutor,
23
24 pub(crate) active_window: RefCell<Option<TestWindow>>,
25 active_display: Rc<dyn PlatformDisplay>,
26 active_cursor: Mutex<CursorStyle>,
27 current_clipboard_item: Mutex<Option<ClipboardItem>>,
28 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
29 current_primary_item: Mutex<Option<ClipboardItem>>,
30 #[cfg(target_os = "macos")]
31 current_find_pasteboard_item: Mutex<Option<ClipboardItem>>,
32 pub(crate) prompts: RefCell<TestPrompts>,
33 screen_capture_sources: RefCell<Vec<TestScreenCaptureSource>>,
34 pub opened_url: RefCell<Option<String>>,
35 pub text_system: Arc<dyn PlatformTextSystem>,
36 pub expect_restart: RefCell<Option<oneshot::Sender<Option<PathBuf>>>>,
37 weak: Weak<Self>,
38}
39
40#[derive(Clone)]
41/// A fake screen capture source, used for testing.
42pub struct TestScreenCaptureSource {}
43
44/// A fake screen capture stream, used for testing.
45pub struct TestScreenCaptureStream {}
46
47impl ScreenCaptureSource for TestScreenCaptureSource {
48 fn metadata(&self) -> Result<SourceMetadata> {
49 Ok(SourceMetadata {
50 id: 0,
51 is_main: None,
52 label: None,
53 resolution: size(DevicePixels(1), DevicePixels(1)),
54 })
55 }
56
57 fn stream(
58 &self,
59 _foreground_executor: &ForegroundExecutor,
60 _frame_callback: Box<dyn Fn(ScreenCaptureFrame) + Send>,
61 ) -> oneshot::Receiver<Result<Box<dyn ScreenCaptureStream>>> {
62 let (mut tx, rx) = oneshot::channel();
63 let stream = TestScreenCaptureStream {};
64 tx.send(Ok(Box::new(stream) as Box<dyn ScreenCaptureStream>))
65 .ok();
66 rx
67 }
68}
69
70impl ScreenCaptureStream for TestScreenCaptureStream {
71 fn metadata(&self) -> Result<SourceMetadata> {
72 TestScreenCaptureSource {}.metadata()
73 }
74}
75
76struct TestPrompt {
77 msg: String,
78 detail: Option<String>,
79 answers: Vec<String>,
80 tx: oneshot::Sender<usize>,
81}
82
83#[derive(Default)]
84pub(crate) struct TestPrompts {
85 multiple_choice: VecDeque<TestPrompt>,
86 new_path: VecDeque<(PathBuf, oneshot::Sender<Result<Option<PathBuf>>>)>,
87}
88
89impl TestPlatform {
90 pub fn new(executor: BackgroundExecutor, foreground_executor: ForegroundExecutor) -> Rc<Self> {
91 let text_system = Arc::new(NoopTextSystem);
92
93 Rc::new_cyclic(|weak| TestPlatform {
94 background_executor: executor,
95 foreground_executor,
96 prompts: Default::default(),
97 screen_capture_sources: Default::default(),
98 active_cursor: Default::default(),
99 active_display: Rc::new(TestDisplay::new()),
100 active_window: Default::default(),
101 expect_restart: Default::default(),
102 current_clipboard_item: Mutex::new(None),
103 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
104 current_primary_item: Mutex::new(None),
105 #[cfg(target_os = "macos")]
106 current_find_pasteboard_item: Mutex::new(None),
107 weak: weak.clone(),
108 opened_url: Default::default(),
109 text_system,
110 })
111 }
112
113 pub(crate) fn simulate_new_path_selection(
114 &self,
115 select_path: impl FnOnce(&std::path::Path) -> Option<std::path::PathBuf>,
116 ) {
117 let (path, tx) = self
118 .prompts
119 .borrow_mut()
120 .new_path
121 .pop_front()
122 .expect("no pending new path prompt");
123 tx.send(Ok(select_path(&path))).ok();
124 }
125
126 #[track_caller]
127 pub(crate) fn simulate_prompt_answer(&self, response: &str) {
128 let prompt = self
129 .prompts
130 .borrow_mut()
131 .multiple_choice
132 .pop_front()
133 .expect("no pending multiple choice prompt");
134 let Some(ix) = prompt.answers.iter().position(|a| a == response) else {
135 panic!(
136 "PROMPT: {}\n{:?}\n{:?}\nCannot respond with {}",
137 prompt.msg, prompt.detail, prompt.answers, response
138 )
139 };
140 prompt.tx.send(ix).ok();
141 }
142
143 pub(crate) fn has_pending_prompt(&self) -> bool {
144 !self.prompts.borrow().multiple_choice.is_empty()
145 }
146
147 pub(crate) fn pending_prompt(&self) -> Option<(String, String)> {
148 let prompts = self.prompts.borrow();
149 let prompt = prompts.multiple_choice.front()?;
150 Some((
151 prompt.msg.clone(),
152 prompt.detail.clone().unwrap_or_default(),
153 ))
154 }
155
156 pub(crate) fn set_screen_capture_sources(&self, sources: Vec<TestScreenCaptureSource>) {
157 *self.screen_capture_sources.borrow_mut() = sources;
158 }
159
160 pub(crate) fn prompt(
161 &self,
162 msg: &str,
163 detail: Option<&str>,
164 answers: &[PromptButton],
165 ) -> oneshot::Receiver<usize> {
166 let (tx, rx) = oneshot::channel();
167 let answers: Vec<String> = answers.iter().map(|s| s.label().to_string()).collect();
168 self.prompts
169 .borrow_mut()
170 .multiple_choice
171 .push_back(TestPrompt {
172 msg: msg.to_string(),
173 detail: detail.map(|s| s.to_string()),
174 answers,
175 tx,
176 });
177 rx
178 }
179
180 pub(crate) fn set_active_window(&self, window: Option<TestWindow>) {
181 let executor = self.foreground_executor();
182 let previous_window = self.active_window.borrow_mut().take();
183 self.active_window.borrow_mut().clone_from(&window);
184
185 executor
186 .spawn(async move {
187 if let Some(previous_window) = previous_window {
188 if let Some(window) = window.as_ref()
189 && Rc::ptr_eq(&previous_window.0, &window.0)
190 {
191 return;
192 }
193 previous_window.simulate_active_status_change(false);
194 }
195 if let Some(window) = window {
196 window.simulate_active_status_change(true);
197 }
198 })
199 .detach();
200 }
201
202 pub(crate) fn did_prompt_for_new_path(&self) -> bool {
203 !self.prompts.borrow().new_path.is_empty()
204 }
205}
206
207impl Platform for TestPlatform {
208 fn background_executor(&self) -> BackgroundExecutor {
209 self.background_executor.clone()
210 }
211
212 fn foreground_executor(&self) -> ForegroundExecutor {
213 self.foreground_executor.clone()
214 }
215
216 fn text_system(&self) -> Arc<dyn PlatformTextSystem> {
217 self.text_system.clone()
218 }
219
220 fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
221 Box::new(TestKeyboardLayout)
222 }
223
224 fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
225 Rc::new(DummyKeyboardMapper)
226 }
227
228 fn on_keyboard_layout_change(&self, _: Box<dyn FnMut()>) {}
229
230 fn on_thermal_state_change(&self, _: Box<dyn FnMut()>) {}
231
232 fn thermal_state(&self) -> ThermalState {
233 ThermalState::Nominal
234 }
235
236 fn run(&self, _on_finish_launching: Box<dyn FnOnce()>) {
237 unimplemented!()
238 }
239
240 fn quit(&self) {}
241
242 fn restart(&self, path: Option<PathBuf>) {
243 if let Some(tx) = self.expect_restart.take() {
244 tx.send(path).unwrap();
245 }
246 }
247
248 fn activate(&self, _ignoring_other_apps: bool) {
249 //
250 }
251
252 fn hide(&self) {
253 unimplemented!()
254 }
255
256 fn hide_other_apps(&self) {
257 unimplemented!()
258 }
259
260 fn unhide_other_apps(&self) {
261 unimplemented!()
262 }
263
264 fn displays(&self) -> Vec<std::rc::Rc<dyn crate::PlatformDisplay>> {
265 vec![self.active_display.clone()]
266 }
267
268 fn primary_display(&self) -> Option<std::rc::Rc<dyn crate::PlatformDisplay>> {
269 Some(self.active_display.clone())
270 }
271
272 fn is_screen_capture_supported(&self) -> bool {
273 true
274 }
275
276 fn screen_capture_sources(
277 &self,
278 ) -> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
279 let (mut tx, rx) = oneshot::channel();
280 tx.send(Ok(self
281 .screen_capture_sources
282 .borrow()
283 .iter()
284 .map(|source| Rc::new(source.clone()) as Rc<dyn ScreenCaptureSource>)
285 .collect()))
286 .ok();
287 rx
288 }
289
290 fn active_window(&self) -> Option<crate::AnyWindowHandle> {
291 self.active_window
292 .borrow()
293 .as_ref()
294 .map(|window| window.0.lock().handle)
295 }
296
297 fn open_window(
298 &self,
299 handle: AnyWindowHandle,
300 params: WindowParams,
301 ) -> anyhow::Result<Box<dyn crate::PlatformWindow>> {
302 let window = TestWindow::new(
303 handle,
304 params,
305 self.weak.clone(),
306 self.active_display.clone(),
307 );
308 Ok(Box::new(window))
309 }
310
311 fn window_appearance(&self) -> WindowAppearance {
312 WindowAppearance::Light
313 }
314
315 fn open_url(&self, url: &str) {
316 *self.opened_url.borrow_mut() = Some(url.to_string())
317 }
318
319 fn on_open_urls(&self, _callback: Box<dyn FnMut(Vec<String>)>) {
320 unimplemented!()
321 }
322
323 fn prompt_for_paths(
324 &self,
325 _options: crate::PathPromptOptions,
326 ) -> oneshot::Receiver<Result<Option<Vec<std::path::PathBuf>>>> {
327 unimplemented!()
328 }
329
330 fn prompt_for_new_path(
331 &self,
332 directory: &std::path::Path,
333 _suggested_name: Option<&str>,
334 ) -> oneshot::Receiver<Result<Option<std::path::PathBuf>>> {
335 let (tx, rx) = oneshot::channel();
336 self.prompts
337 .borrow_mut()
338 .new_path
339 .push_back((directory.to_path_buf(), tx));
340 rx
341 }
342
343 fn can_select_mixed_files_and_dirs(&self) -> bool {
344 true
345 }
346
347 fn reveal_path(&self, _path: &std::path::Path) {
348 unimplemented!()
349 }
350
351 fn on_quit(&self, _callback: Box<dyn FnMut()>) {}
352
353 fn on_reopen(&self, _callback: Box<dyn FnMut()>) {
354 unimplemented!()
355 }
356
357 fn set_menus(&self, _menus: Vec<crate::Menu>, _keymap: &Keymap) {}
358 fn set_dock_menu(&self, _menu: Vec<crate::MenuItem>, _keymap: &Keymap) {}
359
360 fn add_recent_document(&self, _paths: &Path) {}
361
362 fn on_app_menu_action(&self, _callback: Box<dyn FnMut(&dyn crate::Action)>) {}
363
364 fn on_will_open_app_menu(&self, _callback: Box<dyn FnMut()>) {}
365
366 fn on_validate_app_menu_command(&self, _callback: Box<dyn FnMut(&dyn crate::Action) -> bool>) {}
367
368 fn app_path(&self) -> Result<std::path::PathBuf> {
369 unimplemented!()
370 }
371
372 fn path_for_auxiliary_executable(&self, _name: &str) -> Result<std::path::PathBuf> {
373 unimplemented!()
374 }
375
376 fn set_cursor_style(&self, style: crate::CursorStyle) {
377 *self.active_cursor.lock() = style;
378 }
379
380 fn should_auto_hide_scrollbars(&self) -> bool {
381 false
382 }
383
384 fn read_from_clipboard(&self) -> Option<ClipboardItem> {
385 self.current_clipboard_item.lock().clone()
386 }
387
388 fn write_to_clipboard(&self, item: ClipboardItem) {
389 *self.current_clipboard_item.lock() = Some(item);
390 }
391
392 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
393 fn read_from_primary(&self) -> Option<ClipboardItem> {
394 self.current_primary_item.lock().clone()
395 }
396
397 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
398 fn write_to_primary(&self, item: ClipboardItem) {
399 *self.current_primary_item.lock() = Some(item);
400 }
401
402 #[cfg(target_os = "macos")]
403 fn read_from_find_pasteboard(&self) -> Option<ClipboardItem> {
404 self.current_find_pasteboard_item.lock().clone()
405 }
406
407 #[cfg(target_os = "macos")]
408 fn write_to_find_pasteboard(&self, item: ClipboardItem) {
409 *self.current_find_pasteboard_item.lock() = Some(item);
410 }
411
412 fn write_credentials(&self, _url: &str, _username: &str, _password: &[u8]) -> Task<Result<()>> {
413 Task::ready(Ok(()))
414 }
415
416 fn read_credentials(&self, _url: &str) -> Task<Result<Option<(String, Vec<u8>)>>> {
417 Task::ready(Ok(None))
418 }
419
420 fn delete_credentials(&self, _url: &str) -> Task<Result<()>> {
421 Task::ready(Ok(()))
422 }
423
424 fn register_url_scheme(&self, _: &str) -> Task<anyhow::Result<()>> {
425 unimplemented!()
426 }
427
428 fn open_with_system(&self, _path: &Path) {
429 unimplemented!()
430 }
431}
432
433impl TestScreenCaptureSource {
434 /// Create a fake screen capture source, for testing.
435 pub fn new() -> Self {
436 Self {}
437 }
438}
439
440struct TestKeyboardLayout;
441
442impl PlatformKeyboardLayout for TestKeyboardLayout {
443 fn id(&self) -> &str {
444 "zed.keyboard.example"
445 }
446
447 fn name(&self) -> &str {
448 "zed.keyboard.example"
449 }
450}