diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index 435a644669..a3594c0818 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -354,129 +354,117 @@ impl std::fmt::Debug for Command { } } -// #[cfg(test)] -// mod tests { -// use std::sync::Arc; +#[cfg(test)] +mod tests { + use std::sync::Arc; -// use super::*; -// use editor::Editor; -// use gpui::{executor::Deterministic, TestAppContext}; -// use project::Project; -// use workspace::{AppState, Workspace}; + use super::*; + use editor::Editor; + use gpui::TestAppContext; + use project::Project; + use workspace::{AppState, Workspace}; -// #[test] -// fn test_humanize_action_name() { -// assert_eq!( -// humanize_action_name("editor::GoToDefinition"), -// "editor: go to definition" -// ); -// assert_eq!( -// humanize_action_name("editor::Backspace"), -// "editor: backspace" -// ); -// assert_eq!( -// humanize_action_name("go_to_line::Deploy"), -// "go to line: deploy" -// ); -// } + #[test] + fn test_humanize_action_name() { + assert_eq!( + humanize_action_name("editor::GoToDefinition"), + "editor: go to definition" + ); + assert_eq!( + humanize_action_name("editor::Backspace"), + "editor: backspace" + ); + assert_eq!( + humanize_action_name("go_to_line::Deploy"), + "go to line: deploy" + ); + } -// #[gpui::test] -// async fn test_command_palette(deterministic: Arc, cx: &mut TestAppContext) { -// let app_state = init_test(cx); + #[gpui::test] + async fn test_command_palette(cx: &mut TestAppContext) { + let app_state = init_test(cx); -// let project = Project::test(app_state.fs.clone(), [], cx).await; -// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); -// let workspace = window.root(cx); -// let editor = window.add_view(cx, |cx| { -// let mut editor = Editor::single_line(None, cx); -// editor.set_text("abc", cx); -// editor -// }); + let project = Project::test(app_state.fs.clone(), [], cx).await; + let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut cx; -// workspace.update(cx, |workspace, cx| { -// cx.focus(&editor); -// workspace.add_item(Box::new(editor.clone()), cx) -// }); + let editor = cx.build_view(|cx| { + let mut editor = Editor::single_line(cx); + editor.set_text("abc", cx); + editor + }); -// workspace.update(cx, |workspace, cx| { -// toggle_command_palette(workspace, &Toggle, cx); -// }); + workspace.update(cx, |workspace, cx| { + workspace.add_item(Box::new(editor.clone()), cx); + editor.update(cx, |editor, cx| editor.focus(cx)) + }); -// let palette = workspace.read_with(cx, |workspace, _| { -// workspace.modal::().unwrap() -// }); + cx.simulate_keystrokes("cmd-shift-p"); -// palette -// .update(cx, |palette, cx| { -// // Fill up palette's command list by running an empty query; -// // we only need it to subsequently assert that the palette is initially -// // sorted by command's name. -// palette.delegate_mut().update_matches("".to_string(), cx) -// }) -// .await; + let palette = workspace.update(cx, |workspace, cx| { + workspace + .current_modal::(cx) + .unwrap() + .read(cx) + .picker + .clone() + }); -// palette.update(cx, |palette, _| { -// let is_sorted = -// |actions: &[Command]| actions.windows(2).all(|pair| pair[0].name <= pair[1].name); -// assert!(is_sorted(&palette.delegate().actions)); -// }); + palette.update(cx, |palette, _| { + assert!(palette.delegate.commands.len() > 5); + let is_sorted = + |actions: &[Command]| actions.windows(2).all(|pair| pair[0].name <= pair[1].name); + assert!(is_sorted(&palette.delegate.commands)); + }); -// palette -// .update(cx, |palette, cx| { -// palette -// .delegate_mut() -// .update_matches("bcksp".to_string(), cx) -// }) -// .await; + cx.simulate_keystrokes("b c k s p"); -// palette.update(cx, |palette, cx| { -// assert_eq!(palette.delegate().matches[0].string, "editor: backspace"); -// palette.confirm(&Default::default(), cx); -// }); -// deterministic.run_until_parked(); -// editor.read_with(cx, |editor, cx| { -// assert_eq!(editor.text(cx), "ab"); -// }); + palette.update(cx, |palette, _| { + assert_eq!(palette.delegate.matches[0].string, "editor: backspace"); + }); -// // Add namespace filter, and redeploy the palette -// cx.update(|cx| { -// cx.update_default_global::(|filter, _| { -// filter.filtered_namespaces.insert("editor"); -// }) -// }); + cx.simulate_keystrokes("enter"); -// workspace.update(cx, |workspace, cx| { -// toggle_command_palette(workspace, &Toggle, cx); -// }); + workspace.update(cx, |workspace, cx| { + assert!(workspace.current_modal::(cx).is_none()); + assert_eq!(editor.read(cx).text(cx), "ab") + }); -// // Assert editor command not present -// let palette = workspace.read_with(cx, |workspace, _| { -// workspace.modal::().unwrap() -// }); + // Add namespace filter, and redeploy the palette + cx.update(|cx| { + cx.set_global(CommandPaletteFilter::default()); + cx.update_global::(|filter, _| { + filter.filtered_namespaces.insert("editor"); + }) + }); -// palette -// .update(cx, |palette, cx| { -// palette -// .delegate_mut() -// .update_matches("bcksp".to_string(), cx) -// }) -// .await; + cx.simulate_keystrokes("cmd-shift-p"); + cx.simulate_keystrokes("b c k s p"); -// palette.update(cx, |palette, _| { -// assert!(palette.delegate().matches.is_empty()) -// }); -// } + let palette = workspace.update(cx, |workspace, cx| { + workspace + .current_modal::(cx) + .unwrap() + .read(cx) + .picker + .clone() + }); + palette.update(cx, |palette, _| { + assert!(palette.delegate.matches.is_empty()) + }); + } -// fn init_test(cx: &mut TestAppContext) -> Arc { -// cx.update(|cx| { -// let app_state = AppState::test(cx); -// theme::init(cx); -// language::init(cx); -// editor::init(cx); -// workspace::init(app_state.clone(), cx); -// init(cx); -// Project::init_settings(cx); -// app_state -// }) -// } -// } + fn init_test(cx: &mut TestAppContext) -> Arc { + cx.update(|cx| { + let app_state = AppState::test(cx); + theme::init(cx); + language::init(cx); + editor::init(cx); + workspace::init(app_state.clone(), cx); + init(cx); + Project::init_settings(cx); + settings::load_default_keymap(cx); + app_state + }) + } +} diff --git a/crates/gpui2/src/action.rs b/crates/gpui2/src/action.rs index 16487cf18a..dbb510b1c8 100644 --- a/crates/gpui2/src/action.rs +++ b/crates/gpui2/src/action.rs @@ -54,6 +54,9 @@ pub trait Action: std::fmt::Debug + 'static { where Self: Sized; fn build(value: Option) -> Result> + where + Self: Sized; + fn is_registered() -> bool where Self: Sized; @@ -88,6 +91,14 @@ where Ok(Box::new(action)) } + fn is_registered() -> bool { + ACTION_REGISTRY + .read() + .names_by_type_id + .get(&TypeId::of::()) + .is_some() + } + fn partial_eq(&self, action: &dyn Action) -> bool { action .as_any() diff --git a/crates/gpui2/src/app/test_context.rs b/crates/gpui2/src/app/test_context.rs index cc59b7a16a..5397a2214d 100644 --- a/crates/gpui2/src/app/test_context.rs +++ b/crates/gpui2/src/app/test_context.rs @@ -1,8 +1,8 @@ use crate::{ div, Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext, BackgroundExecutor, Context, Div, EventEmitter, ForegroundExecutor, InputEvent, KeyDownEvent, - Keystroke, Model, ModelContext, Render, Result, Task, TestDispatcher, TestPlatform, View, - ViewContext, VisualContext, WindowContext, WindowHandle, WindowOptions, + Keystroke, Model, ModelContext, Render, Result, Task, TestDispatcher, TestPlatform, TestWindow, + View, ViewContext, VisualContext, WindowContext, WindowHandle, WindowOptions, }; use anyhow::{anyhow, bail}; use futures::{Stream, StreamExt}; @@ -220,7 +220,21 @@ impl TestAppContext { { window .update(self, |_, cx| cx.dispatch_action(action.boxed_clone())) - .unwrap() + .unwrap(); + + self.background_executor.run_until_parked() + } + + pub fn simulate_keystrokes(&mut self, window: AnyWindowHandle, keystrokes: &str) { + for keystroke in keystrokes + .split(" ") + .map(Keystroke::parse) + .map(Result::unwrap) + { + self.dispatch_keystroke(window, keystroke.into(), false); + } + + self.background_executor.run_until_parked() } pub fn dispatch_keystroke( @@ -229,15 +243,41 @@ impl TestAppContext { keystroke: Keystroke, is_held: bool, ) { + let keystroke2 = keystroke.clone(); let handled = window .update(self, |_, cx| { cx.dispatch_event(InputEvent::KeyDown(KeyDownEvent { keystroke, is_held })) }) .is_ok_and(|handled| handled); - - if !handled { - // todo!() simluate input here + if handled { + return; } + + let input_handler = self.update_test_window(window, |window| window.input_handler.clone()); + let Some(input_handler) = input_handler else { + panic!( + "dispatch_keystroke {:?} failed to dispatch action or input", + &keystroke2 + ); + }; + let text = keystroke2.ime_key.unwrap_or(keystroke2.key); + input_handler.lock().replace_text_in_range(None, &text); + } + + pub fn update_test_window( + &mut self, + window: AnyWindowHandle, + f: impl FnOnce(&mut TestWindow) -> R, + ) -> R { + window + .update(self, |_, cx| { + f(cx.window + .platform_window + .as_any_mut() + .downcast_mut::() + .unwrap()) + }) + .unwrap() } pub fn notifications(&mut self, entity: &Model) -> impl Stream { @@ -401,12 +441,20 @@ impl<'a> VisualTestContext<'a> { Self { cx, window } } + pub fn run_until_parked(&self) { + self.cx.background_executor.run_until_parked(); + } + pub fn dispatch_action(&mut self, action: A) where A: Action, { self.cx.dispatch_action(self.window, action) } + + pub fn simulate_keystrokes(&mut self, keystrokes: &str) { + self.cx.simulate_keystrokes(self.window, keystrokes) + } } impl<'a> Context for VisualTestContext<'a> { diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index a08d1619ae..e76d82f36f 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -229,6 +229,20 @@ pub trait InteractiveComponent: Sized + Element { mut self, listener: impl Fn(&mut V, &A, &mut ViewContext) + 'static, ) -> Self { + // NOTE: this debug assert has the side-effect of working around + // a bug where a crate consisting only of action definitions does + // not register the actions in debug builds: + // + // https://github.com/rust-lang/rust/issues/47384 + // https://github.com/mmastrac/rust-ctor/issues/280 + // + // if we are relying on this side-effect still, removing the debug_assert! + // likely breaks the command_palette tests. + debug_assert!( + A::is_registered(), + "{:?} is not registered as an action", + A::qualified_name() + ); self.interactivity().action_listeners.push(( TypeId::of::(), Box::new(move |view, action, phase, cx| { diff --git a/crates/gpui2/src/platform/test/window.rs b/crates/gpui2/src/platform/test/window.rs index adb15c4266..e355c3aa4b 100644 --- a/crates/gpui2/src/platform/test/window.rs +++ b/crates/gpui2/src/platform/test/window.rs @@ -22,7 +22,7 @@ pub struct TestWindow { bounds: WindowBounds, current_scene: Mutex>, display: Rc, - input_handler: Option>, + pub(crate) input_handler: Option>>>, handlers: Mutex, platform: Weak, sprite_atlas: Arc, @@ -80,11 +80,11 @@ impl PlatformWindow for TestWindow { } fn as_any_mut(&mut self) -> &mut dyn std::any::Any { - todo!() + self } fn set_input_handler(&mut self, input_handler: Box) { - self.input_handler = Some(input_handler); + self.input_handler = Some(Arc::new(Mutex::new(input_handler))); } fn prompt( diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index c177ffc8c2..808f903bd0 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -189,7 +189,7 @@ impl Drop for FocusHandle { pub struct Window { pub(crate) handle: AnyWindowHandle, pub(crate) removed: bool, - platform_window: Box, + pub(crate) platform_window: Box, display_id: DisplayId, sprite_atlas: Arc, rem_size: Pixels,