Fix window double borrows (#23739)

Mikayla Maki created

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

Change summary

crates/assistant/src/inline_assistant.rs                        |  24 
crates/assistant2/src/inline_prompt_editor.rs                   |  24 
crates/assistant_context_editor/src/context_editor.rs           |  26 
crates/copilot/src/sign_in.rs                                   |  26 
crates/editor/src/display_map/block_map.rs                      |   3 
crates/editor/src/element.rs                                    |   4 
crates/go_to_line/src/go_to_line.rs                             |  26 
crates/gpui/src/app.rs                                          |  12 
crates/gpui/src/app/model_context.rs                            |  44 
crates/gpui/src/gpui.rs                                         |   2 
crates/gpui/src/platform.rs                                     |   2 
crates/gpui/src/window.rs                                       |  10 
crates/gpui_macros/Cargo.toml                                   |   2 
crates/gpui_macros/src/derive_app_context.rs                    |  88 ++
crates/gpui_macros/src/derive_visual_context.rs                 |  71 +
crates/gpui_macros/src/gpui_macros.rs                           |  66 +
crates/gpui_macros/tests/derive_context.rs                      |  13 
crates/inline_completion_button/src/inline_completion_button.rs |  55 
crates/prompt_library/src/prompt_library.rs                     |  99 +-
crates/search/src/search.rs                                     |  17 
crates/workspace/src/notifications.rs                           | 114 +-
crates/workspace/src/workspace.rs                               |  30 
crates/zed/src/zed.rs                                           |   4 
crates/zed/src/zed/inline_completion_registry.rs                |   6 
24 files changed, 468 insertions(+), 300 deletions(-)

Detailed changes

crates/assistant/src/inline_assistant.rs 🔗

@@ -1878,19 +1878,17 @@ impl PromptEditor {
     ) {
         match event {
             EditorEvent::Edited { .. } => {
-                if let Some(workspace) = window.window_handle().downcast::<Workspace>() {
-                    workspace
-                        .update(cx, |workspace, _, cx| {
-                            let is_via_ssh = workspace
-                                .project()
-                                .update(cx, |project, _| project.is_via_ssh());
-
-                            workspace
-                                .client()
-                                .telemetry()
-                                .log_edit_event("inline assist", is_via_ssh);
-                        })
-                        .log_err();
+                if let Some(workspace) = window.root::<Workspace>().flatten() {
+                    workspace.update(cx, |workspace, cx| {
+                        let is_via_ssh = workspace
+                            .project()
+                            .update(cx, |project, _| project.is_via_ssh());
+
+                        workspace
+                            .client()
+                            .telemetry()
+                            .log_edit_event("inline assist", is_via_ssh);
+                    });
                 }
                 let prompt = self.editor.read(cx).text(cx);
                 if self

crates/assistant2/src/inline_prompt_editor.rs 🔗

@@ -304,19 +304,17 @@ impl<T: 'static> PromptEditor<T> {
     ) {
         match event {
             EditorEvent::Edited { .. } => {
-                if let Some(workspace) = window.window_handle().downcast::<Workspace>() {
-                    workspace
-                        .update(cx, |workspace, _, cx| {
-                            let is_via_ssh = workspace
-                                .project()
-                                .update(cx, |project, _| project.is_via_ssh());
-
-                            workspace
-                                .client()
-                                .telemetry()
-                                .log_edit_event("inline assist", is_via_ssh);
-                        })
-                        .log_err();
+                if let Some(workspace) = window.root::<Workspace>().flatten() {
+                    workspace.update(cx, |workspace, cx| {
+                        let is_via_ssh = workspace
+                            .project()
+                            .update(cx, |project, _| project.is_via_ssh());
+
+                        workspace
+                            .client()
+                            .telemetry()
+                            .log_edit_event("inline assist", is_via_ssh);
+                    });
                 }
                 let prompt = self.editor.read(cx).text(cx);
                 if self

crates/assistant_context_editor/src/context_editor.rs 🔗

@@ -23,11 +23,11 @@ use fs::Fs;
 use futures::FutureExt;
 use gpui::{
     actions, div, img, impl_internal_actions, percentage, point, prelude::*, pulsating_between,
-    size, Animation, AnimationExt, AnyElement, AnyView, AnyWindowHandle, App, AsyncWindowContext,
-    ClipboardEntry, ClipboardItem, CursorStyle, Empty, Entity, EventEmitter, FocusHandle,
-    Focusable, FontWeight, Global, InteractiveElement, IntoElement, ParentElement, Pixels, Render,
-    RenderImage, SharedString, Size, StatefulInteractiveElement, Styled, Subscription, Task,
-    Transformation, WeakEntity,
+    size, Animation, AnimationExt, AnyElement, AnyView, App, AsyncWindowContext, ClipboardEntry,
+    ClipboardItem, CursorStyle, Empty, Entity, EventEmitter, FocusHandle, Focusable, FontWeight,
+    Global, InteractiveElement, IntoElement, ParentElement, Pixels, Render, RenderImage,
+    SharedString, Size, StatefulInteractiveElement, Styled, Subscription, Task, Transformation,
+    WeakEntity,
 };
 use indexed_docs::IndexedDocsStore;
 use language::{language_settings::SoftWrap, BufferSnapshot, LspAdapterDelegate, ToOffset};
@@ -978,21 +978,20 @@ impl ContextEditor {
                     .unwrap();
                 let render_block: RenderBlock = Arc::new({
                     let this = this.clone();
-                    let window_handle = window.window_handle();
                     let patch_range = range.clone();
                     move |cx: &mut BlockContext<'_, '_>| {
                         let max_width = cx.max_width;
                         let gutter_width = cx.gutter_dimensions.full_width();
                         let block_id = cx.block_id;
                         let selected = cx.selected;
-                        this.update(&mut **cx, |this, cx| {
+                        this.update_in(cx, |this, window, cx| {
                             this.render_patch_block(
                                 patch_range.clone(),
                                 max_width,
                                 gutter_width,
                                 block_id,
                                 selected,
-                                window_handle,
+                                window,
                                 cx,
                             )
                         })
@@ -2198,15 +2197,12 @@ impl ContextEditor {
         gutter_width: Pixels,
         id: BlockId,
         selected: bool,
-        window_handle: AnyWindowHandle,
+        window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Option<AnyElement> {
-        let snapshot = window_handle
-            .update(cx, |_, window, cx| {
-                self.editor
-                    .update(cx, |editor, cx| editor.snapshot(window, cx))
-            })
-            .ok()?;
+        let snapshot = self
+            .editor
+            .update(cx, |editor, cx| editor.snapshot(window, cx));
         let (excerpt_id, _buffer_id, _) = snapshot.buffer_snapshot.as_singleton().unwrap();
         let excerpt_id = *excerpt_id;
         let anchor = snapshot

crates/copilot/src/sign_in.rs 🔗

@@ -18,16 +18,12 @@ pub fn initiate_sign_in(window: &mut Window, cx: &mut App) {
         return;
     };
     let status = copilot.read(cx).status();
-    let Some(workspace) = window.window_handle().downcast::<Workspace>() else {
+    let Some(workspace) = window.root::<Workspace>().flatten() else {
         return;
     };
     match status {
         Status::Starting { task } => {
-            let Some(workspace) = window.window_handle().downcast::<Workspace>() else {
-                return;
-            };
-
-            let Ok(workspace) = workspace.update(cx, |workspace, _window, cx| {
+            workspace.update(cx, |workspace, cx| {
                 workspace.show_toast(
                     Toast::new(
                         NotificationId::unique::<CopilotStartingToast>(),
@@ -35,11 +31,9 @@ pub fn initiate_sign_in(window: &mut Window, cx: &mut App) {
                     ),
                     cx,
                 );
-                workspace.weak_handle()
-            }) else {
-                return;
-            };
+            });
 
+            let workspace = workspace.downgrade();
             cx.spawn(|mut cx| async move {
                 task.await;
                 if let Some(copilot) = cx.update(|cx| Copilot::global(cx)).ok().flatten() {
@@ -69,13 +63,11 @@ pub fn initiate_sign_in(window: &mut Window, cx: &mut App) {
         }
         _ => {
             copilot.update(cx, |this, cx| this.sign_in(cx)).detach();
-            workspace
-                .update(cx, |this, window, cx| {
-                    this.toggle_modal(window, cx, |_, cx| {
-                        CopilotCodeVerification::new(&copilot, cx)
-                    });
-                })
-                .ok();
+            workspace.update(cx, |this, cx| {
+                this.toggle_modal(window, cx, |_, cx| {
+                    CopilotCodeVerification::new(&copilot, cx)
+                });
+            });
         }
     }
 }

crates/editor/src/display_map/block_map.rs 🔗

@@ -225,8 +225,11 @@ pub enum BlockStyle {
     Sticky,
 }
 
+#[derive(gpui::AppContext, gpui::VisualContext)]
 pub struct BlockContext<'a, 'b> {
+    #[window]
     pub window: &'a mut Window,
+    #[app]
     pub app: &'b mut App,
     pub anchor_x: Pixels,
     pub max_width: Pixels,

crates/editor/src/element.rs 🔗

@@ -4147,9 +4147,9 @@ impl EditorElement {
             // In singleton buffers, we select corresponding lines on the line number click, so use | -like cursor.
             // In multi buffers, we open file at the line number clicked, so use a pointing hand cursor.
             if is_singleton {
-                window.set_cursor_style(CursorStyle::IBeam, hitbox);
+                window.set_cursor_style(CursorStyle::IBeam, &hitbox);
             } else {
-                window.set_cursor_style(CursorStyle::PointingHand, hitbox);
+                window.set_cursor_style(CursorStyle::PointingHand, &hitbox);
             }
         }
     }

crates/go_to_line/src/go_to_line.rs 🔗

@@ -5,8 +5,8 @@ use editor::{
     actions::Tab, scroll::Autoscroll, Anchor, Editor, MultiBufferSnapshot, ToOffset, ToPoint,
 };
 use gpui::{
-    div, prelude::*, AnyWindowHandle, App, DismissEvent, Entity, EventEmitter, FocusHandle,
-    Focusable, Render, SharedString, Styled, Subscription,
+    div, prelude::*, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render,
+    SharedString, Styled, Subscription,
 };
 use language::Buffer;
 use settings::Settings;
@@ -133,19 +133,15 @@ impl GoToLine {
         }
     }
 
-    fn release(&mut self, window: AnyWindowHandle, cx: &mut App) {
-        window
-            .update(cx, |_, window, cx| {
-                let scroll_position = self.prev_scroll_position.take();
-                self.active_editor.update(cx, |editor, cx| {
-                    editor.clear_row_highlights::<GoToLineRowHighlights>();
-                    if let Some(scroll_position) = scroll_position {
-                        editor.set_scroll_position(scroll_position, window, cx);
-                    }
-                    cx.notify();
-                })
-            })
-            .ok();
+    fn release(&mut self, window: &mut Window, cx: &mut App) {
+        let scroll_position = self.prev_scroll_position.take();
+        self.active_editor.update(cx, |editor, cx| {
+            editor.clear_row_highlights::<GoToLineRowHighlights>();
+            if let Some(scroll_position) = scroll_position {
+                editor.set_scroll_position(scroll_position, window, cx);
+            }
+            cx.notify();
+        })
     }
 
     fn on_line_editor_event(

crates/gpui/src/app.rs 🔗

@@ -1236,15 +1236,9 @@ impl App {
         T: 'static,
     {
         let window_handle = window.handle;
-        let (subscription, activate) = self.release_listeners.insert(
-            handle.entity_id(),
-            Box::new(move |entity, cx| {
-                let entity = entity.downcast_mut().expect("invalid entity type");
-                let _ = window_handle.update(cx, |_, window, cx| on_release(entity, window, cx));
-            }),
-        );
-        activate();
-        subscription
+        self.observe_release(&handle, move |entity, cx| {
+            let _ = window_handle.update(cx, |_, window, cx| on_release(entity, window, cx));
+        })
     }
 
     /// Register a callback to be invoked when a keystroke is received by the application

crates/gpui/src/app/model_context.rs 🔗

@@ -323,46 +323,32 @@ impl<'a, T: 'static> Context<'a, T> {
     pub fn on_release_in(
         &mut self,
         window: &Window,
-        on_release: impl FnOnce(&mut T, AnyWindowHandle, &mut App) + 'static,
+        on_release: impl FnOnce(&mut T, &mut Window, &mut App) + 'static,
     ) -> Subscription {
-        let window_handle = window.handle;
-        let (subscription, activate) = self.release_listeners.insert(
-            self.entity_id(),
-            Box::new(move |this, cx| {
-                let this = this.downcast_mut().expect("invalid entity type");
-                on_release(this, window_handle, cx)
-            }),
-        );
-        activate();
-        subscription
+        let entity = self.entity();
+        self.app.observe_release_in(&entity, window, on_release)
     }
 
     /// Register a callback to be invoked when the given Model or View is released.
-    pub fn observe_release_in<V2>(
+    pub fn observe_release_in<T2>(
         &self,
-        observed: &Entity<V2>,
+        observed: &Entity<T2>,
         window: &Window,
-        mut on_release: impl FnMut(&mut T, &mut V2, &mut Window, &mut Context<'_, T>) + 'static,
+        mut on_release: impl FnMut(&mut T, &mut T2, &mut Window, &mut Context<'_, T>) + 'static,
     ) -> Subscription
     where
         T: 'static,
-        V2: 'static,
+        T2: 'static,
     {
         let observer = self.weak_entity();
-        let window_handle = window.handle;
-        let (subscription, activate) = self.release_listeners.insert(
-            observed.entity_id(),
-            Box::new(move |observed, cx| {
-                let observed = observed
-                    .downcast_mut()
-                    .expect("invalid observed entity type");
-                let _ = window_handle.update(cx, |_, window, cx| {
-                    observer.update(cx, |this, cx| on_release(this, observed, window, cx))
-                });
-            }),
-        );
-        activate();
-        subscription
+        self.app
+            .observe_release_in(observed, window, move |observed, window, cx| {
+                observer
+                    .update(cx, |observer, cx| {
+                        on_release(observer, observed, window, cx)
+                    })
+                    .ok();
+            })
     }
 
     /// Register a callback to be invoked when the window is resized.

crates/gpui/src/gpui.rs 🔗

@@ -129,7 +129,7 @@ pub use elements::*;
 pub use executor::*;
 pub use geometry::*;
 pub use global::*;
-pub use gpui_macros::{register_action, test, IntoElement, Render};
+pub use gpui_macros::{register_action, test, AppContext, IntoElement, Render, VisualContext};
 pub use http_client;
 pub use input::*;
 pub use interactive::*;

crates/gpui/src/platform.rs 🔗

@@ -1271,7 +1271,7 @@ impl ClipboardItem {
 
         for entry in self.entries.iter() {
             if let ClipboardEntry::String(ClipboardString { text, metadata: _ }) = entry {
-                answer.push_str(text);
+                answer.push_str(&text);
                 any_entries = true;
             }
         }

crates/gpui/src/window.rs 🔗

@@ -3,8 +3,8 @@ use crate::{
     AnyView, App, AppContext, Arena, Asset, AsyncWindowContext, AvailableSpace, Background, Bounds,
     BoxShadow, Context, Corners, CursorStyle, Decorations, DevicePixels, DispatchActionListener,
     DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter,
-    FileDropEvent, Flatten, FontId, Global, GlobalElementId, GlyphId, GpuSpecs, Hsla, InputHandler,
-    IsZero, KeyBinding, KeyContext, KeyDownEvent, KeyEvent, Keystroke, KeystrokeEvent, LayoutId,
+    FileDropEvent, FontId, Global, GlobalElementId, GlyphId, GpuSpecs, Hsla, InputHandler, IsZero,
+    KeyBinding, KeyContext, KeyDownEvent, KeyEvent, Keystroke, KeystrokeEvent, LayoutId,
     LineLayoutIndex, Modifiers, ModifiersChangedEvent, MonochromeSprite, MouseButton, MouseEvent,
     MouseMoveEvent, MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput,
     PlatformInputHandler, PlatformWindow, Point, PolychromeSprite, PromptLevel, Quad, Render,
@@ -677,6 +677,9 @@ pub(crate) struct ElementStateBox {
 fn default_bounds(display_id: Option<DisplayId>, cx: &mut App) -> Bounds<Pixels> {
     const DEFAULT_WINDOW_OFFSET: Point<Pixels> = point(px(0.), px(35.));
 
+    // TODO, BUG: if you open a window with the currently active window
+    // on the stack, this will erroneously select the 'unwrap_or_else'
+    // code path
     cx.active_window()
         .and_then(|w| w.update(cx, |_, window, _| window.bounds()).ok())
         .map(|mut bounds| {
@@ -3775,11 +3778,12 @@ impl<V: 'static + Render> WindowHandle<V> {
     /// Get the root view out of this window.
     ///
     /// This will fail if the window is closed or if the root view's type does not match `V`.
+    #[cfg(any(test, feature = "test-support"))]
     pub fn root<C>(&self, cx: &mut C) -> Result<Entity<V>>
     where
         C: AppContext,
     {
-        Flatten::flatten(cx.update_window(self.any_handle, |root_view, _, _| {
+        crate::Flatten::flatten(cx.update_window(self.any_handle, |root_view, _, _| {
             root_view
                 .downcast::<V>()
                 .map_err(|_| anyhow!("the type of the window's root view has changed"))

crates/gpui_macros/Cargo.toml 🔗

@@ -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"

crates/gpui_macros/src/derive_app_context.rs 🔗

@@ -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()
+}

crates/gpui_macros/src/derive_visual_context.rs 🔗

@@ -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()
+}

crates/gpui_macros/src/gpui_macros.rs 🔗

@@ -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,
+    }
+}

crates/gpui_macros/tests/derive_context.rs 🔗

@@ -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,
+    }
+}

crates/inline_completion_button/src/inline_completion_button.rs 🔗

@@ -91,17 +91,16 @@ impl Render for InlineCompletionButton {
                         IconButton::new("copilot-error", icon)
                             .icon_size(IconSize::Small)
                             .on_click(cx.listener(move |_, _, window, cx| {
-                                if let Some(workspace) =
-                                    window.window_handle().downcast::<Workspace>()
-                                {
-                                    workspace
-                                        .update(cx, |workspace, _, cx| {
-                                            workspace.show_toast(
-                                                Toast::new(
-                                                    NotificationId::unique::<CopilotErrorToast>(),
-                                                    format!("Copilot can't be started: {}", e),
-                                                )
-                                                .on_click("Reinstall Copilot", |_, cx| {
+                                if let Some(workspace) = window.root::<Workspace>().flatten() {
+                                    workspace.update(cx, |workspace, cx| {
+                                        workspace.show_toast(
+                                            Toast::new(
+                                                NotificationId::unique::<CopilotErrorToast>(),
+                                                format!("Copilot can't be started: {}", e),
+                                            )
+                                            .on_click(
+                                                "Reinstall Copilot",
+                                                |_, cx| {
                                                     if let Some(copilot) = Copilot::global(cx) {
                                                         copilot
                                                             .update(cx, |copilot, cx| {
@@ -109,11 +108,11 @@ impl Render for InlineCompletionButton {
                                                             })
                                                             .detach();
                                                     }
-                                                }),
-                                                cx,
-                                            );
-                                        })
-                                        .ok();
+                                                },
+                                            ),
+                                            cx,
+                                        );
+                                    });
                                 }
                             }))
                             .tooltip(|window, cx| {
@@ -398,19 +397,17 @@ impl InlineCompletionButton {
                 ),
                 None,
                 move |window, cx| {
-                    if let Some(workspace) = window.window_handle().downcast::<Workspace>() {
-                        if let Ok(workspace) = workspace.root(cx) {
-                            let workspace = workspace.downgrade();
-                            window
-                                .spawn(cx, |cx| {
-                                    configure_disabled_globs(
-                                        workspace,
-                                        path_enabled.then_some(path.clone()),
-                                        cx,
-                                    )
-                                })
-                                .detach_and_log_err(cx);
-                        }
+                    if let Some(workspace) = window.root().flatten() {
+                        let workspace = workspace.downgrade();
+                        window
+                            .spawn(cx, |cx| {
+                                configure_disabled_globs(
+                                    workspace,
+                                    path_enabled.then_some(path.clone()),
+                                    cx,
+                                )
+                            })
+                            .detach_and_log_err(cx);
                     }
                 },
             );

crates/prompt_library/src/prompt_library.rs 🔗

@@ -81,49 +81,62 @@ pub fn open_prompt_library(
     make_completion_provider: Arc<dyn Fn() -> Box<dyn CompletionProvider>>,
     cx: &mut App,
 ) -> Task<Result<WindowHandle<PromptLibrary>>> {
-    let existing_window = cx
-        .windows()
-        .into_iter()
-        .find_map(|window| window.downcast::<PromptLibrary>());
-    if let Some(existing_window) = existing_window {
-        existing_window
-            .update(cx, |_, window, _| window.activate_window())
-            .ok();
-        Task::ready(Ok(existing_window))
-    } else {
-        let store = PromptStore::global(cx);
-        cx.spawn(|cx| async move {
-            let store = store.await?;
-            cx.update(|cx| {
-                let app_id = ReleaseChannel::global(cx).app_id();
-                let bounds = Bounds::centered(None, size(px(1024.0), px(768.0)), cx);
-                cx.open_window(
-                    WindowOptions {
-                        titlebar: Some(TitlebarOptions {
-                            title: Some("Prompt Library".into()),
-                            appears_transparent: cfg!(target_os = "macos"),
-                            traffic_light_position: Some(point(px(9.0), px(9.0))),
-                        }),
-                        app_id: Some(app_id.to_owned()),
-                        window_bounds: Some(WindowBounds::Windowed(bounds)),
-                        ..Default::default()
-                    },
-                    |window, cx| {
-                        cx.new(|cx| {
-                            PromptLibrary::new(
-                                store,
-                                language_registry,
-                                inline_assist_delegate,
-                                make_completion_provider,
-                                window,
-                                cx,
-                            )
-                        })
-                    },
-                )
-            })?
-        })
-    }
+    let store = PromptStore::global(cx);
+    cx.spawn(|cx| async move {
+        // We query windows in spawn so that all windows have been returned to GPUI
+        let existing_window = cx
+            .update(|cx| {
+                let existing_window = cx
+                    .windows()
+                    .into_iter()
+                    .find_map(|window| window.downcast::<PromptLibrary>());
+                if let Some(existing_window) = existing_window {
+                    existing_window
+                        .update(cx, |_, window, _| window.activate_window())
+                        .ok();
+
+                    Some(existing_window)
+                } else {
+                    None
+                }
+            })
+            .ok()
+            .flatten();
+
+        if let Some(existing_window) = existing_window {
+            return Ok(existing_window);
+        }
+
+        let store = store.await?;
+        cx.update(|cx| {
+            let app_id = ReleaseChannel::global(cx).app_id();
+            let bounds = Bounds::centered(None, size(px(1024.0), px(768.0)), cx);
+            cx.open_window(
+                WindowOptions {
+                    titlebar: Some(TitlebarOptions {
+                        title: Some("Prompt Library".into()),
+                        appears_transparent: cfg!(target_os = "macos"),
+                        traffic_light_position: Some(point(px(9.0), px(9.0))),
+                    }),
+                    app_id: Some(app_id.to_owned()),
+                    window_bounds: Some(WindowBounds::Windowed(bounds)),
+                    ..Default::default()
+                },
+                |window, cx| {
+                    cx.new(|cx| {
+                        PromptLibrary::new(
+                            store,
+                            language_registry,
+                            inline_assist_delegate,
+                            make_completion_provider,
+                            window,
+                            cx,
+                        )
+                    })
+                },
+            )
+        })?
+    })
 }
 
 pub struct PromptLibrary {

crates/search/src/search.rs 🔗

@@ -126,16 +126,15 @@ pub(crate) fn show_no_more_matches(window: &mut Window, cx: &mut App) {
     window.defer(cx, |window, cx| {
         struct NotifType();
         let notification_id = NotificationId::unique::<NotifType>();
-        let Some(workspace) = window.window_handle().downcast::<Workspace>() else {
+
+        let Some(workspace) = window.root::<Workspace>().flatten() else {
             return;
         };
-        workspace
-            .update(cx, |workspace, _, cx| {
-                workspace.show_toast(
-                    Toast::new(notification_id.clone(), "No more matches").autohide(),
-                    cx,
-                );
-            })
-            .ok();
+        workspace.update(cx, |workspace, cx| {
+            workspace.show_toast(
+                Toast::new(notification_id.clone(), "No more matches").autohide(),
+                cx,
+            );
+        })
     });
 }

crates/workspace/src/notifications.rs 🔗

@@ -1,6 +1,4 @@
 use crate::{Toast, Workspace};
-use anyhow::Context as _;
-use anyhow::{anyhow, Result};
 use gpui::{
     svg, AnyView, App, AppContext as _, AsyncWindowContext, ClipboardItem, Context, DismissEvent,
     Entity, EventEmitter, Global, PromptLevel, Render, ScrollHandle, Task,
@@ -535,72 +533,61 @@ pub fn show_app_notification<V: Notification + 'static>(
     id: NotificationId,
     cx: &mut App,
     build_notification: impl Fn(&mut Context<Workspace>) -> Entity<V> + 'static,
-) -> Result<()> {
-    // Handle dismiss events by removing the notification from all workspaces.
-    let build_notification: Rc<dyn Fn(&mut Context<Workspace>) -> AnyView> = Rc::new({
-        let id = id.clone();
-        move |cx| {
-            let notification = build_notification(cx);
-            cx.subscribe(&notification, {
-                let id = id.clone();
-                move |_, _, _: &DismissEvent, cx| {
-                    dismiss_app_notification(&id, cx);
-                }
-            })
-            .detach();
-            notification.into()
-        }
-    });
-
-    // Store the notification so that new workspaces also receive it.
-    cx.global_mut::<GlobalAppNotifications>()
-        .insert(id.clone(), build_notification.clone());
-
-    let mut notify_errors = Vec::new();
+) {
+    // Defer notification creation so that windows on the stack can be returned to GPUI
+    cx.defer(move |cx| {
+        // Handle dismiss events by removing the notification from all workspaces.
+        let build_notification: Rc<dyn Fn(&mut Context<Workspace>) -> AnyView> = Rc::new({
+            let id = id.clone();
+            move |cx| {
+                let notification = build_notification(cx);
+                cx.subscribe(&notification, {
+                    let id = id.clone();
+                    move |_, _, _: &DismissEvent, cx| {
+                        dismiss_app_notification(&id, cx);
+                    }
+                })
+                .detach();
+                notification.into()
+            }
+        });
 
-    for window in cx.windows() {
-        if let Some(workspace_window) = window.downcast::<Workspace>() {
-            let notify_result = workspace_window.update(cx, |workspace, _window, cx| {
-                workspace.show_notification_without_handling_dismiss_events(&id, cx, |cx| {
-                    build_notification(cx)
-                });
-            });
-            match notify_result {
-                Ok(()) => {}
-                Err(notify_err) => notify_errors.push(notify_err),
+        // Store the notification so that new workspaces also receive it.
+        cx.global_mut::<GlobalAppNotifications>()
+            .insert(id.clone(), build_notification.clone());
+
+        for window in cx.windows() {
+            if let Some(workspace_window) = window.downcast::<Workspace>() {
+                workspace_window
+                    .update(cx, |workspace, _window, cx| {
+                        workspace.show_notification_without_handling_dismiss_events(
+                            &id,
+                            cx,
+                            |cx| build_notification(cx),
+                        );
+                    })
+                    .ok(); // Doesn't matter if the windows are dropped
             }
         }
-    }
-
-    if notify_errors.is_empty() {
-        Ok(())
-    } else {
-        Err(anyhow!(
-            "No workspaces were able to show notification. Errors:\n\n{}",
-            notify_errors
-                .iter()
-                .map(|e| e.to_string())
-                .collect::<Vec<_>>()
-                .join("\n\n")
-        ))
-    }
+    });
 }
 
 pub fn dismiss_app_notification(id: &NotificationId, cx: &mut App) {
-    cx.global_mut::<GlobalAppNotifications>().remove(id);
-    for window in cx.windows() {
-        if let Some(workspace_window) = window.downcast::<Workspace>() {
-            let id = id.clone();
-            // This spawn is necessary in order to dismiss the notification on which the click
-            // occurred, because in that case we're already in the middle of an update.
-            cx.spawn(move |mut cx| async move {
-                workspace_window.update(&mut cx, |workspace, _window, cx| {
-                    workspace.dismiss_notification(&id, cx)
-                })
-            })
-            .detach_and_log_err(cx);
+    let id = id.clone();
+    // Defer notification dismissal so that windows on the stack can be returned to GPUI
+    cx.defer(move |cx| {
+        cx.global_mut::<GlobalAppNotifications>().remove(&id);
+        for window in cx.windows() {
+            if let Some(workspace_window) = window.downcast::<Workspace>() {
+                let id = id.clone();
+                workspace_window
+                    .update(cx, |workspace, _window, cx| {
+                        workspace.dismiss_notification(&id, cx)
+                    })
+                    .ok();
+            }
         }
-    }
+    });
 }
 
 pub trait NotifyResultExt {
@@ -662,9 +649,8 @@ where
                             move |_cx| ErrorMessagePrompt::new(message)
                         })
                     }
-                })
-                .with_context(|| format!("Error while showing error notification: {message}"))
-                .log_err();
+                });
+
                 None
             }
         }

crates/workspace/src/workspace.rs 🔗

@@ -5607,36 +5607,6 @@ impl std::fmt::Debug for OpenPaths {
     }
 }
 
-pub fn activate_workspace_for_project(
-    cx: &mut App,
-    predicate: impl Fn(&Project, &App) -> bool + Send + 'static,
-) -> Option<WindowHandle<Workspace>> {
-    for window in cx.windows() {
-        let Some(workspace) = window.downcast::<Workspace>() else {
-            continue;
-        };
-
-        let predicate = workspace
-            .update(cx, |workspace, window, cx| {
-                let project = workspace.project.read(cx);
-                if predicate(project, cx) {
-                    window.activate_window();
-                    true
-                } else {
-                    false
-                }
-            })
-            .log_err()
-            .unwrap_or(false);
-
-        if predicate {
-            return Some(workspace);
-        }
-    }
-
-    None
-}
-
 pub async fn last_opened_workspace_location() -> Option<SerializedWorkspaceLocation> {
     DB.last_workspace().await.log_err().flatten()
 }

crates/zed/src/zed.rs 🔗

@@ -1200,8 +1200,7 @@ fn show_keymap_file_json_error(
                     cx.emit(DismissEvent);
                 })
         })
-    })
-    .log_err();
+    });
 }
 
 fn show_keymap_file_load_error(
@@ -1245,7 +1244,6 @@ fn show_keymap_file_load_error(
                     })
                 })
             })
-            .log_err();
         })
         .ok();
     })

crates/zed/src/zed/inline_completion_registry.rs 🔗

@@ -9,7 +9,6 @@ use gpui::{AnyWindowHandle, App, AppContext as _, Context, Entity, WeakEntity, W
 use language::language_settings::{all_language_settings, InlineCompletionProvider};
 use settings::SettingsStore;
 use supermaven::{Supermaven, SupermavenCompletionProvider};
-use workspace::Workspace;
 use zed_predict_tos::ZedPredictTos;
 
 pub fn init(client: Arc<Client>, user_store: Entity<UserStore>, cx: &mut App) {
@@ -115,8 +114,9 @@ pub fn init(client: Arc<Client>, user_store: Entity<UserStore>, cx: &mut App) {
                                 return;
                             };
 
-                            let Some(workspace) =
-                                window.downcast::<Workspace>().and_then(|w| w.root(cx).ok())
+                            let Some(Some(workspace)) = window
+                                .update(cx, |_, window, _| window.root().flatten())
+                                .ok()
                             else {
                                 return;
                             };