Detailed changes
@@ -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
@@ -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
);
}
@@ -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(