Fix window double borrows (#23739)

Fix bugs caused by the window context PR, where the window could be on
the stack and is then requested from the App.
This PR also adds derive macros for `AppContext` and `VisualContext` so
that it's easy to define further contexts in API code, such as
`editor::BlockContext`.

Release Notes:

- N/A
This commit is contained in:
Mikayla Maki 2025-01-27 13:56:29 -08:00 committed by GitHub
parent 29bfb56739
commit a7c549b85b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 465 additions and 297 deletions

View file

@ -11,7 +11,7 @@ workspace = true
[lib]
path = "src/gpui_macros.rs"
proc-macro = true
doctest = false
doctest = true
[dependencies]
proc-macro2 = "1.0.66"

View file

@ -0,0 +1,88 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
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 gen = quote! {
impl #impl_generics gpui::AppContext for #type_name #type_generics
#where_clause
{
type Result<T> = T;
fn new<T: 'static>(
&mut self,
build_model: impl FnOnce(&mut gpui::Context<'_, T>) -> T,
) -> Self::Result<gpui::Entity<T>> {
self.#app_variable.new(build_model)
}
fn reserve_entity<T: 'static>(&mut self) -> Self::Result<gpui::Reservation<T>> {
self.#app_variable.reserve_entity()
}
fn insert_entity<T: 'static>(
&mut self,
reservation: gpui::Reservation<T>,
build_model: impl FnOnce(&mut gpui::Context<'_, T>) -> T,
) -> Self::Result<gpui::Entity<T>> {
self.#app_variable.insert_entity(reservation, build_model)
}
fn update_entity<T, R>(
&mut self,
handle: &gpui::Entity<T>,
update: impl FnOnce(&mut T, &mut gpui::Context<'_, T>) -> R,
) -> Self::Result<R>
where
T: 'static,
{
self.#app_variable.update_entity(handle, update)
}
fn read_entity<T, R>(
&self,
handle: &gpui::Entity<T>,
read: impl FnOnce(&T, &gpui::App) -> R,
) -> Self::Result<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 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)
}
}
};
gen.into()
}

View file

@ -0,0 +1,71 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
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 gen = quote! {
impl #impl_generics gpui::VisualContext for #type_name #type_generics
#where_clause
{
fn window_handle(&self) -> gpui::AnyWindowHandle {
self.#window_variable.window_handle()
}
fn update_window_entity<T: 'static, R>(
&mut self,
model: &gpui::Entity<T>,
update: impl FnOnce(&mut T, &mut gpui::Window, &mut gpui::Context<T>) -> R,
) -> Self::Result<R> {
gpui::AppContext::update_entity(self.#app_variable, model, |entity, cx| update(entity, self.#window_variable, cx))
}
fn new_window_entity<T: 'static>(
&mut self,
build_model: impl FnOnce(&mut gpui::Window, &mut gpui::Context<'_, T>) -> T,
) -> Self::Result<gpui::Entity<T>> {
gpui::AppContext::new(self.#app_variable, |cx| build_model(self.#window_variable, cx))
}
fn replace_root_view<V>(
&mut self,
build_view: impl FnOnce(&mut gpui::Window, &mut gpui::Context<V>) -> V,
) -> Self::Result<gpui::Entity<V>>
where
V: 'static + gpui::Render,
{
self.#window_variable.replace_root(self.#app_variable, build_view)
}
fn focus<V>(&mut self, model: &gpui::Entity<V>) -> Self::Result<()>
where
V: gpui::Focusable,
{
let focus_handle = gpui::Focusable::focus_handle(model, self.#app_variable);
self.#window_variable.focus(&focus_handle)
}
}
};
gen.into()
}

View file

@ -1,11 +1,14 @@
mod derive_app_context;
mod derive_into_element;
mod derive_path_static_str;
mod derive_render;
mod derive_visual_context;
mod register_action;
mod styles;
mod test;
use proc_macro::TokenStream;
use syn::{DeriveInput, Ident};
/// register_action! can be used to register an action with the GPUI runtime.
/// You should typically use `gpui::actions!` or `gpui::impl_actions!` instead,
@ -34,6 +37,57 @@ pub fn derive_path_static_str(input: TokenStream) -> TokenStream {
derive_path_static_str::derive_path_static_str(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)]
@ -115,3 +169,15 @@ pub fn box_shadow_style_methods(input: TokenStream) -> TokenStream {
pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
test::test(args, function)
}
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,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,
}
}