logiguard fork v3: full patch set on verified 8c74db0 tree
Includes prior-session patches (carry forward so the app compiles): - crates/gpui/build.rs: cross-compile manifest fix - crates/gpui/src/platform.rs: PlatformWindow::activate_with_token trait method - crates/gpui/src/window.rs: Window::activate_with_token public API - crates/gpui_linux/src/linux/wayland/window.rs: WaylandWindow::activate_with_token + activate() keyboard-serial fix Plus the focus-serial fix: - serial.rs: SerialKind::KeyboardEnter - client.rs: store wl_keyboard.enter serial; latest_serial_of() Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
27
crates/gpui_macros/Cargo.toml
Normal file
27
crates/gpui_macros/Cargo.toml
Normal file
@@ -0,0 +1,27 @@
|
||||
[package]
|
||||
name = "gpui_macros"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish = false
|
||||
license = "Apache-2.0"
|
||||
description = "Macros used by gpui"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[features]
|
||||
inspector = []
|
||||
|
||||
[lib]
|
||||
path = "src/gpui_macros.rs"
|
||||
proc-macro = true
|
||||
doctest = true
|
||||
|
||||
[dependencies]
|
||||
heck.workspace = true
|
||||
proc-macro2.workspace = true
|
||||
quote.workspace = true
|
||||
syn.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { workspace = true, features = ["inspector"] }
|
||||
1
crates/gpui_macros/LICENSE-APACHE
Symbolic link
1
crates/gpui_macros/LICENSE-APACHE
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-APACHE
|
||||
211
crates/gpui_macros/src/derive_action.rs
Normal file
211
crates/gpui_macros/src/derive_action.rs
Normal file
@@ -0,0 +1,211 @@
|
||||
use crate::register_action::generate_register_action;
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro2::Ident;
|
||||
use quote::quote;
|
||||
use syn::{Data, DeriveInput, LitStr, Token, parse::ParseStream};
|
||||
|
||||
pub(crate) fn derive_action(input: TokenStream) -> TokenStream {
|
||||
let input = syn::parse_macro_input!(input as DeriveInput);
|
||||
|
||||
let struct_name = &input.ident;
|
||||
let mut name_argument = None;
|
||||
let mut deprecated_aliases = Vec::new();
|
||||
let mut no_json = false;
|
||||
let mut no_register = false;
|
||||
let mut namespace = None;
|
||||
let mut deprecated = None;
|
||||
let mut doc_str: Option<String> = None;
|
||||
|
||||
/*
|
||||
*
|
||||
* #[action()]
|
||||
* Struct Foo {
|
||||
* bar: bool // is bar considered an attribute
|
||||
}
|
||||
*/
|
||||
for attr in &input.attrs {
|
||||
if attr.path().is_ident("action") {
|
||||
attr.parse_nested_meta(|meta| {
|
||||
if meta.path.is_ident("name") {
|
||||
if name_argument.is_some() {
|
||||
return Err(meta.error("'name' argument specified multiple times"));
|
||||
}
|
||||
meta.input.parse::<Token![=]>()?;
|
||||
let lit: LitStr = meta.input.parse()?;
|
||||
name_argument = Some(lit.value());
|
||||
} else if meta.path.is_ident("namespace") {
|
||||
if namespace.is_some() {
|
||||
return Err(meta.error("'namespace' argument specified multiple times"));
|
||||
}
|
||||
meta.input.parse::<Token![=]>()?;
|
||||
let ident: Ident = meta.input.parse()?;
|
||||
namespace = Some(ident.to_string());
|
||||
} else if meta.path.is_ident("no_json") {
|
||||
if no_json {
|
||||
return Err(meta.error("'no_json' argument specified multiple times"));
|
||||
}
|
||||
no_json = true;
|
||||
} else if meta.path.is_ident("no_register") {
|
||||
if no_register {
|
||||
return Err(meta.error("'no_register' argument specified multiple times"));
|
||||
}
|
||||
no_register = true;
|
||||
} else if meta.path.is_ident("deprecated_aliases") {
|
||||
if !deprecated_aliases.is_empty() {
|
||||
return Err(
|
||||
meta.error("'deprecated_aliases' argument specified multiple times")
|
||||
);
|
||||
}
|
||||
meta.input.parse::<Token![=]>()?;
|
||||
// Parse array of string literals
|
||||
let content;
|
||||
syn::bracketed!(content in meta.input);
|
||||
let aliases = content.parse_terminated(
|
||||
|input: ParseStream| input.parse::<LitStr>(),
|
||||
Token![,],
|
||||
)?;
|
||||
deprecated_aliases.extend(aliases.into_iter().map(|lit| lit.value()));
|
||||
} else if meta.path.is_ident("deprecated") {
|
||||
if deprecated.is_some() {
|
||||
return Err(meta.error("'deprecated' argument specified multiple times"));
|
||||
}
|
||||
meta.input.parse::<Token![=]>()?;
|
||||
let lit: LitStr = meta.input.parse()?;
|
||||
deprecated = Some(lit.value());
|
||||
} else {
|
||||
return Err(meta.error(format!(
|
||||
"'{:?}' argument not recognized, expected \
|
||||
'namespace', 'no_json', 'no_register, 'deprecated_aliases', or 'deprecated'",
|
||||
meta.path
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.unwrap_or_else(|e| panic!("in #[action] attribute: {}", e));
|
||||
} else if attr.path().is_ident("doc") {
|
||||
use syn::{Expr::Lit, ExprLit, Lit::Str, Meta, MetaNameValue};
|
||||
if let Meta::NameValue(MetaNameValue {
|
||||
value:
|
||||
Lit(ExprLit {
|
||||
lit: Str(ref lit_str),
|
||||
..
|
||||
}),
|
||||
..
|
||||
}) = attr.meta
|
||||
{
|
||||
let doc = lit_str.value();
|
||||
let doc_str = doc_str.get_or_insert_default();
|
||||
doc_str.push_str(doc.trim());
|
||||
doc_str.push('\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let name = name_argument.unwrap_or_else(|| struct_name.to_string());
|
||||
|
||||
if name.contains("::") {
|
||||
panic!(
|
||||
"in #[action] attribute: `name = \"{name}\"` must not contain `::`, \
|
||||
also specify `namespace` instead"
|
||||
);
|
||||
}
|
||||
|
||||
let full_name = if let Some(namespace) = namespace {
|
||||
format!("{namespace}::{name}")
|
||||
} else {
|
||||
name
|
||||
};
|
||||
|
||||
let is_unit_struct = matches!(&input.data, Data::Struct(data) if data.fields.is_empty());
|
||||
|
||||
let build_fn_body = if no_json {
|
||||
let error_msg = format!("{} cannot be built from JSON", full_name);
|
||||
quote! { Err(gpui::private::anyhow::anyhow!(#error_msg)) }
|
||||
} else if is_unit_struct {
|
||||
quote! { Ok(Box::new(Self)) }
|
||||
} else {
|
||||
quote! { Ok(Box::new(gpui::private::serde_json::from_value::<Self>(_value)?)) }
|
||||
};
|
||||
|
||||
let json_schema_fn_body = if no_json || is_unit_struct {
|
||||
quote! { None }
|
||||
} else {
|
||||
quote! { Some(<Self as gpui::private::schemars::JsonSchema>::json_schema(_generator)) }
|
||||
};
|
||||
|
||||
let deprecated_aliases_fn_body = if deprecated_aliases.is_empty() {
|
||||
quote! { &[] }
|
||||
} else {
|
||||
let aliases = deprecated_aliases.iter();
|
||||
quote! { &[#(#aliases),*] }
|
||||
};
|
||||
|
||||
let deprecation_fn_body = if let Some(message) = deprecated {
|
||||
quote! { Some(#message) }
|
||||
} else {
|
||||
quote! { None }
|
||||
};
|
||||
|
||||
let documentation_fn_body = if let Some(doc) = doc_str {
|
||||
let doc = doc.trim();
|
||||
quote! { Some(#doc) }
|
||||
} else {
|
||||
quote! { None }
|
||||
};
|
||||
|
||||
let registration = if no_register {
|
||||
quote! {}
|
||||
} else {
|
||||
generate_register_action(struct_name)
|
||||
};
|
||||
|
||||
TokenStream::from(quote! {
|
||||
#registration
|
||||
|
||||
impl gpui::Action for #struct_name {
|
||||
fn name(&self) -> &'static str {
|
||||
#full_name
|
||||
}
|
||||
|
||||
fn name_for_type() -> &'static str
|
||||
where
|
||||
Self: Sized
|
||||
{
|
||||
#full_name
|
||||
}
|
||||
|
||||
fn partial_eq(&self, action: &dyn gpui::Action) -> bool {
|
||||
action
|
||||
.as_any()
|
||||
.downcast_ref::<Self>()
|
||||
.map_or(false, |a| self == a)
|
||||
}
|
||||
|
||||
fn boxed_clone(&self) -> Box<dyn gpui::Action> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
|
||||
fn build(_value: gpui::private::serde_json::Value) -> gpui::Result<Box<dyn gpui::Action>> {
|
||||
#build_fn_body
|
||||
}
|
||||
|
||||
fn action_json_schema(
|
||||
_generator: &mut gpui::private::schemars::SchemaGenerator,
|
||||
) -> Option<gpui::private::schemars::Schema> {
|
||||
#json_schema_fn_body
|
||||
}
|
||||
|
||||
fn deprecated_aliases() -> &'static [&'static str] {
|
||||
#deprecated_aliases_fn_body
|
||||
}
|
||||
|
||||
fn deprecation_message() -> Option<&'static str> {
|
||||
#deprecation_fn_body
|
||||
}
|
||||
|
||||
fn documentation() -> Option<&'static str> {
|
||||
#documentation_fn_body
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
119
crates/gpui_macros/src/derive_app_context.rs
Normal file
119
crates/gpui_macros/src/derive_app_context.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{DeriveInput, parse_macro_input};
|
||||
|
||||
use crate::get_simple_attribute_field;
|
||||
|
||||
pub fn derive_app_context(input: TokenStream) -> TokenStream {
|
||||
let ast = parse_macro_input!(input as DeriveInput);
|
||||
|
||||
let Some(app_variable) = get_simple_attribute_field(&ast, "app") else {
|
||||
return quote! {
|
||||
compile_error!("Derive must have an #[app] attribute to detect the &mut App field");
|
||||
}
|
||||
.into();
|
||||
};
|
||||
|
||||
let type_name = &ast.ident;
|
||||
let (impl_generics, type_generics, where_clause) = ast.generics.split_for_impl();
|
||||
|
||||
let r#gen = quote! {
|
||||
impl #impl_generics gpui::AppContext for #type_name #type_generics
|
||||
#where_clause
|
||||
{
|
||||
fn new<T: 'static>(
|
||||
&mut self,
|
||||
build_entity: impl FnOnce(&mut gpui::Context<'_, T>) -> T,
|
||||
) -> gpui::Entity<T> {
|
||||
self.#app_variable.new(build_entity)
|
||||
}
|
||||
|
||||
fn reserve_entity<T: 'static>(&mut self) -> gpui::Reservation<T> {
|
||||
self.#app_variable.reserve_entity()
|
||||
}
|
||||
|
||||
fn insert_entity<T: 'static>(
|
||||
&mut self,
|
||||
reservation: gpui::Reservation<T>,
|
||||
build_entity: impl FnOnce(&mut gpui::Context<'_, T>) -> T,
|
||||
) -> gpui::Entity<T> {
|
||||
self.#app_variable.insert_entity(reservation, build_entity)
|
||||
}
|
||||
|
||||
fn update_entity<T, R>(
|
||||
&mut self,
|
||||
handle: &gpui::Entity<T>,
|
||||
update: impl FnOnce(&mut T, &mut gpui::Context<'_, T>) -> R,
|
||||
) -> R
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
self.#app_variable.update_entity(handle, update)
|
||||
}
|
||||
|
||||
fn as_mut<'y, 'z, T>(
|
||||
&'y mut self,
|
||||
handle: &'z gpui::Entity<T>,
|
||||
) -> gpui::GpuiBorrow<'y, T>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
self.#app_variable.as_mut(handle)
|
||||
}
|
||||
|
||||
fn read_entity<T, R>(
|
||||
&self,
|
||||
handle: &gpui::Entity<T>,
|
||||
read: impl FnOnce(&T, &gpui::App) -> R,
|
||||
) -> R
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
self.#app_variable.read_entity(handle, read)
|
||||
}
|
||||
|
||||
fn update_window<T, F>(&mut self, window: gpui::AnyWindowHandle, f: F) -> gpui::Result<T>
|
||||
where
|
||||
F: FnOnce(gpui::AnyView, &mut gpui::Window, &mut gpui::App) -> T,
|
||||
{
|
||||
self.#app_variable.update_window(window, f)
|
||||
}
|
||||
|
||||
fn with_window<R>(
|
||||
&mut self,
|
||||
entity_id: gpui::EntityId,
|
||||
f: impl FnOnce(&mut gpui::Window, &mut gpui::App) -> R,
|
||||
) -> Option<R>
|
||||
{
|
||||
self.#app_variable.with_window(entity_id, f)
|
||||
}
|
||||
|
||||
fn read_window<T, R>(
|
||||
&self,
|
||||
window: &gpui::WindowHandle<T>,
|
||||
read: impl FnOnce(gpui::Entity<T>, &gpui::App) -> R,
|
||||
) -> gpui::Result<R>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
self.#app_variable.read_window(window, read)
|
||||
}
|
||||
|
||||
fn background_spawn<R>(&self, future: impl std::future::Future<Output = R> + Send + 'static) -> gpui::Task<R>
|
||||
where
|
||||
R: Send + 'static,
|
||||
{
|
||||
self.#app_variable.background_spawn(future)
|
||||
}
|
||||
|
||||
fn read_global<G, R>(&self, callback: impl FnOnce(&G, &gpui::App) -> R) -> R
|
||||
where
|
||||
G: gpui::Global,
|
||||
{
|
||||
self.#app_variable.read_global(callback)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
r#gen.into()
|
||||
}
|
||||
305
crates/gpui_macros/src/derive_inspector_reflection.rs
Normal file
305
crates/gpui_macros/src/derive_inspector_reflection.rs
Normal file
@@ -0,0 +1,305 @@
|
||||
//! Implements `#[derive_inspector_reflection]` macro to provide runtime access to trait methods
|
||||
//! that have the shape `fn method(self) -> Self`. This code was generated using Zed Agent with Claude Opus 4.
|
||||
|
||||
use heck::ToSnakeCase as _;
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro2::{Span, TokenStream as TokenStream2};
|
||||
use quote::quote;
|
||||
use syn::{
|
||||
Attribute, Expr, FnArg, Ident, Item, ItemTrait, Lit, Meta, Path, ReturnType, TraitItem, Type,
|
||||
parse_macro_input, parse_quote,
|
||||
visit_mut::{self, VisitMut},
|
||||
};
|
||||
|
||||
pub fn derive_inspector_reflection(_args: TokenStream, input: TokenStream) -> TokenStream {
|
||||
let mut item = parse_macro_input!(input as Item);
|
||||
|
||||
// First, expand any macros in the trait
|
||||
match &mut item {
|
||||
Item::Trait(trait_item) => {
|
||||
let mut expander = MacroExpander;
|
||||
expander.visit_item_trait_mut(trait_item);
|
||||
}
|
||||
_ => {
|
||||
return syn::Error::new_spanned(
|
||||
quote!(#item),
|
||||
"#[derive_inspector_reflection] can only be applied to traits",
|
||||
)
|
||||
.to_compile_error()
|
||||
.into();
|
||||
}
|
||||
}
|
||||
|
||||
// Now process the expanded trait
|
||||
match item {
|
||||
Item::Trait(trait_item) => generate_reflected_trait(trait_item),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_reflected_trait(trait_item: ItemTrait) -> TokenStream {
|
||||
let trait_name = &trait_item.ident;
|
||||
let vis = &trait_item.vis;
|
||||
|
||||
// Determine if we're being called from within the gpui crate
|
||||
let call_site = Span::call_site();
|
||||
let inspector_reflection_path = if is_called_from_gpui_crate(call_site) {
|
||||
quote! { crate::inspector_reflection }
|
||||
} else {
|
||||
quote! { ::gpui::inspector_reflection }
|
||||
};
|
||||
|
||||
// Collect method information for methods of form fn name(self) -> Self or fn name(mut self) -> Self
|
||||
let mut method_infos = Vec::new();
|
||||
|
||||
for item in &trait_item.items {
|
||||
if let TraitItem::Fn(method) = item {
|
||||
let method_name = &method.sig.ident;
|
||||
|
||||
// Check if method has self or mut self receiver
|
||||
let has_valid_self_receiver = method
|
||||
.sig
|
||||
.inputs
|
||||
.iter()
|
||||
.any(|arg| matches!(arg, FnArg::Receiver(r) if r.reference.is_none()));
|
||||
|
||||
// Check if method returns Self
|
||||
let returns_self = match &method.sig.output {
|
||||
ReturnType::Type(_, ty) => {
|
||||
matches!(**ty, Type::Path(ref path) if path.path.is_ident("Self"))
|
||||
}
|
||||
ReturnType::Default => false,
|
||||
};
|
||||
|
||||
// Check if method has exactly one parameter (self or mut self)
|
||||
let param_count = method.sig.inputs.len();
|
||||
|
||||
// Include methods of form fn name(self) -> Self or fn name(mut self) -> Self
|
||||
// This includes methods with default implementations
|
||||
if has_valid_self_receiver && returns_self && param_count == 1 {
|
||||
// Extract documentation and cfg attributes
|
||||
let doc = extract_doc_comment(&method.attrs);
|
||||
let cfg_attrs = extract_cfg_attributes(&method.attrs);
|
||||
method_infos.push((method_name.clone(), doc, cfg_attrs));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate the reflection module name
|
||||
let reflection_mod_name = Ident::new(
|
||||
&format!("{}_reflection", trait_name.to_string().to_snake_case()),
|
||||
trait_name.span(),
|
||||
);
|
||||
|
||||
// Generate wrapper functions for each method
|
||||
// These wrappers use type erasure to allow runtime invocation
|
||||
let wrapper_functions = method_infos.iter().map(|(method_name, _doc, cfg_attrs)| {
|
||||
let wrapper_name = Ident::new(
|
||||
&format!("__wrapper_{}", method_name),
|
||||
method_name.span(),
|
||||
);
|
||||
quote! {
|
||||
#(#cfg_attrs)*
|
||||
fn #wrapper_name<T: #trait_name + 'static>(value: Box<dyn std::any::Any>) -> Box<dyn std::any::Any> {
|
||||
if let Ok(concrete) = value.downcast::<T>() {
|
||||
Box::new(concrete.#method_name())
|
||||
} else {
|
||||
panic!("Type mismatch in reflection wrapper");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Generate method info entries
|
||||
let method_info_entries = method_infos.iter().map(|(method_name, doc, cfg_attrs)| {
|
||||
let method_name_str = method_name.to_string();
|
||||
let wrapper_name = Ident::new(&format!("__wrapper_{}", method_name), method_name.span());
|
||||
let doc_expr = match doc {
|
||||
Some(doc_str) => quote! { Some(#doc_str) },
|
||||
None => quote! { None },
|
||||
};
|
||||
quote! {
|
||||
#(#cfg_attrs)*
|
||||
#inspector_reflection_path::FunctionReflection {
|
||||
name: #method_name_str,
|
||||
function: #wrapper_name::<T>,
|
||||
documentation: #doc_expr,
|
||||
_type: ::std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Generate the complete output
|
||||
let output = quote! {
|
||||
#trait_item
|
||||
|
||||
/// Implements function reflection
|
||||
#vis mod #reflection_mod_name {
|
||||
use super::*;
|
||||
|
||||
#(#wrapper_functions)*
|
||||
|
||||
/// Get all reflectable methods for a concrete type implementing the trait
|
||||
pub fn methods<T: #trait_name + 'static>() -> Vec<#inspector_reflection_path::FunctionReflection<T>> {
|
||||
vec![
|
||||
#(#method_info_entries),*
|
||||
]
|
||||
}
|
||||
|
||||
/// Find a method by name for a concrete type implementing the trait
|
||||
pub fn find_method<T: #trait_name + 'static>(name: &str) -> Option<#inspector_reflection_path::FunctionReflection<T>> {
|
||||
methods::<T>().into_iter().find(|m| m.name == name)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
TokenStream::from(output)
|
||||
}
|
||||
|
||||
fn extract_doc_comment(attrs: &[Attribute]) -> Option<String> {
|
||||
let mut doc_lines = Vec::new();
|
||||
|
||||
for attr in attrs {
|
||||
if attr.path().is_ident("doc")
|
||||
&& let Meta::NameValue(meta) = &attr.meta
|
||||
&& let Expr::Lit(expr_lit) = &meta.value
|
||||
&& let Lit::Str(lit_str) = &expr_lit.lit
|
||||
{
|
||||
let line = lit_str.value();
|
||||
let line = line.strip_prefix(' ').unwrap_or(&line);
|
||||
doc_lines.push(line.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if doc_lines.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(doc_lines.join("\n"))
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_cfg_attributes(attrs: &[Attribute]) -> Vec<Attribute> {
|
||||
attrs
|
||||
.iter()
|
||||
.filter(|attr| attr.path().is_ident("cfg"))
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn is_called_from_gpui_crate(_span: Span) -> bool {
|
||||
// Check if we're being called from within the gpui crate by examining the call site
|
||||
// This is a heuristic approach - we check if the current crate name is "gpui"
|
||||
std::env::var("CARGO_PKG_NAME").is_ok_and(|name| name == "gpui")
|
||||
}
|
||||
|
||||
struct MacroExpander;
|
||||
|
||||
impl VisitMut for MacroExpander {
|
||||
fn visit_item_trait_mut(&mut self, trait_item: &mut ItemTrait) {
|
||||
let mut expanded_items = Vec::new();
|
||||
let mut items_to_keep = Vec::new();
|
||||
|
||||
for item in trait_item.items.drain(..) {
|
||||
match item {
|
||||
TraitItem::Macro(macro_item) => {
|
||||
// Try to expand known macros
|
||||
if let Some(expanded) = try_expand_macro(¯o_item) {
|
||||
expanded_items.extend(expanded);
|
||||
} else {
|
||||
// Keep unknown macros as-is
|
||||
items_to_keep.push(TraitItem::Macro(macro_item));
|
||||
}
|
||||
}
|
||||
other => {
|
||||
items_to_keep.push(other);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rebuild the items list with expanded content first, then original items
|
||||
trait_item.items = expanded_items;
|
||||
trait_item.items.extend(items_to_keep);
|
||||
|
||||
// Continue visiting
|
||||
visit_mut::visit_item_trait_mut(self, trait_item);
|
||||
}
|
||||
}
|
||||
|
||||
fn try_expand_macro(macro_item: &syn::TraitItemMacro) -> Option<Vec<TraitItem>> {
|
||||
let path = ¯o_item.mac.path;
|
||||
|
||||
// Check if this is one of our known style macros
|
||||
let macro_name = path_to_string(path);
|
||||
|
||||
// Handle the known macros by calling their implementations
|
||||
match macro_name.as_str() {
|
||||
"gpui_macros::style_helpers" | "style_helpers" => {
|
||||
let tokens = macro_item.mac.tokens.clone();
|
||||
let expanded = crate::styles::style_helpers(TokenStream::from(tokens));
|
||||
parse_expanded_items(expanded)
|
||||
}
|
||||
"gpui_macros::visibility_style_methods" | "visibility_style_methods" => {
|
||||
let tokens = macro_item.mac.tokens.clone();
|
||||
let expanded = crate::styles::visibility_style_methods(TokenStream::from(tokens));
|
||||
parse_expanded_items(expanded)
|
||||
}
|
||||
"gpui_macros::margin_style_methods" | "margin_style_methods" => {
|
||||
let tokens = macro_item.mac.tokens.clone();
|
||||
let expanded = crate::styles::margin_style_methods(TokenStream::from(tokens));
|
||||
parse_expanded_items(expanded)
|
||||
}
|
||||
"gpui_macros::padding_style_methods" | "padding_style_methods" => {
|
||||
let tokens = macro_item.mac.tokens.clone();
|
||||
let expanded = crate::styles::padding_style_methods(TokenStream::from(tokens));
|
||||
parse_expanded_items(expanded)
|
||||
}
|
||||
"gpui_macros::position_style_methods" | "position_style_methods" => {
|
||||
let tokens = macro_item.mac.tokens.clone();
|
||||
let expanded = crate::styles::position_style_methods(TokenStream::from(tokens));
|
||||
parse_expanded_items(expanded)
|
||||
}
|
||||
"gpui_macros::overflow_style_methods" | "overflow_style_methods" => {
|
||||
let tokens = macro_item.mac.tokens.clone();
|
||||
let expanded = crate::styles::overflow_style_methods(TokenStream::from(tokens));
|
||||
parse_expanded_items(expanded)
|
||||
}
|
||||
"gpui_macros::cursor_style_methods" | "cursor_style_methods" => {
|
||||
let tokens = macro_item.mac.tokens.clone();
|
||||
let expanded = crate::styles::cursor_style_methods(TokenStream::from(tokens));
|
||||
parse_expanded_items(expanded)
|
||||
}
|
||||
"gpui_macros::border_style_methods" | "border_style_methods" => {
|
||||
let tokens = macro_item.mac.tokens.clone();
|
||||
let expanded = crate::styles::border_style_methods(TokenStream::from(tokens));
|
||||
parse_expanded_items(expanded)
|
||||
}
|
||||
"gpui_macros::box_shadow_style_methods" | "box_shadow_style_methods" => {
|
||||
let tokens = macro_item.mac.tokens.clone();
|
||||
let expanded = crate::styles::box_shadow_style_methods(TokenStream::from(tokens));
|
||||
parse_expanded_items(expanded)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn path_to_string(path: &Path) -> String {
|
||||
path.segments
|
||||
.iter()
|
||||
.map(|seg| seg.ident.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("::")
|
||||
}
|
||||
|
||||
fn parse_expanded_items(expanded: TokenStream) -> Option<Vec<TraitItem>> {
|
||||
let tokens = TokenStream2::from(expanded);
|
||||
|
||||
// Try to parse the expanded tokens as trait items
|
||||
// We need to wrap them in a dummy trait to parse properly
|
||||
let dummy_trait: ItemTrait = parse_quote! {
|
||||
trait Dummy {
|
||||
#tokens
|
||||
}
|
||||
};
|
||||
|
||||
Some(dummy_trait.items)
|
||||
}
|
||||
24
crates/gpui_macros/src/derive_into_element.rs
Normal file
24
crates/gpui_macros/src/derive_into_element.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{DeriveInput, parse_macro_input};
|
||||
|
||||
pub fn derive_into_element(input: TokenStream) -> TokenStream {
|
||||
let ast = parse_macro_input!(input as DeriveInput);
|
||||
let type_name = &ast.ident;
|
||||
let (impl_generics, type_generics, where_clause) = ast.generics.split_for_impl();
|
||||
|
||||
let r#gen = quote! {
|
||||
impl #impl_generics gpui::IntoElement for #type_name #type_generics
|
||||
#where_clause
|
||||
{
|
||||
type Element = gpui::Component<Self>;
|
||||
|
||||
#[track_caller]
|
||||
fn into_element(self) -> Self::Element {
|
||||
gpui::Component::new(self)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
r#gen.into()
|
||||
}
|
||||
21
crates/gpui_macros/src/derive_render.rs
Normal file
21
crates/gpui_macros/src/derive_render.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{DeriveInput, parse_macro_input};
|
||||
|
||||
pub fn derive_render(input: TokenStream) -> TokenStream {
|
||||
let ast = parse_macro_input!(input as DeriveInput);
|
||||
let type_name = &ast.ident;
|
||||
let (impl_generics, type_generics, where_clause) = ast.generics.split_for_impl();
|
||||
|
||||
let r#gen = quote! {
|
||||
impl #impl_generics gpui::Render for #type_name #type_generics
|
||||
#where_clause
|
||||
{
|
||||
fn render(&mut self, _window: &mut gpui::Window, _cx: &mut gpui::Context<Self>) -> impl gpui::Element {
|
||||
gpui::Empty
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
r#gen.into()
|
||||
}
|
||||
73
crates/gpui_macros/src/derive_visual_context.rs
Normal file
73
crates/gpui_macros/src/derive_visual_context.rs
Normal file
@@ -0,0 +1,73 @@
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{DeriveInput, parse_macro_input};
|
||||
|
||||
use super::get_simple_attribute_field;
|
||||
|
||||
pub fn derive_visual_context(input: TokenStream) -> TokenStream {
|
||||
let ast = parse_macro_input!(input as DeriveInput);
|
||||
|
||||
let Some(window_variable) = get_simple_attribute_field(&ast, "window") else {
|
||||
return quote! {
|
||||
compile_error!("Derive must have a #[window] attribute to detect the &mut Window field");
|
||||
}
|
||||
.into();
|
||||
};
|
||||
|
||||
let Some(app_variable) = get_simple_attribute_field(&ast, "app") else {
|
||||
return quote! {
|
||||
compile_error!("Derive must have a #[app] attribute to detect the &mut App field");
|
||||
}
|
||||
.into();
|
||||
};
|
||||
|
||||
let type_name = &ast.ident;
|
||||
let (impl_generics, type_generics, where_clause) = ast.generics.split_for_impl();
|
||||
|
||||
let r#gen = quote! {
|
||||
impl #impl_generics gpui::VisualContext for #type_name #type_generics
|
||||
#where_clause
|
||||
{
|
||||
type Result<T> = T;
|
||||
|
||||
fn window_handle(&self) -> gpui::AnyWindowHandle {
|
||||
self.#window_variable.window_handle()
|
||||
}
|
||||
|
||||
fn update_window_entity<T: 'static, R>(
|
||||
&mut self,
|
||||
entity: &gpui::Entity<T>,
|
||||
update: impl FnOnce(&mut T, &mut gpui::Window, &mut gpui::Context<T>) -> R,
|
||||
) -> R {
|
||||
gpui::AppContext::update_entity(self.#app_variable, entity, |entity, cx| update(entity, self.#window_variable, cx))
|
||||
}
|
||||
|
||||
fn new_window_entity<T: 'static>(
|
||||
&mut self,
|
||||
build_entity: impl FnOnce(&mut gpui::Window, &mut gpui::Context<'_, T>) -> T,
|
||||
) -> gpui::Entity<T> {
|
||||
gpui::AppContext::new(self.#app_variable, |cx| build_entity(self.#window_variable, cx))
|
||||
}
|
||||
|
||||
fn replace_root_view<V>(
|
||||
&mut self,
|
||||
build_view: impl FnOnce(&mut gpui::Window, &mut gpui::Context<V>) -> V,
|
||||
) -> gpui::Entity<V>
|
||||
where
|
||||
V: 'static + gpui::Render,
|
||||
{
|
||||
self.#window_variable.replace_root(self.#app_variable, build_view)
|
||||
}
|
||||
|
||||
fn focus<V>(&mut self, entity: &gpui::Entity<V>)
|
||||
where
|
||||
V: gpui::Focusable,
|
||||
{
|
||||
let focus_handle = gpui::Focusable::focus_handle(entity, self.#app_variable);
|
||||
self.#window_variable.focus(&focus_handle, self.#app_variable);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
r#gen.into()
|
||||
}
|
||||
297
crates/gpui_macros/src/gpui_macros.rs
Normal file
297
crates/gpui_macros/src/gpui_macros.rs
Normal file
@@ -0,0 +1,297 @@
|
||||
mod derive_action;
|
||||
mod derive_app_context;
|
||||
mod derive_into_element;
|
||||
mod derive_render;
|
||||
mod derive_visual_context;
|
||||
mod property_test;
|
||||
mod register_action;
|
||||
mod styles;
|
||||
mod test;
|
||||
|
||||
#[cfg(any(feature = "inspector", debug_assertions))]
|
||||
mod derive_inspector_reflection;
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use syn::{DeriveInput, Ident};
|
||||
|
||||
/// `Action` derive macro - see the trait documentation for details.
|
||||
#[proc_macro_derive(Action, attributes(action))]
|
||||
pub fn derive_action(input: TokenStream) -> TokenStream {
|
||||
derive_action::derive_action(input)
|
||||
}
|
||||
|
||||
/// This can be used to register an action with the GPUI runtime when you want to manually implement
|
||||
/// the `Action` trait. Typically you should use the `Action` derive macro or `actions!` macro
|
||||
/// instead.
|
||||
#[proc_macro]
|
||||
pub fn register_action(ident: TokenStream) -> TokenStream {
|
||||
register_action::register_action(ident)
|
||||
}
|
||||
|
||||
/// #[derive(IntoElement)] is used to create a Component out of anything that implements
|
||||
/// the `RenderOnce` trait.
|
||||
#[proc_macro_derive(IntoElement)]
|
||||
pub fn derive_into_element(input: TokenStream) -> TokenStream {
|
||||
derive_into_element::derive_into_element(input)
|
||||
}
|
||||
|
||||
#[proc_macro_derive(Render)]
|
||||
#[doc(hidden)]
|
||||
pub fn derive_render(input: TokenStream) -> TokenStream {
|
||||
derive_render::derive_render(input)
|
||||
}
|
||||
|
||||
/// #[derive(AppContext)] is used to create a context out of anything that holds a `&mut App`
|
||||
/// Note that a `#[app]` attribute is required to identify the variable holding the &mut App.
|
||||
///
|
||||
/// Failure to add the attribute causes a compile error:
|
||||
///
|
||||
/// ```compile_fail
|
||||
/// # #[macro_use] extern crate gpui_macros;
|
||||
/// # #[macro_use] extern crate gpui;
|
||||
/// #[derive(AppContext)]
|
||||
/// struct MyContext<'a> {
|
||||
/// app: &'a mut gpui::App
|
||||
/// }
|
||||
/// ```
|
||||
#[proc_macro_derive(AppContext, attributes(app))]
|
||||
pub fn derive_app_context(input: TokenStream) -> TokenStream {
|
||||
derive_app_context::derive_app_context(input)
|
||||
}
|
||||
|
||||
/// #[derive(VisualContext)] is used to create a visual context out of anything that holds a `&mut Window` and
|
||||
/// implements `AppContext`
|
||||
/// Note that a `#[app]` and a `#[window]` attribute are required to identify the variables holding the &mut App,
|
||||
/// and &mut Window respectively.
|
||||
///
|
||||
/// Failure to add both attributes causes a compile error:
|
||||
///
|
||||
/// ```compile_fail
|
||||
/// # #[macro_use] extern crate gpui_macros;
|
||||
/// # #[macro_use] extern crate gpui;
|
||||
/// #[derive(VisualContext)]
|
||||
/// struct MyContext<'a, 'b> {
|
||||
/// #[app]
|
||||
/// app: &'a mut gpui::App,
|
||||
/// window: &'b mut gpui::Window
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ```compile_fail
|
||||
/// # #[macro_use] extern crate gpui_macros;
|
||||
/// # #[macro_use] extern crate gpui;
|
||||
/// #[derive(VisualContext)]
|
||||
/// struct MyContext<'a, 'b> {
|
||||
/// app: &'a mut gpui::App,
|
||||
/// #[window]
|
||||
/// window: &'b mut gpui::Window
|
||||
/// }
|
||||
/// ```
|
||||
#[proc_macro_derive(VisualContext, attributes(window, app))]
|
||||
pub fn derive_visual_context(input: TokenStream) -> TokenStream {
|
||||
derive_visual_context::derive_visual_context(input)
|
||||
}
|
||||
|
||||
/// Used by GPUI to generate the style helpers.
|
||||
#[proc_macro]
|
||||
#[doc(hidden)]
|
||||
pub fn style_helpers(input: TokenStream) -> TokenStream {
|
||||
styles::style_helpers(input)
|
||||
}
|
||||
|
||||
/// Generates methods for visibility styles.
|
||||
#[proc_macro]
|
||||
pub fn visibility_style_methods(input: TokenStream) -> TokenStream {
|
||||
styles::visibility_style_methods(input)
|
||||
}
|
||||
|
||||
/// Generates methods for margin styles.
|
||||
#[proc_macro]
|
||||
pub fn margin_style_methods(input: TokenStream) -> TokenStream {
|
||||
styles::margin_style_methods(input)
|
||||
}
|
||||
|
||||
/// Generates methods for padding styles.
|
||||
#[proc_macro]
|
||||
pub fn padding_style_methods(input: TokenStream) -> TokenStream {
|
||||
styles::padding_style_methods(input)
|
||||
}
|
||||
|
||||
/// Generates methods for position styles.
|
||||
#[proc_macro]
|
||||
pub fn position_style_methods(input: TokenStream) -> TokenStream {
|
||||
styles::position_style_methods(input)
|
||||
}
|
||||
|
||||
/// Generates methods for overflow styles.
|
||||
#[proc_macro]
|
||||
pub fn overflow_style_methods(input: TokenStream) -> TokenStream {
|
||||
styles::overflow_style_methods(input)
|
||||
}
|
||||
|
||||
/// Generates methods for cursor styles.
|
||||
#[proc_macro]
|
||||
pub fn cursor_style_methods(input: TokenStream) -> TokenStream {
|
||||
styles::cursor_style_methods(input)
|
||||
}
|
||||
|
||||
/// Generates methods for border styles.
|
||||
#[proc_macro]
|
||||
pub fn border_style_methods(input: TokenStream) -> TokenStream {
|
||||
styles::border_style_methods(input)
|
||||
}
|
||||
|
||||
/// Generates methods for box shadow styles.
|
||||
#[proc_macro]
|
||||
pub fn box_shadow_style_methods(input: TokenStream) -> TokenStream {
|
||||
styles::box_shadow_style_methods(input)
|
||||
}
|
||||
|
||||
/// `#[gpui::test]` can be used to annotate test functions that run with GPUI support.
|
||||
///
|
||||
/// It supports both synchronous and asynchronous tests, and can provide you with
|
||||
/// as many `TestAppContext` instances as you need.
|
||||
/// The output contains a `#[test]` annotation so this can be used with any existing
|
||||
/// test harness (`cargo test` or `cargo-nextest`).
|
||||
///
|
||||
/// ```
|
||||
/// #[gpui::test]
|
||||
/// async fn test_foo(mut cx: &TestAppContext) { }
|
||||
/// ```
|
||||
///
|
||||
/// In addition to passing a TestAppContext, you can also ask for a `StdRnd` instance.
|
||||
/// this will be seeded with the `SEED` environment variable and is used internally by
|
||||
/// the ForegroundExecutor and BackgroundExecutor to run tasks deterministically in tests.
|
||||
/// Using the same `StdRng` for behavior in your test will allow you to exercise a wide
|
||||
/// variety of scenarios and interleavings just by changing the seed.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `#[gpui::test]` with no arguments runs once with the seed `0` or `SEED` env var if set.
|
||||
/// - `#[gpui::test(seed = 10)]` runs once with the seed `10`.
|
||||
/// - `#[gpui::test(seeds(10, 20, 30))]` runs three times with seeds `10`, `20`, and `30`.
|
||||
/// - `#[gpui::test(iterations = 5)]` runs five times, providing as seed the values in the range `0..5`.
|
||||
/// - `#[gpui::test(retries = 3)]` runs up to four times if it fails to try and make it pass.
|
||||
/// - `#[gpui::test(on_failure = "crate::test::report_failure")]` will call the specified function after the
|
||||
/// tests fail so that you can write out more detail about the failure.
|
||||
///
|
||||
/// You can combine `iterations = ...` with `seeds(...)`:
|
||||
/// - `#[gpui::test(iterations = 5, seed = 10)]` is equivalent to `#[gpui::test(seeds(0, 1, 2, 3, 4, 10))]`.
|
||||
/// - `#[gpui::test(iterations = 5, seeds(10, 20, 30)]` is equivalent to `#[gpui::test(seeds(0, 1, 2, 3, 4, 10, 20, 30))]`.
|
||||
/// - `#[gpui::test(seeds(10, 20, 30), iterations = 5]` is equivalent to `#[gpui::test(seeds(0, 1, 2, 3, 4, 10, 20, 30))]`.
|
||||
///
|
||||
/// # Environment Variables
|
||||
///
|
||||
/// - `SEED`: sets a seed for the first run
|
||||
/// - `ITERATIONS`: forces the value of the `iterations` argument
|
||||
#[proc_macro_attribute]
|
||||
pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
|
||||
test::test(args, function)
|
||||
}
|
||||
|
||||
/// A variant of `#[gpui::test]` that supports property-based testing.
|
||||
///
|
||||
/// A property test, much like a standard GPUI randomized test, allows testing
|
||||
/// claims of the form "for any possible X, Y should hold". For example:
|
||||
/// ```
|
||||
/// #[gpui::property_test]
|
||||
/// fn test_arithmetic(x: i32, y: i32) {
|
||||
/// assert!(x == y || x < y || x > y);
|
||||
/// }
|
||||
/// ```
|
||||
/// Standard GPUI randomized tests provide you with an instance of `StdRng` to
|
||||
/// generate random data in a controlled manner. Property-based tests have some
|
||||
/// advantages, however:
|
||||
/// - Shrinking - the harness also understands a notion of the "complexity" of a
|
||||
/// particular value. This allows it to find the "simplest possible value that
|
||||
/// causes the test to fail".
|
||||
/// - Ergonomics/clarity - the property-testing harness will automatically
|
||||
/// generate values, removing the need to fill the test body with generation
|
||||
/// logic.
|
||||
/// - Failure persistence - if a failing seed is identified, it is stored in a
|
||||
/// file, which can be checked in, and future runs will check these cases before
|
||||
/// future cases.
|
||||
///
|
||||
/// Property tests work best when all inputs can be generated up-front and kept
|
||||
/// in a simple data structure. Sometimes, this isn't possible - for example, if
|
||||
/// a test needs to make a random decision based on the current state of some
|
||||
/// structure. In this case, a standard GPUI randomized test may be more
|
||||
/// suitable.
|
||||
///
|
||||
/// ## Customizing random values
|
||||
///
|
||||
/// This macro is based on the [`#[proptest::property_test]`] macro, but handles
|
||||
/// some of the same GPUI-specific arguments as `#[gpui::test]`. Specifically,
|
||||
/// `&{mut,} TestAppContext` and `BackgroundExecutor` work as normal. `StdRng`
|
||||
/// arguments are **explicitly forbidden**, since they break shrinking, and are
|
||||
/// a common footgun.
|
||||
///
|
||||
/// All other arguments are forwarded to the underlying proptest macro.
|
||||
///
|
||||
/// Note: much of the following is copied from the proptest docs, specifically the
|
||||
/// [`#[proptest::property_test]`] macro docs.
|
||||
///
|
||||
/// Random values of type `T` are generated by a `Strategy<Value = T>` object.
|
||||
/// Some types have a canonical `Strategy` - these types also implement
|
||||
/// `Arbitrary`. Parameters to a `#[gpui::property_test]`, by default, use a
|
||||
/// type's `Arbitrary` implementation. If you'd like to provide a custom
|
||||
/// strategy, you can use `#[strategy = ...]` on the argument:
|
||||
/// ```
|
||||
/// #[gpui::property_test]
|
||||
/// fn int_test(#[strategy = 1..10] x: i32, #[strategy = "[a-zA-Z0-9]{20}"] s: String) {
|
||||
/// assert!(s.len() > (x as usize));
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// For more information on writing custom `Strategy` and `Arbitrary`
|
||||
/// implementations, see [the proptest book][book], and the [`Strategy`] trait.
|
||||
///
|
||||
/// ## Scheduler
|
||||
///
|
||||
/// Similar to `#[gpui::test]`, this macro will choose random seeds for the test
|
||||
/// scheduler. It uses `.no_shrink()` to tell proptest that all seeds are
|
||||
/// roughly equivalent in terms of "complexity". If `$SEED` is set, it will
|
||||
/// affect **ONLY** the seed passed to the scheduler. To control other values,
|
||||
/// use custom `Strategy`s.
|
||||
///
|
||||
/// [`#[proptest::property_test]`]: https://docs.rs/proptest/latest/proptest/attr.property_test.html
|
||||
/// [book]: https://proptest-rs.github.io/proptest/intro.html
|
||||
/// [`Strategy`]: https://docs.rs/proptest/latest/proptest/strategy/trait.Strategy.html
|
||||
#[proc_macro_attribute]
|
||||
pub fn property_test(args: TokenStream, function: TokenStream) -> TokenStream {
|
||||
property_test::test(args.into(), function.into()).into()
|
||||
}
|
||||
|
||||
/// When added to a trait, `#[derive_inspector_reflection]` generates a module which provides
|
||||
/// enumeration and lookup by name of all methods that have the shape `fn method(self) -> Self`.
|
||||
/// This is used by the inspector so that it can use the builder methods in `Styled` and
|
||||
/// `StyledExt`.
|
||||
///
|
||||
/// The generated module will have the name `<snake_case_trait_name>_reflection` and contain the
|
||||
/// following functions:
|
||||
///
|
||||
/// ```ignore
|
||||
/// pub fn methods::<T: TheTrait + 'static>() -> Vec<gpui::inspector_reflection::FunctionReflection<T>>;
|
||||
///
|
||||
/// pub fn find_method::<T: TheTrait + 'static>() -> Option<gpui::inspector_reflection::FunctionReflection<T>>;
|
||||
/// ```
|
||||
///
|
||||
/// The `invoke` method on `FunctionReflection` will run the method. `FunctionReflection` also
|
||||
/// provides the method's documentation.
|
||||
#[cfg(any(feature = "inspector", debug_assertions))]
|
||||
#[proc_macro_attribute]
|
||||
pub fn derive_inspector_reflection(_args: TokenStream, input: TokenStream) -> TokenStream {
|
||||
derive_inspector_reflection::derive_inspector_reflection(_args, input)
|
||||
}
|
||||
|
||||
pub(crate) fn get_simple_attribute_field(ast: &DeriveInput, name: &'static str) -> Option<Ident> {
|
||||
match &ast.data {
|
||||
syn::Data::Struct(data_struct) => data_struct
|
||||
.fields
|
||||
.iter()
|
||||
.find(|field| field.attrs.iter().any(|attr| attr.path().is_ident(name)))
|
||||
.map(|field| field.ident.clone().unwrap()),
|
||||
syn::Data::Enum(_) => None,
|
||||
syn::Data::Union(_) => None,
|
||||
}
|
||||
}
|
||||
273
crates/gpui_macros/src/property_test.rs
Normal file
273
crates/gpui_macros/src/property_test.rs
Normal file
@@ -0,0 +1,273 @@
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{ToTokens, format_ident, quote, quote_spanned};
|
||||
use syn::{
|
||||
Expr, FnArg, Ident, ItemFn, MetaNameValue, Token, Type,
|
||||
parse::{Parse, ParseStream},
|
||||
parse2,
|
||||
punctuated::Punctuated,
|
||||
spanned::Spanned,
|
||||
token::Comma,
|
||||
};
|
||||
|
||||
pub fn test(args: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let item_span = item.span();
|
||||
let Ok(func) = parse2::<ItemFn>(item) else {
|
||||
return quote_spanned! { item_span =>
|
||||
compile_error!("#[gpui::property_test] must be placed on a function");
|
||||
};
|
||||
};
|
||||
|
||||
let args = match parse2::<Args>(args) {
|
||||
Ok(args) => args,
|
||||
Err(e) => return e.to_compile_error(),
|
||||
};
|
||||
|
||||
let test_name = func.sig.ident.clone();
|
||||
let test_ret_ty = func.sig.output.clone();
|
||||
let inner_fn_name = format_ident!("__{test_name}");
|
||||
let outer_fn_attributes = &func.attrs;
|
||||
|
||||
let parsed_args = parse_args(func.sig.inputs, &test_name);
|
||||
|
||||
let inner_body = func.block;
|
||||
let inner_arg_decls = parsed_args.inner_fn_decl_args;
|
||||
let asyncness = func.sig.asyncness;
|
||||
|
||||
let inner_fn = quote! {
|
||||
let #inner_fn_name = #asyncness move |#inner_arg_decls| #inner_body;
|
||||
};
|
||||
|
||||
let arg_errors = parsed_args.errors;
|
||||
let proptest_args = parsed_args.proptest_args;
|
||||
let inner_args = parsed_args.inner_fn_args;
|
||||
let cx_vars = parsed_args.cx_vars;
|
||||
let cx_teardowns = parsed_args.cx_teardowns;
|
||||
|
||||
let proptest_args = quote! {
|
||||
#[strategy = ::gpui::seed_strategy()] __seed: u64,
|
||||
#proptest_args
|
||||
};
|
||||
|
||||
let run_test_body = match &asyncness {
|
||||
None => quote! {
|
||||
#cx_vars
|
||||
let result = #inner_fn_name(#inner_args);
|
||||
#cx_teardowns
|
||||
result
|
||||
},
|
||||
Some(_) => quote! {
|
||||
let foreground_executor = gpui::ForegroundExecutor::new(std::sync::Arc::new(dispatcher.clone()));
|
||||
#cx_vars
|
||||
let result = foreground_executor.block_test(#inner_fn_name(#inner_args));
|
||||
#cx_teardowns
|
||||
result
|
||||
},
|
||||
};
|
||||
|
||||
let fixed_macro_invocation = args.render();
|
||||
|
||||
quote! {
|
||||
#arg_errors
|
||||
|
||||
#fixed_macro_invocation
|
||||
#(#outer_fn_attributes)*
|
||||
fn #test_name(#proptest_args) #test_ret_ty {
|
||||
#inner_fn
|
||||
|
||||
::gpui::run_test_once(
|
||||
__seed,
|
||||
Box::new(move |dispatcher| #test_ret_ty {
|
||||
#run_test_body
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Args {
|
||||
config: Option<Expr>,
|
||||
remaining_args: Vec<MetaNameValue>,
|
||||
errors: TokenStream,
|
||||
}
|
||||
|
||||
impl Args {
|
||||
/// By default, proptest uses random seeds unless `$PROPTEST_SEED` is set.
|
||||
/// Rather than managing both `$SEED` and `$PROPTEST_SEED`, we intercept
|
||||
/// `config = ...` tokens and add a call to `gpui::apply_seed_to_config`.
|
||||
fn render(&self) -> TokenStream {
|
||||
let user_provided_config = match &self.config {
|
||||
None => quote! { ::gpui::proptest::prelude::ProptestConfig::default() },
|
||||
Some(config) => config.into_token_stream(),
|
||||
};
|
||||
|
||||
let fixed_config = quote!(::gpui::apply_seed_to_proptest_config(#user_provided_config));
|
||||
let remaining_args = &self.remaining_args;
|
||||
let errors = &self.errors;
|
||||
|
||||
quote! {
|
||||
#errors
|
||||
#[::gpui::proptest::property_test(
|
||||
proptest_path = "::gpui::proptest",
|
||||
config = #fixed_config,
|
||||
#(#remaining_args,)*
|
||||
)]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for Args {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let pairs = Punctuated::<MetaNameValue, Token![,]>::parse_terminated(input)?;
|
||||
|
||||
let mut config = None;
|
||||
let mut remaining_args = vec![];
|
||||
let mut errors = quote!();
|
||||
|
||||
for pair in pairs {
|
||||
match pair.path.get_ident().map(Ident::to_string).as_deref() {
|
||||
Some("config") => config = Some(pair.value),
|
||||
Some("proptest_path") => errors.extend(quote_spanned! {pair.span() =>
|
||||
compile_error!("`gpui::property_test` overrides the `proptest_path` parameter")
|
||||
}),
|
||||
_ => remaining_args.push(pair),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
config,
|
||||
remaining_args,
|
||||
errors,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ParsedArgs {
|
||||
cx_vars: TokenStream,
|
||||
cx_teardowns: TokenStream,
|
||||
proptest_args: TokenStream,
|
||||
errors: TokenStream,
|
||||
|
||||
// exprs passed at the call-site
|
||||
inner_fn_args: TokenStream,
|
||||
// args in the declaration
|
||||
inner_fn_decl_args: TokenStream,
|
||||
}
|
||||
|
||||
fn parse_args(args: Punctuated<FnArg, Comma>, test_name: &Ident) -> ParsedArgs {
|
||||
let mut parsed = ParsedArgs::default();
|
||||
let mut args = args.into_iter().collect();
|
||||
|
||||
remove_cxs(&mut parsed, &mut args, test_name);
|
||||
remove_std_rng(&mut parsed, &mut args);
|
||||
remove_background_executor(&mut parsed, &mut args);
|
||||
|
||||
// all remaining args forwarded to proptest's macro
|
||||
parsed.proptest_args = quote!( #(#args),* );
|
||||
|
||||
parsed
|
||||
}
|
||||
|
||||
fn remove_cxs(parsed: &mut ParsedArgs, args: &mut Vec<FnArg>, test_name: &Ident) {
|
||||
let mut ix = 0;
|
||||
args.retain_mut(|arg| {
|
||||
if !is_test_cx(arg) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let cx_varname = format_ident!("cx_{ix}");
|
||||
ix += 1;
|
||||
|
||||
parsed.cx_vars.extend(quote!(
|
||||
let mut #cx_varname = gpui::TestAppContext::build(
|
||||
dispatcher.clone(),
|
||||
Some(stringify!(#test_name)),
|
||||
);
|
||||
));
|
||||
parsed.cx_teardowns.extend(quote!(
|
||||
dispatcher.run_until_parked();
|
||||
#cx_varname.executor().forbid_parking();
|
||||
#cx_varname.quit();
|
||||
dispatcher.run_until_parked();
|
||||
));
|
||||
|
||||
parsed.inner_fn_decl_args.extend(quote!(#arg,));
|
||||
parsed.inner_fn_args.extend(quote!(&mut #cx_varname,));
|
||||
|
||||
false
|
||||
});
|
||||
}
|
||||
|
||||
fn remove_std_rng(parsed: &mut ParsedArgs, args: &mut Vec<FnArg>) {
|
||||
args.retain_mut(|arg| {
|
||||
if !is_std_rng(arg) {
|
||||
return true;
|
||||
}
|
||||
|
||||
parsed.errors.extend(quote_spanned! { arg.span() =>
|
||||
compile_error!("`StdRng` is not allowed in a property test. Consider implementing `Arbitrary`, or implementing a custom `Strategy`. https://altsysrq.github.io/proptest-book/proptest/tutorial/strategy-basics.html");
|
||||
});
|
||||
|
||||
false
|
||||
});
|
||||
}
|
||||
|
||||
fn remove_background_executor(parsed: &mut ParsedArgs, args: &mut Vec<FnArg>) {
|
||||
args.retain_mut(|arg| {
|
||||
if !is_background_executor(arg) {
|
||||
return true;
|
||||
}
|
||||
|
||||
parsed.inner_fn_decl_args.extend(quote!(#arg,));
|
||||
parsed
|
||||
.inner_fn_args
|
||||
.extend(quote!(gpui::BackgroundExecutor::new(std::sync::Arc::new(
|
||||
dispatcher.clone()
|
||||
)),));
|
||||
|
||||
false
|
||||
});
|
||||
}
|
||||
|
||||
// Matches `&TestAppContext` or `&foo::bar::baz::TestAppContext`
|
||||
fn is_test_cx(arg: &FnArg) -> bool {
|
||||
let FnArg::Typed(arg) = arg else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let Type::Reference(ty) = &*arg.ty else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let Type::Path(ty) = &*ty.elem else {
|
||||
return false;
|
||||
};
|
||||
|
||||
ty.path
|
||||
.segments
|
||||
.last()
|
||||
.is_some_and(|seg| seg.ident == "TestAppContext")
|
||||
}
|
||||
|
||||
fn is_std_rng(arg: &FnArg) -> bool {
|
||||
is_path_with_last_segment(arg, "StdRng")
|
||||
}
|
||||
|
||||
fn is_background_executor(arg: &FnArg) -> bool {
|
||||
is_path_with_last_segment(arg, "BackgroundExecutor")
|
||||
}
|
||||
|
||||
fn is_path_with_last_segment(arg: &FnArg, last_segment: &str) -> bool {
|
||||
let FnArg::Typed(arg) = arg else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let Type::Path(ty) = &*arg.ty else {
|
||||
return false;
|
||||
};
|
||||
|
||||
ty.path
|
||||
.segments
|
||||
.last()
|
||||
.is_some_and(|seg| seg.ident == last_segment)
|
||||
}
|
||||
47
crates/gpui_macros/src/register_action.rs
Normal file
47
crates/gpui_macros/src/register_action.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro2::{Ident, TokenStream as TokenStream2};
|
||||
use quote::{format_ident, quote};
|
||||
use syn::parse_macro_input;
|
||||
|
||||
pub(crate) fn register_action(ident: TokenStream) -> TokenStream {
|
||||
let name = parse_macro_input!(ident as Ident);
|
||||
let registration = generate_register_action(&name);
|
||||
|
||||
TokenStream::from(quote! {
|
||||
#registration
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn generate_register_action(type_name: &Ident) -> TokenStream2 {
|
||||
let action_builder_fn_name = format_ident!(
|
||||
"__gpui_actions_builder_{}",
|
||||
type_name.to_string().to_lowercase()
|
||||
);
|
||||
|
||||
quote! {
|
||||
impl #type_name {
|
||||
/// This is an auto generated function, do not use.
|
||||
#[automatically_derived]
|
||||
#[doc(hidden)]
|
||||
fn __autogenerated() {
|
||||
/// This is an auto generated function, do not use.
|
||||
#[doc(hidden)]
|
||||
fn #action_builder_fn_name() -> gpui::MacroActionData {
|
||||
gpui::MacroActionData {
|
||||
name: <#type_name as gpui::Action>::name_for_type(),
|
||||
type_id: ::std::any::TypeId::of::<#type_name>(),
|
||||
build: <#type_name as gpui::Action>::build,
|
||||
json_schema: <#type_name as gpui::Action>::action_json_schema,
|
||||
deprecated_aliases: <#type_name as gpui::Action>::deprecated_aliases(),
|
||||
deprecation_message: <#type_name as gpui::Action>::deprecation_message(),
|
||||
documentation: <#type_name as gpui::Action>::documentation(),
|
||||
}
|
||||
}
|
||||
|
||||
gpui::private::inventory::submit! {
|
||||
gpui::MacroActionBuilder(#action_builder_fn_name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1466
crates/gpui_macros/src/styles.rs
Normal file
1466
crates/gpui_macros/src/styles.rs
Normal file
File diff suppressed because it is too large
Load Diff
347
crates/gpui_macros/src/test.rs
Normal file
347
crates/gpui_macros/src/test.rs
Normal file
@@ -0,0 +1,347 @@
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro2::Ident;
|
||||
use quote::{format_ident, quote};
|
||||
use std::mem;
|
||||
use syn::{
|
||||
self, Expr, ExprLit, FnArg, ItemFn, Lit, Meta, MetaList, PathSegment, Token, Type,
|
||||
parse::{Parse, ParseStream},
|
||||
parse_quote,
|
||||
punctuated::Punctuated,
|
||||
spanned::Spanned,
|
||||
};
|
||||
|
||||
struct Args {
|
||||
seeds: Vec<u64>,
|
||||
max_retries: usize,
|
||||
max_iterations: usize,
|
||||
on_failure_fn_name: proc_macro2::TokenStream,
|
||||
}
|
||||
|
||||
impl Parse for Args {
|
||||
fn parse(input: ParseStream) -> Result<Self, syn::Error> {
|
||||
let mut seeds = Vec::<u64>::new();
|
||||
let mut max_retries = 0;
|
||||
let mut max_iterations = 1;
|
||||
let mut on_failure_fn_name = quote!(None);
|
||||
|
||||
let metas = Punctuated::<Meta, Token![,]>::parse_terminated(input)?;
|
||||
|
||||
for meta in metas {
|
||||
let ident = {
|
||||
let meta_path = match &meta {
|
||||
Meta::NameValue(meta) => &meta.path,
|
||||
Meta::List(list) => &list.path,
|
||||
Meta::Path(path) => {
|
||||
return Err(syn::Error::new(path.span(), "invalid path argument"));
|
||||
}
|
||||
};
|
||||
let Some(ident) = meta_path.get_ident() else {
|
||||
return Err(syn::Error::new(meta_path.span(), "unexpected path"));
|
||||
};
|
||||
ident.to_string()
|
||||
};
|
||||
|
||||
match (&meta, ident.as_str()) {
|
||||
(Meta::NameValue(meta), "retries") => {
|
||||
max_retries = parse_usize_from_expr(&meta.value)?
|
||||
}
|
||||
(Meta::NameValue(meta), "iterations") => {
|
||||
max_iterations = parse_usize_from_expr(&meta.value)?
|
||||
}
|
||||
(Meta::NameValue(meta), "on_failure") => {
|
||||
let Expr::Lit(ExprLit {
|
||||
lit: Lit::Str(name),
|
||||
..
|
||||
}) = &meta.value
|
||||
else {
|
||||
return Err(syn::Error::new(
|
||||
meta.value.span(),
|
||||
"on_failure argument must be a string",
|
||||
));
|
||||
};
|
||||
let segments = name
|
||||
.value()
|
||||
.split("::")
|
||||
.map(|part| PathSegment::from(Ident::new(part, name.span())))
|
||||
.collect();
|
||||
let path = syn::Path {
|
||||
leading_colon: None,
|
||||
segments,
|
||||
};
|
||||
on_failure_fn_name = quote!(Some(#path));
|
||||
}
|
||||
(Meta::NameValue(meta), "seed") => {
|
||||
seeds = vec![parse_usize_from_expr(&meta.value)? as u64]
|
||||
}
|
||||
(Meta::List(list), "seeds") => seeds = parse_u64_array(list)?,
|
||||
(Meta::Path(_), _) => {
|
||||
return Err(syn::Error::new(meta.span(), "invalid path argument"));
|
||||
}
|
||||
(_, _) => {
|
||||
return Err(syn::Error::new(meta.span(), "invalid argument name"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Args {
|
||||
seeds,
|
||||
max_retries,
|
||||
max_iterations,
|
||||
on_failure_fn_name,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
|
||||
let args = syn::parse_macro_input!(args as Args);
|
||||
let mut inner_fn = match syn::parse::<ItemFn>(function) {
|
||||
Ok(f) => f,
|
||||
Err(err) => return error_to_stream(err),
|
||||
};
|
||||
|
||||
let inner_fn_attributes = mem::take(&mut inner_fn.attrs);
|
||||
let inner_fn_name = format_ident!("__{}", inner_fn.sig.ident);
|
||||
let outer_fn_name = mem::replace(&mut inner_fn.sig.ident, inner_fn_name.clone());
|
||||
|
||||
let result = generate_test_function(
|
||||
args,
|
||||
inner_fn,
|
||||
inner_fn_attributes,
|
||||
inner_fn_name,
|
||||
outer_fn_name,
|
||||
);
|
||||
match result {
|
||||
Ok(tokens) => tokens,
|
||||
Err(tokens) => tokens,
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_test_function(
|
||||
args: Args,
|
||||
inner_fn: ItemFn,
|
||||
inner_fn_attributes: Vec<syn::Attribute>,
|
||||
inner_fn_name: Ident,
|
||||
outer_fn_name: Ident,
|
||||
) -> Result<TokenStream, TokenStream> {
|
||||
let seeds = &args.seeds;
|
||||
let max_retries = args.max_retries;
|
||||
let num_iterations = args.max_iterations;
|
||||
let on_failure_fn_name = &args.on_failure_fn_name;
|
||||
let seeds = quote!( #(#seeds),* );
|
||||
|
||||
let mut outer_fn: ItemFn = if inner_fn.sig.asyncness.is_some() {
|
||||
// Pass to the test function the number of app contexts that it needs,
|
||||
// based on its parameter list.
|
||||
let mut cx_vars = proc_macro2::TokenStream::new();
|
||||
let mut cx_teardowns = proc_macro2::TokenStream::new();
|
||||
let mut inner_fn_args = proc_macro2::TokenStream::new();
|
||||
for (ix, arg) in inner_fn.sig.inputs.iter().enumerate() {
|
||||
if let FnArg::Typed(arg) = arg {
|
||||
if let Type::Path(ty) = &*arg.ty {
|
||||
let last_segment = ty.path.segments.last();
|
||||
match last_segment.map(|s| s.ident.to_string()).as_deref() {
|
||||
Some("StdRng") => {
|
||||
inner_fn_args.extend(quote!(rand::SeedableRng::seed_from_u64(_seed),));
|
||||
continue;
|
||||
}
|
||||
Some("BackgroundExecutor") => {
|
||||
inner_fn_args.extend(quote!(gpui::BackgroundExecutor::new(
|
||||
std::sync::Arc::new(dispatcher.clone()),
|
||||
),));
|
||||
continue;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
} else if let Type::Reference(ty) = &*arg.ty
|
||||
&& let Type::Path(ty) = &*ty.elem
|
||||
{
|
||||
let last_segment = ty.path.segments.last();
|
||||
if let Some("TestAppContext") =
|
||||
last_segment.map(|s| s.ident.to_string()).as_deref()
|
||||
{
|
||||
let cx_varname = format_ident!("cx_{}", ix);
|
||||
cx_vars.extend(quote!(
|
||||
let mut #cx_varname = gpui::TestAppContext::build(
|
||||
dispatcher.clone(),
|
||||
Some(stringify!(#outer_fn_name)),
|
||||
);
|
||||
let _entity_refcounts = #cx_varname.app.borrow().ref_counts_drop_handle();
|
||||
));
|
||||
cx_teardowns.extend(quote!(
|
||||
#cx_varname.run_until_parked();
|
||||
#cx_varname.update(|cx| { cx.background_executor().forbid_parking(); cx.quit(); });
|
||||
#cx_varname.run_until_parked();
|
||||
drop(#cx_varname);
|
||||
));
|
||||
inner_fn_args.extend(quote!(&mut #cx_varname,));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Err(error_with_message("invalid function signature", arg));
|
||||
}
|
||||
|
||||
parse_quote! {
|
||||
#[test]
|
||||
fn #outer_fn_name() {
|
||||
#inner_fn
|
||||
|
||||
gpui::run_test(
|
||||
#num_iterations,
|
||||
&[#seeds],
|
||||
#max_retries,
|
||||
&mut |dispatcher, _seed| {
|
||||
let exec = std::sync::Arc::new(dispatcher.clone());
|
||||
#cx_vars
|
||||
gpui::ForegroundExecutor::new(exec.clone()).block_test(#inner_fn_name(#inner_fn_args));
|
||||
drop(exec);
|
||||
#cx_teardowns
|
||||
// Ideally we would only drop cancelled tasks, that way we could detect leaks due to task <-> entity
|
||||
// cycles as cancelled tasks will be dropped properly once the runnable gets run again
|
||||
//
|
||||
// async-task does not give us the power to do this just yet though
|
||||
dispatcher.drain_tasks();
|
||||
drop(dispatcher);
|
||||
},
|
||||
#on_failure_fn_name
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Pass to the test function the number of app contexts that it needs,
|
||||
// based on its parameter list.
|
||||
let mut cx_vars = proc_macro2::TokenStream::new();
|
||||
let mut cx_teardowns = proc_macro2::TokenStream::new();
|
||||
let mut inner_fn_args = proc_macro2::TokenStream::new();
|
||||
for (ix, arg) in inner_fn.sig.inputs.iter().enumerate() {
|
||||
if let FnArg::Typed(arg) = arg {
|
||||
if let Type::Path(ty) = &*arg.ty {
|
||||
let last_segment = ty.path.segments.last();
|
||||
|
||||
if let Some("StdRng") = last_segment.map(|s| s.ident.to_string()).as_deref() {
|
||||
inner_fn_args.extend(quote!(rand::SeedableRng::seed_from_u64(_seed),));
|
||||
continue;
|
||||
}
|
||||
} else if let Type::Reference(ty) = &*arg.ty
|
||||
&& let Type::Path(ty) = &*ty.elem
|
||||
{
|
||||
let last_segment = ty.path.segments.last();
|
||||
match last_segment.map(|s| s.ident.to_string()).as_deref() {
|
||||
Some("App") => {
|
||||
let cx_varname = format_ident!("cx_{}", ix);
|
||||
let cx_varname_lock = format_ident!("cx_{}_lock", ix);
|
||||
cx_vars.extend(quote!(
|
||||
let mut #cx_varname = gpui::TestAppContext::build(
|
||||
dispatcher.clone(),
|
||||
Some(stringify!(#outer_fn_name))
|
||||
);
|
||||
let mut #cx_varname_lock = #cx_varname.app.borrow_mut();
|
||||
let _entity_refcounts = #cx_varname_lock.ref_counts_drop_handle();
|
||||
));
|
||||
inner_fn_args.extend(quote!(&mut #cx_varname_lock,));
|
||||
cx_teardowns.extend(quote!(
|
||||
drop(#cx_varname_lock);
|
||||
#cx_varname.run_until_parked();
|
||||
#cx_varname.update(|cx| { cx.background_executor().forbid_parking(); cx.quit(); });
|
||||
#cx_varname.run_until_parked();
|
||||
drop(#cx_varname);
|
||||
));
|
||||
continue;
|
||||
}
|
||||
Some("TestAppContext") => {
|
||||
let cx_varname = format_ident!("cx_{}", ix);
|
||||
cx_vars.extend(quote!(
|
||||
let mut #cx_varname = gpui::TestAppContext::build(
|
||||
dispatcher.clone(),
|
||||
Some(stringify!(#outer_fn_name))
|
||||
);
|
||||
let _entity_refcounts = #cx_varname.app.borrow().ref_counts_drop_handle();
|
||||
));
|
||||
cx_teardowns.extend(quote!(
|
||||
#cx_varname.run_until_parked();
|
||||
#cx_varname.update(|cx| { cx.background_executor().forbid_parking(); cx.quit(); });
|
||||
#cx_varname.run_until_parked();
|
||||
drop(#cx_varname);
|
||||
));
|
||||
inner_fn_args.extend(quote!(&mut #cx_varname,));
|
||||
continue;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Err(error_with_message("invalid function signature", arg));
|
||||
}
|
||||
|
||||
parse_quote! {
|
||||
#[test]
|
||||
fn #outer_fn_name() {
|
||||
#inner_fn
|
||||
|
||||
gpui::run_test(
|
||||
#num_iterations,
|
||||
&[#seeds],
|
||||
#max_retries,
|
||||
&mut |dispatcher, _seed| {
|
||||
#cx_vars
|
||||
#inner_fn_name(#inner_fn_args);
|
||||
#cx_teardowns
|
||||
// Ideally we would only drop cancelled tasks, that way we could detect leaks due to task <-> entity
|
||||
// cycles as cancelled tasks will be dropped properly once they runnable gets run again
|
||||
//
|
||||
// async-task does not give us the power to do this just yet though
|
||||
dispatcher.drain_tasks();
|
||||
drop(dispatcher);
|
||||
},
|
||||
#on_failure_fn_name,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
outer_fn.attrs.extend(inner_fn_attributes);
|
||||
|
||||
Ok(TokenStream::from(quote!(#outer_fn)))
|
||||
}
|
||||
|
||||
fn parse_usize_from_expr(expr: &Expr) -> Result<usize, syn::Error> {
|
||||
let Expr::Lit(ExprLit {
|
||||
lit: Lit::Int(int), ..
|
||||
}) = expr
|
||||
else {
|
||||
return Err(syn::Error::new(expr.span(), "expected an integer"));
|
||||
};
|
||||
int.base10_parse()
|
||||
.map_err(|_| syn::Error::new(int.span(), "failed to parse integer"))
|
||||
}
|
||||
|
||||
fn parse_u64_array(meta_list: &MetaList) -> Result<Vec<u64>, syn::Error> {
|
||||
let mut result = Vec::new();
|
||||
let tokens = &meta_list.tokens;
|
||||
let parser = |input: ParseStream| {
|
||||
let exprs = Punctuated::<Expr, Token![,]>::parse_terminated(input)?;
|
||||
for expr in exprs {
|
||||
if let Expr::Lit(ExprLit {
|
||||
lit: Lit::Int(int), ..
|
||||
}) = expr
|
||||
{
|
||||
let value: usize = int.base10_parse()?;
|
||||
result.push(value as u64);
|
||||
} else {
|
||||
return Err(syn::Error::new(expr.span(), "expected an integer"));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
syn::parse::Parser::parse2(parser, tokens.clone())?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn error_with_message(message: &str, spanned: impl Spanned) -> TokenStream {
|
||||
error_to_stream(syn::Error::new(spanned.span(), message))
|
||||
}
|
||||
|
||||
fn error_to_stream(err: syn::Error) -> TokenStream {
|
||||
TokenStream::from(err.into_compile_error())
|
||||
}
|
||||
13
crates/gpui_macros/tests/derive_context.rs
Normal file
13
crates/gpui_macros/tests/derive_context.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
#[test]
|
||||
fn test_derive_context() {
|
||||
use gpui::{App, Window};
|
||||
use gpui_macros::{AppContext, VisualContext};
|
||||
|
||||
#[derive(AppContext, VisualContext)]
|
||||
struct _MyCustomContext<'a, 'b> {
|
||||
#[app]
|
||||
app: &'a mut App,
|
||||
#[window]
|
||||
window: &'b mut Window,
|
||||
}
|
||||
}
|
||||
133
crates/gpui_macros/tests/derive_inspector_reflection.rs
Normal file
133
crates/gpui_macros/tests/derive_inspector_reflection.rs
Normal file
@@ -0,0 +1,133 @@
|
||||
//! This code was generated using Zed Agent with Claude Opus 4.
|
||||
|
||||
// gate on rust-analyzer so rust-analyzer never needs to expand this macro, it takes up to 10 seconds to expand due to inefficiencies in rust-analyzers proc-macro srv
|
||||
#[cfg_attr(not(rust_analyzer), gpui_macros::derive_inspector_reflection)]
|
||||
trait Transform: Clone {
|
||||
/// Doubles the value
|
||||
fn double(self) -> Self;
|
||||
|
||||
/// Triples the value
|
||||
fn triple(self) -> Self;
|
||||
|
||||
/// Increments the value by one
|
||||
///
|
||||
/// This method has a default implementation
|
||||
fn increment(self) -> Self {
|
||||
// Default implementation
|
||||
self.add_one()
|
||||
}
|
||||
|
||||
/// Quadruples the value by doubling twice
|
||||
fn quadruple(self) -> Self {
|
||||
// Default implementation with mut self
|
||||
self.double().double()
|
||||
}
|
||||
|
||||
// These methods will be filtered out:
|
||||
#[allow(dead_code)]
|
||||
fn add(&self, other: &Self) -> Self;
|
||||
#[allow(dead_code)]
|
||||
fn set_value(&mut self, value: i32);
|
||||
#[allow(dead_code)]
|
||||
fn get_value(&self) -> i32;
|
||||
|
||||
/// Adds one to the value
|
||||
fn add_one(self) -> Self;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
struct Number(i32);
|
||||
|
||||
impl Transform for Number {
|
||||
fn double(self) -> Self {
|
||||
Number(self.0 * 2)
|
||||
}
|
||||
|
||||
fn triple(self) -> Self {
|
||||
Number(self.0 * 3)
|
||||
}
|
||||
|
||||
fn add(&self, other: &Self) -> Self {
|
||||
Number(self.0 + other.0)
|
||||
}
|
||||
|
||||
fn set_value(&mut self, value: i32) {
|
||||
self.0 = value;
|
||||
}
|
||||
|
||||
fn get_value(&self) -> i32 {
|
||||
self.0
|
||||
}
|
||||
|
||||
fn add_one(self) -> Self {
|
||||
Number(self.0 + 1)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_derive_inspector_reflection() {
|
||||
use transform_reflection::*;
|
||||
|
||||
// Get all methods that match the pattern fn(self) -> Self or fn(mut self) -> Self
|
||||
let methods = methods::<Number>();
|
||||
|
||||
assert_eq!(methods.len(), 5);
|
||||
let method_names: Vec<_> = methods.iter().map(|m| m.name).collect();
|
||||
assert!(method_names.contains(&"double"));
|
||||
assert!(method_names.contains(&"triple"));
|
||||
assert!(method_names.contains(&"increment"));
|
||||
assert!(method_names.contains(&"quadruple"));
|
||||
assert!(method_names.contains(&"add_one"));
|
||||
|
||||
// Invoke methods by name
|
||||
let num = Number(5);
|
||||
|
||||
let doubled = find_method::<Number>("double").unwrap().invoke(num.clone());
|
||||
assert_eq!(doubled, Number(10));
|
||||
|
||||
let tripled = find_method::<Number>("triple").unwrap().invoke(num.clone());
|
||||
assert_eq!(tripled, Number(15));
|
||||
|
||||
let incremented = find_method::<Number>("increment")
|
||||
.unwrap()
|
||||
.invoke(num.clone());
|
||||
assert_eq!(incremented, Number(6));
|
||||
|
||||
let quadrupled = find_method::<Number>("quadruple").unwrap().invoke(num);
|
||||
assert_eq!(quadrupled, Number(20));
|
||||
|
||||
// Try to invoke a non-existent method
|
||||
let result = find_method::<Number>("nonexistent");
|
||||
assert!(result.is_none());
|
||||
|
||||
// Chain operations
|
||||
let num = Number(10);
|
||||
let result = find_method::<Number>("double")
|
||||
.map(|m| m.invoke(num))
|
||||
.and_then(|n| find_method::<Number>("increment").map(|m| m.invoke(n)))
|
||||
.and_then(|n| find_method::<Number>("triple").map(|m| m.invoke(n)));
|
||||
|
||||
assert_eq!(result, Some(Number(63))); // (10 * 2 + 1) * 3 = 63
|
||||
|
||||
// Test documentationumentation capture
|
||||
let double_method = find_method::<Number>("double").unwrap();
|
||||
assert_eq!(double_method.documentation, Some("Doubles the value"));
|
||||
|
||||
let triple_method = find_method::<Number>("triple").unwrap();
|
||||
assert_eq!(triple_method.documentation, Some("Triples the value"));
|
||||
|
||||
let increment_method = find_method::<Number>("increment").unwrap();
|
||||
assert_eq!(
|
||||
increment_method.documentation,
|
||||
Some("Increments the value by one\n\nThis method has a default implementation")
|
||||
);
|
||||
|
||||
let quadruple_method = find_method::<Number>("quadruple").unwrap();
|
||||
assert_eq!(
|
||||
quadruple_method.documentation,
|
||||
Some("Quadruples the value by doubling twice")
|
||||
);
|
||||
|
||||
let add_one_method = find_method::<Number>("add_one").unwrap();
|
||||
assert_eq!(add_one_method.documentation, Some("Adds one to the value"));
|
||||
}
|
||||
7
crates/gpui_macros/tests/render_test.rs
Normal file
7
crates/gpui_macros/tests/render_test.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
#[test]
|
||||
fn test_derive_render() {
|
||||
use gpui_macros::Render;
|
||||
|
||||
#[derive(Render)]
|
||||
struct _Element;
|
||||
}
|
||||
Reference in New Issue
Block a user