logiguard fork: GPUI xdg-activation keyboard-focus serial fix
Some checks failed
Update All Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Triage Project Sync (#84) / Sync triage project (push) Has been cancelled
release_nightly / notify_on_failure (push) Has been cancelled
release_nightly / check_style (push) Has been cancelled
release_nightly / run_tests_windows (push) Has been cancelled
release_nightly / clippy_windows (push) Has been cancelled
release_nightly / bundle_linux_aarch64 (push) Has been cancelled
release_nightly / bundle_linux_x86_64 (push) Has been cancelled
release_nightly / bundle_mac_aarch64 (push) Has been cancelled
release_nightly / bundle_mac_x86_64 (push) Has been cancelled
release_nightly / bundle_windows_aarch64 (push) Has been cancelled
release_nightly / bundle_windows_x86_64 (push) Has been cancelled
release_nightly / build_nix_linux_x86_64 (push) Has been cancelled
release_nightly / build_nix_mac_aarch64 (push) Has been cancelled
release_nightly / update_nightly_tag (push) Has been cancelled
Hotfix Review Monitor / check-hotfix-reviews (push) Has been cancelled
Stale PR Review Reminder / check-stale-prs (push) Has been cancelled
Update Weekly Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Bump collab-staging Tag / update-collab-staging-tag (push) Has been cancelled
compliance_check / scheduled_compliance_check (push) Has been cancelled

Single-commit orphan branch: full zed-industries/zed @ 8c74db0 source tree
with a 3-file patch applied (no upstream history).

Patch (crates/gpui_linux/src/linux/wayland/):
  - serial.rs: add SerialKind::KeyboardEnter
  - client.rs: store wl_keyboard.enter serial; add latest_serial_of()
  - window.rs: activate() uses keyboard-enter serial (Mutter focus gate)

Mutter honors window activation only when the token carries the keyboard-
focus serial from wl_keyboard.enter; GPUI used a stale mouse-press serial.
See docs/tray-window-focus-wayland.md in logiguard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mohamad Khani
2026-07-14 02:22:17 +03:30
commit b72a46db68
3984 changed files with 1583326 additions and 0 deletions

View 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"] }

View File

@@ -0,0 +1 @@
../../LICENSE-APACHE

View 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
}
}
})
}

View 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()
}

View 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(&macro_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 = &macro_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)
}

View 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()
}

View 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()
}

View 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()
}

View 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,
}
}

View 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)
}

View 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)
}
}
}
}
}

File diff suppressed because it is too large Load Diff

View 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())
}

View 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,
}
}

View 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"));
}

View File

@@ -0,0 +1,7 @@
#[test]
fn test_derive_render() {
use gpui_macros::Render;
#[derive(Render)]
struct _Element;
}