diff --git a/crates/client2/src/client2.rs b/crates/client2/src/client2.rs index 4a1c5f321c375a097a89252830bd17c572d0c648..93ec7f329bfab51f11d3689904cbc4d5edb62071 100644 --- a/crates/client2/src/client2.rs +++ b/crates/client2/src/client2.rs @@ -80,7 +80,6 @@ pub fn init(client: &Arc, cx: &mut AppContext) { init_settings(cx); let client = Arc::downgrade(client); - cx.register_action_type::(); cx.on_action({ let client = client.clone(); move |_: &SignIn, cx| { @@ -93,7 +92,6 @@ pub fn init(client: &Arc, cx: &mut AppContext) { } }); - cx.register_action_type::(); cx.on_action({ let client = client.clone(); move |_: &SignOut, cx| { @@ -106,7 +104,6 @@ pub fn init(client: &Arc, cx: &mut AppContext) { } }); - cx.register_action_type::(); cx.on_action({ let client = client.clone(); move |_: &Reconnect, cx| { diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 29742f365120a63df882e8f1bc77dba67264e1df..a37442be57edb41c7afcfef08d6c60b3a4c1f3c7 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -448,13 +448,12 @@ pub fn init(cx: &mut AppContext) { // cx.register_action_type(Editor::paste); // cx.register_action_type(Editor::undo); // cx.register_action_type(Editor::redo); - cx.register_action_type::(); // cx.register_action_type(Editor::move_page_up); - cx.register_action_type::(); + // cx.register_action_type::(); // cx.register_action_type(Editor::move_page_down); // cx.register_action_type(Editor::next_screen); - cx.register_action_type::(); - cx.register_action_type::(); + // cx.register_action_type::(); + // cx.register_action_type::(); // cx.register_action_type(Editor::move_to_previous_word_start); // cx.register_action_type(Editor::move_to_previous_subword_start); // cx.register_action_type(Editor::move_to_next_word_end); diff --git a/crates/go_to_line2/src/go_to_line.rs b/crates/go_to_line2/src/go_to_line.rs index 13d283ecff245abb11bafd75cabca94ac79e80dc..b8b91b16468c0e97e25946c3e24be5a9e062dd21 100644 --- a/crates/go_to_line2/src/go_to_line.rs +++ b/crates/go_to_line2/src/go_to_line.rs @@ -4,7 +4,6 @@ use workspace::ModalRegistry; actions!(Toggle); pub fn init(cx: &mut AppContext) { - cx.register_action_type::(); cx.global_mut::() .register_modal(Toggle, |_, cx| { // if let Some(editor) = workspace diff --git a/crates/gpui2/src/action.rs b/crates/gpui2/src/action.rs index 90f312f502ca60d011c7c40c1dac60a1a5427bef..4d89ba1826e03a75a71b2fbd27018b511d784a02 100644 --- a/crates/gpui2/src/action.rs +++ b/crates/gpui2/src/action.rs @@ -1,9 +1,54 @@ use crate::SharedString; use anyhow::{anyhow, Context, Result}; use collections::{HashMap, HashSet}; +use lazy_static::lazy_static; +use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard}; use serde::Deserialize; use std::any::{type_name, Any}; +/// Actions are used to implement keyboard-driven UI. +/// When you declare an action, you can bind keys to the action in the keymap and +/// listeners for that action in the element tree. +/// +/// To declare a list of simple actions, you can use the actions! macro, which defines a simple unit struct +/// action for each listed action name. +/// ```rust +/// actions!(MoveUp, MoveDown, MoveLeft, MoveRight, Newline); +/// ``` +/// More complex data types can also be actions. If you annotate your type with the `#[action]` proc macro, +/// it will automatically +/// ``` +/// #[action] +/// pub struct SelectNext { +/// pub replace_newest: bool, +/// } +/// +/// Any type A that satisfies the following bounds is automatically an action: +/// +/// ``` +/// A: for<'a> Deserialize<'a> + PartialEq + Clone + Default + std::fmt::Debug + 'static, +/// ``` +/// +/// The `#[action]` annotation will derive these implementations for your struct automatically. If you +/// want to control them manually, you can use the lower-level `#[register_action]` macro, which only +/// generates the code needed to register your action before `main`. Then you'll need to implement all +/// the traits manually. +/// +/// ``` +/// #[gpui::register_action] +/// #[derive(gpui::serde::Deserialize, std::cmp::PartialEq, std::clone::Clone, std::fmt::Debug)] +/// pub struct Paste { +/// pub content: SharedString, +/// } +/// +/// impl std::default::Default for Paste { +/// fn default() -> Self { +/// Self { +/// content: SharedString::from("🍝"), +/// } +/// } +/// } +/// ``` pub trait Action: std::fmt::Debug + 'static { fn qualified_name() -> SharedString where @@ -17,32 +62,7 @@ pub trait Action: std::fmt::Debug + 'static { fn as_any(&self) -> &dyn Any; } -// actions defines structs that can be used as actions. -#[macro_export] -macro_rules! actions { - () => {}; - - ( $name:ident ) => { - #[derive(::std::clone::Clone, ::std::default::Default, ::std::fmt::Debug, ::std::cmp::PartialEq, $crate::serde::Deserialize)] - pub struct $name; - }; - - ( $name:ident { $($token:tt)* } ) => { - #[derive(::std::clone::Clone, ::std::default::Default, ::std::fmt::Debug, ::std::cmp::PartialEq, $crate::serde::Deserialize)] - pub struct $name { $($token)* } - }; - - ( $name:ident, $($rest:tt)* ) => { - actions!($name); - actions!($($rest)*); - }; - - ( $name:ident { $($token:tt)* }, $($rest:tt)* ) => { - actions!($name { $($token)* }); - actions!($($rest)*); - }; -} - +// Types become actions by satisfying a list of trait bounds. impl Action for A where A: for<'a> Deserialize<'a> + PartialEq + Clone + Default + std::fmt::Debug + 'static, @@ -80,6 +100,61 @@ where } } +type ActionBuilder = fn(json: Option) -> anyhow::Result>; + +lazy_static! { + static ref ACTION_REGISTRY: RwLock = RwLock::default(); +} + +#[derive(Default)] +struct ActionRegistry { + builders_by_name: HashMap, + all_names: Vec, // So we can return a static slice. +} + +/// Register an action type to allow it to be referenced in keymaps. +pub fn register_action() { + let name = A::qualified_name(); + let mut lock = ACTION_REGISTRY.write(); + lock.builders_by_name.insert(name.clone(), A::build); + lock.all_names.push(name); +} + +/// Construct an action based on its name and optional JSON parameters sourced from the keymap. +pub fn build_action(name: &str, params: Option) -> Result> { + let lock = ACTION_REGISTRY.read(); + let build_action = lock + .builders_by_name + .get(name) + .ok_or_else(|| anyhow!("no action type registered for {}", name))?; + (build_action)(params) +} + +pub fn all_action_names() -> MappedRwLockReadGuard<'static, [SharedString]> { + let lock = ACTION_REGISTRY.read(); + RwLockReadGuard::map(lock, |registry: &ActionRegistry| { + registry.all_names.as_slice() + }) +} + +/// Defines unit structs that can be used as actions. +/// To use more complex data types as actions, annotate your type with the #[action] macro. +#[macro_export] +macro_rules! actions { + () => {}; + + ( $name:ident ) => { + #[gpui::register_action] + #[derive(::std::clone::Clone, ::std::default::Default, ::std::fmt::Debug, ::std::cmp::PartialEq, $crate::serde::Deserialize)] + pub struct $name; + }; + + ( $name:ident, $($rest:tt)* ) => { + actions!($name); + actions!($($rest)*); + }; +} + #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct DispatchContext { set: HashSet, @@ -317,22 +392,23 @@ fn skip_whitespace(source: &str) -> &str { #[cfg(test)] mod tests { use super::*; + use crate as gpui; use DispatchContextPredicate::*; #[test] fn test_actions_definition() { { - actions!(A, B { field: i32 }, C, D, E, F {}, G); + actions!(A, B, C, D, E, F, G); } { actions!( A, - B { field: i32 }, + B, C, D, E, - F {}, + F, G, // Don't wrap, test the trailing comma ); } diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index 42d34e0c202e9ceaddbabeb85e6f6fac23502d39..f2ac3a91cfacd1d9713266cc2f3f5aca31694cc8 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -17,9 +17,9 @@ use crate::{ current_platform, image_cache::ImageCache, Action, AnyBox, AnyView, AnyWindowHandle, AppMetadata, AssetSource, BackgroundExecutor, ClipboardItem, Context, DispatchPhase, DisplayId, Entity, FocusEvent, FocusHandle, FocusId, ForegroundExecutor, KeyBinding, Keymap, LayoutId, - PathPromptOptions, Pixels, Platform, PlatformDisplay, Point, Render, SharedString, - SubscriberSet, Subscription, SvgRenderer, Task, TextStyle, TextStyleRefinement, TextSystem, - View, Window, WindowContext, WindowHandle, WindowId, + PathPromptOptions, Pixels, Platform, PlatformDisplay, Point, Render, SubscriberSet, + Subscription, SvgRenderer, Task, TextStyle, TextStyleRefinement, TextSystem, View, Window, + WindowContext, WindowHandle, WindowId, }; use anyhow::{anyhow, Result}; use collections::{HashMap, HashSet, VecDeque}; @@ -140,7 +140,6 @@ impl App { } } -type ActionBuilder = fn(json: Option) -> anyhow::Result>; pub(crate) type FrameCallback = Box; type Handler = Box bool + 'static>; type Listener = Box bool + 'static>; @@ -176,7 +175,6 @@ pub struct AppContext { pub(crate) keymap: Arc>, pub(crate) global_action_listeners: HashMap>>, - action_builders: HashMap, pending_effects: VecDeque, pub(crate) pending_notifications: HashSet, pub(crate) pending_global_notifications: HashSet, @@ -234,7 +232,6 @@ impl AppContext { windows: SlotMap::with_key(), keymap: Arc::new(Mutex::new(Keymap::default())), global_action_listeners: HashMap::default(), - action_builders: HashMap::default(), pending_effects: VecDeque::new(), pending_notifications: HashSet::default(), pending_global_notifications: HashSet::default(), @@ -695,10 +692,6 @@ impl AppContext { ) } - pub fn all_action_names<'a>(&'a self) -> impl Iterator + 'a { - self.action_builders.keys().cloned() - } - /// Move the global of the given type to the stack. pub(crate) fn lease_global(&mut self) -> GlobalLease { GlobalLease::new( @@ -761,24 +754,6 @@ impl AppContext { })); } - /// Register an action type to allow it to be referenced in keymaps. - pub fn register_action_type(&mut self) { - self.action_builders.insert(A::qualified_name(), A::build); - } - - /// Construct an action based on its name and parameters. - pub fn build_action( - &mut self, - name: &str, - params: Option, - ) -> Result> { - let build = self - .action_builders - .get(name) - .ok_or_else(|| anyhow!("no action type registered for {}", name))?; - (build)(params) - } - /// Event handlers propagate events by default. Call this method to stop dispatching to /// event handlers with a lower z-index (mouse) or higher in the tree (keyboard). This is /// the opposite of [propagate]. It's also possible to cancel a call to [propagate] by diff --git a/crates/gpui2/src/gpui2.rs b/crates/gpui2/src/gpui2.rs index e59b196a91ff252da9a41179912b1e7e718ba0c1..8f3dc6c314c0542eb47fc7213fcf766399d33a3f 100644 --- a/crates/gpui2/src/gpui2.rs +++ b/crates/gpui2/src/gpui2.rs @@ -37,6 +37,7 @@ pub use anyhow::Result; pub use app::*; pub use assets::*; pub use color::*; +pub use ctor::ctor; pub use element::*; pub use elements::*; pub use executor::*; diff --git a/crates/gpui2_macros/src/action.rs b/crates/gpui2_macros/src/action.rs new file mode 100644 index 0000000000000000000000000000000000000000..e4f54e58d5c601561925be05e962626dd5fd5d64 --- /dev/null +++ b/crates/gpui2_macros/src/action.rs @@ -0,0 +1,54 @@ +// Input: +// +// #[action] +// struct Foo { +// bar: String, +// } + +// Output: +// +// #[gpui::register_action] +// #[derive(gpui::serde::Deserialize, std::cmp::PartialEq, std::clone::Clone, std::default::Default, std::fmt::Debug)] +// struct Foo { +// bar: String, +// } + +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, DeriveInput}; + +pub fn action(_attr: TokenStream, item: TokenStream) -> TokenStream { + let input = parse_macro_input!(item as DeriveInput); + let name = &input.ident; + let attrs = input + .attrs + .into_iter() + .filter(|attr| !attr.path.is_ident("action")) + .collect::>(); + + let attributes = quote! { + #[gpui::register_action] + #[derive(gpui::serde::Deserialize, std::cmp::PartialEq, std::clone::Clone, std::default::Default, std::fmt::Debug)] + #(#attrs)* + }; + + let output = match input.data { + syn::Data::Struct(ref struct_data) => { + let fields = &struct_data.fields; + quote! { + #attributes + struct #name { #fields } + } + } + syn::Data::Enum(ref enum_data) => { + let variants = &enum_data.variants; + quote! { + #attributes + enum #name { #variants } + } + } + _ => panic!("Expected a struct or an enum."), + }; + + TokenStream::from(output) +} diff --git a/crates/gpui2_macros/src/gpui2_macros.rs b/crates/gpui2_macros/src/gpui2_macros.rs index 2e0c0547f79e29f1e3bcc5bfcc43cfed516f7df9..80b67e1a12393d76d1ecffc5a342dc8eaefacc40 100644 --- a/crates/gpui2_macros/src/gpui2_macros.rs +++ b/crates/gpui2_macros/src/gpui2_macros.rs @@ -1,14 +1,26 @@ -use proc_macro::TokenStream; - +mod action; mod derive_component; +mod register_action; mod style_helpers; mod test; +use proc_macro::TokenStream; + #[proc_macro] pub fn style_helpers(args: TokenStream) -> TokenStream { style_helpers::style_helpers(args) } +#[proc_macro_attribute] +pub fn action(attr: TokenStream, item: TokenStream) -> TokenStream { + action::action(attr, item) +} + +#[proc_macro_attribute] +pub fn register_action(attr: TokenStream, item: TokenStream) -> TokenStream { + register_action::register_action(attr, item) +} + #[proc_macro_derive(Component, attributes(component))] pub fn derive_component(input: TokenStream) -> TokenStream { derive_component::derive_component(input) diff --git a/crates/gpui2_macros/src/register_action.rs b/crates/gpui2_macros/src/register_action.rs new file mode 100644 index 0000000000000000000000000000000000000000..ab00daf4773a620200d2edd8129dd666a233bfde --- /dev/null +++ b/crates/gpui2_macros/src/register_action.rs @@ -0,0 +1,32 @@ +// Input: +// +// struct FooBar {} + +// Output: +// +// struct FooBar {} +// +// #[allow(non_snake_case)] +// #[gpui2::ctor] +// fn register_foobar_builder() { +// gpui2::register_action_builder::() +// } +use proc_macro::TokenStream; +use quote::{format_ident, quote}; +use syn::{parse_macro_input, DeriveInput}; + +pub fn register_action(_attr: TokenStream, item: TokenStream) -> TokenStream { + let input = parse_macro_input!(item as DeriveInput); + let type_name = &input.ident; + let ctor_fn_name = format_ident!("register_{}_builder", type_name); + + let expanded = quote! { + #input + #[allow(non_snake_case)] + #[gpui::ctor] + fn #ctor_fn_name() { + gpui::register_action::<#type_name>() + } + }; + TokenStream::from(expanded) +} diff --git a/crates/settings2/src/keymap_file.rs b/crates/settings2/src/keymap_file.rs index 93635935cbd746b7e6fc3d96294e5895ee5c01e4..9f279864ee739243c9c475395aef6d228582fe0a 100644 --- a/crates/settings2/src/keymap_file.rs +++ b/crates/settings2/src/keymap_file.rs @@ -73,9 +73,9 @@ impl KeymapFile { "Expected first item in array to be a string." ))); }; - cx.build_action(&name, Some(data)) + gpui::build_action(&name, Some(data)) } - Value::String(name) => cx.build_action(&name, None), + Value::String(name) => gpui::build_action(&name, None), Value::Null => Ok(no_action()), _ => { return Some(Err(anyhow!("Expected two-element array, got {action:?}"))) diff --git a/crates/storybook2/src/stories/focus.rs b/crates/storybook2/src/stories/focus.rs index 0f580d0cbf171ef32b2c83465ad8601785304227..57f9869859710bfd07967af6e8170c8048634124 100644 --- a/crates/storybook2/src/stories/focus.rs +++ b/crates/storybook2/src/stories/focus.rs @@ -15,8 +15,6 @@ impl FocusStory { KeyBinding::new("cmd-a", ActionB, Some("child-1")), KeyBinding::new("cmd-c", ActionC, None), ]); - cx.register_action_type::(); - cx.register_action_type::(); cx.build_view(move |cx| Self {}) } diff --git a/crates/zed2/src/languages/json.rs b/crates/zed2/src/languages/json.rs index 63f909ae2a2e264ea672dee48e305ba1be82e066..cf9b33d9683cadd9f5b27c41104d215becd1110d 100644 --- a/crates/zed2/src/languages/json.rs +++ b/crates/zed2/src/languages/json.rs @@ -107,7 +107,7 @@ impl LspAdapter for JsonLspAdapter { &self, cx: &mut AppContext, ) -> BoxFuture<'static, serde_json::Value> { - let action_names = cx.all_action_names().collect::>(); + let action_names = gpui::all_action_names(); let staff_mode = cx.is_staff(); let language_names = &self.languages.language_names(); let settings_schema = cx.global::().json_schema(