Register actions statically / globally (#3264)
This updates our approach to action registration to make it
static/global.
There are 3 different approaches to creating an action, depending on the
complexity of your action's implementation. All of them involve defining
a data type with the correct trait implementations and registering it,
each a bit more powerful / verbose.
* Define a simple list of unit structs that implement `Action` -
`actions!(Foo, Bar, Baz)`
* Make a more complex data type into an action with `#[action]`. This
derives all the necessary traits and registers the action.
```rs
#[action]
struct MoveLeft {
word: true
}
```
* Implement all traits yourself and just register the action with
`#[register_action]`.
Release Notes:
N/A
This commit is contained in:
@@ -80,7 +80,6 @@ pub fn init(client: &Arc<Client>, cx: &mut AppContext) {
|
||||
init_settings(cx);
|
||||
|
||||
let client = Arc::downgrade(client);
|
||||
cx.register_action_type::<SignIn>();
|
||||
cx.on_action({
|
||||
let client = client.clone();
|
||||
move |_: &SignIn, cx| {
|
||||
@@ -93,7 +92,6 @@ pub fn init(client: &Arc<Client>, cx: &mut AppContext) {
|
||||
}
|
||||
});
|
||||
|
||||
cx.register_action_type::<SignOut>();
|
||||
cx.on_action({
|
||||
let client = client.clone();
|
||||
move |_: &SignOut, cx| {
|
||||
@@ -106,7 +104,6 @@ pub fn init(client: &Arc<Client>, cx: &mut AppContext) {
|
||||
}
|
||||
});
|
||||
|
||||
cx.register_action_type::<Reconnect>();
|
||||
cx.on_action({
|
||||
let client = client.clone();
|
||||
move |_: &Reconnect, cx| {
|
||||
|
||||
@@ -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::<MoveUp>();
|
||||
// cx.register_action_type(Editor::move_page_up);
|
||||
cx.register_action_type::<MoveDown>();
|
||||
// cx.register_action_type::<MoveDown>();
|
||||
// cx.register_action_type(Editor::move_page_down);
|
||||
// cx.register_action_type(Editor::next_screen);
|
||||
cx.register_action_type::<MoveLeft>();
|
||||
cx.register_action_type::<MoveRight>();
|
||||
// cx.register_action_type::<MoveLeft>();
|
||||
// cx.register_action_type::<MoveRight>();
|
||||
// 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);
|
||||
|
||||
@@ -4,7 +4,6 @@ use workspace::ModalRegistry;
|
||||
actions!(Toggle);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.register_action_type::<Toggle>();
|
||||
cx.global_mut::<ModalRegistry>()
|
||||
.register_modal(Toggle, |_, cx| {
|
||||
// if let Some(editor) = workspace
|
||||
|
||||
+105
-29
@@ -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<A> 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<serde_json::Value>) -> anyhow::Result<Box<dyn Action>>;
|
||||
|
||||
lazy_static! {
|
||||
static ref ACTION_REGISTRY: RwLock<ActionRegistry> = RwLock::default();
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ActionRegistry {
|
||||
builders_by_name: HashMap<SharedString, ActionBuilder>,
|
||||
all_names: Vec<SharedString>, // So we can return a static slice.
|
||||
}
|
||||
|
||||
/// Register an action type to allow it to be referenced in keymaps.
|
||||
pub fn register_action<A: 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<serde_json::Value>) -> Result<Box<dyn Action>> {
|
||||
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<SharedString>,
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
+3
-28
@@ -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<serde_json::Value>) -> anyhow::Result<Box<dyn Action>>;
|
||||
pub(crate) type FrameCallback = Box<dyn FnOnce(&mut AppContext)>;
|
||||
type Handler = Box<dyn FnMut(&mut AppContext) -> bool + 'static>;
|
||||
type Listener = Box<dyn FnMut(&dyn Any, &mut AppContext) -> bool + 'static>;
|
||||
@@ -176,7 +175,6 @@ pub struct AppContext {
|
||||
pub(crate) keymap: Arc<Mutex<Keymap>>,
|
||||
pub(crate) global_action_listeners:
|
||||
HashMap<TypeId, Vec<Box<dyn Fn(&dyn Action, DispatchPhase, &mut Self)>>>,
|
||||
action_builders: HashMap<SharedString, ActionBuilder>,
|
||||
pending_effects: VecDeque<Effect>,
|
||||
pub(crate) pending_notifications: HashSet<EntityId>,
|
||||
pub(crate) pending_global_notifications: HashSet<TypeId>,
|
||||
@@ -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<Item = SharedString> + 'a {
|
||||
self.action_builders.keys().cloned()
|
||||
}
|
||||
|
||||
/// Move the global of the given type to the stack.
|
||||
pub(crate) fn lease_global<G: 'static>(&mut self) -> GlobalLease<G> {
|
||||
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<A: Action>(&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<serde_json::Value>,
|
||||
) -> Result<Box<dyn Action>> {
|
||||
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
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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::<Vec<_>>();
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// Input:
|
||||
//
|
||||
// struct FooBar {}
|
||||
|
||||
// Output:
|
||||
//
|
||||
// struct FooBar {}
|
||||
//
|
||||
// #[allow(non_snake_case)]
|
||||
// #[gpui2::ctor]
|
||||
// fn register_foobar_builder() {
|
||||
// gpui2::register_action_builder::<Foo>()
|
||||
// }
|
||||
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)
|
||||
}
|
||||
@@ -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:?}")))
|
||||
|
||||
@@ -15,8 +15,6 @@ impl FocusStory {
|
||||
KeyBinding::new("cmd-a", ActionB, Some("child-1")),
|
||||
KeyBinding::new("cmd-c", ActionC, None),
|
||||
]);
|
||||
cx.register_action_type::<ActionA>();
|
||||
cx.register_action_type::<ActionB>();
|
||||
|
||||
cx.build_view(move |cx| Self {})
|
||||
}
|
||||
|
||||
@@ -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::<Vec<_>>();
|
||||
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::<SettingsStore>().json_schema(
|
||||
|
||||
Reference in New Issue
Block a user