Strong app state (#52602)

Mikayla Maki created

This PR upgrades the workspace's app state to a strong pointer. It's an
app-global concern, and it should only die when the app dies. When the
app dies the process exits, so the cyclic reference problem isn't an
issue.

Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Release Notes:

- N/A

Change summary

crates/component_preview/examples/component_preview.rs         |   2 
crates/copilot_ui/src/sign_in.rs                               |  11 
crates/edit_prediction/src/edit_prediction.rs                  |   2 
crates/edit_prediction/src/edit_prediction_tests.rs            |   4 
crates/edit_prediction_ui/src/edit_prediction_button.rs        |   6 
crates/editor/src/editor.rs                                    |  40 
crates/recent_projects/src/remote_servers.rs                   |   1 
crates/recent_projects/src/wsl_picker.rs                       |   3 
crates/settings_ui/src/pages/edit_prediction_provider_setup.rs |   9 
crates/settings_ui/src/settings_ui.rs                          | 111 +-
crates/workspace/src/workspace.rs                              |  84 -
crates/zed/src/main.rs                                         |   6 
crates/zed/src/visual_test_runner.rs                           |   4 
crates/zed/src/zed.rs                                          | 167 +--
14 files changed, 202 insertions(+), 248 deletions(-)

Detailed changes

crates/component_preview/examples/component_preview.rs 🔗

@@ -65,7 +65,7 @@ fn main() {
             node_runtime,
             session,
         });
-        AppState::set_global(Arc::downgrade(&app_state), cx);
+        AppState::set_global(app_state.clone(), cx);
 
         workspace::init(app_state.clone(), cx);
         init(app_state.clone(), cx);

crates/copilot_ui/src/sign_in.rs 🔗

@@ -481,7 +481,6 @@ impl ConfigurationView {
         cx: &mut Context<Self>,
     ) -> Self {
         let copilot = AppState::try_global(cx)
-            .and_then(|state| state.upgrade())
             .and_then(|state| GlobalCopilotAuth::try_get_or_init(state, cx));
 
         Self {
@@ -578,9 +577,8 @@ impl ConfigurationView {
             )
             .when(edit_prediction, |this| this.tab_index(0isize))
             .on_click(|_, window, cx| {
-                if let Some(app_state) = AppState::global(cx).upgrade()
-                    && let Some(copilot) = GlobalCopilotAuth::try_get_or_init(app_state, cx)
-                {
+                let app_state = AppState::global(cx);
+                if let Some(copilot) = GlobalCopilotAuth::try_get_or_init(app_state, cx) {
                     initiate_sign_in(copilot.0, window, cx)
                 }
             })
@@ -608,9 +606,8 @@ impl ConfigurationView {
                     .color(Color::Muted),
             )
             .on_click(|_, window, cx| {
-                if let Some(app_state) = AppState::global(cx).upgrade()
-                    && let Some(copilot) = GlobalCopilotAuth::try_get_or_init(app_state, cx)
-                {
+                let app_state = AppState::global(cx);
+                if let Some(copilot) = GlobalCopilotAuth::try_get_or_init(app_state, cx) {
                     reinstall_and_sign_in(copilot.0, window, cx);
                 }
             })

crates/edit_prediction/src/edit_prediction.rs 🔗

@@ -1992,7 +1992,7 @@ impl EditPredictionStore {
 }
 
 fn currently_following(project: &Entity<Project>, cx: &App) -> bool {
-    let Some(app_state) = AppState::try_global(cx).and_then(|app_state| app_state.upgrade()) else {
+    let Some(app_state) = AppState::try_global(cx) else {
         return false;
     };
 

crates/edit_prediction/src/edit_prediction_tests.rs 🔗

@@ -204,7 +204,7 @@ async fn test_diagnostics_refresh_suppressed_while_following(cx: &mut TestAppCon
 
     let app_state = cx.update(|cx| {
         let app_state = AppState::test(cx);
-        AppState::set_global(Arc::downgrade(&app_state), cx);
+        AppState::set_global(app_state.clone(), cx);
         app_state
     });
 
@@ -214,7 +214,7 @@ async fn test_diagnostics_refresh_suppressed_while_following(cx: &mut TestAppCon
         .read_with(cx, |multi_workspace, _| multi_workspace.workspace().clone())
         .unwrap();
     cx.update(|cx| {
-        AppState::set_global(Arc::downgrade(workspace.read(cx).app_state()), cx);
+        AppState::set_global(workspace.read(cx).app_state().clone(), cx);
     });
     let _ = app_state;
 

crates/edit_prediction_ui/src/edit_prediction_button.rs 🔗

@@ -1418,9 +1418,9 @@ pub fn get_available_providers(cx: &mut App) -> Vec<EditPredictionProvider> {
 
     providers.push(EditPredictionProvider::Zed);
 
-    if let Some(app_state) = workspace::AppState::global(cx).upgrade()
-        && copilot::GlobalCopilotAuth::try_get_or_init(app_state, cx)
-            .is_some_and(|copilot| copilot.0.read(cx).is_authenticated())
+    let app_state = workspace::AppState::global(cx);
+    if copilot::GlobalCopilotAuth::try_get_or_init(app_state, cx)
+        .is_some_and(|copilot| copilot.0.read(cx).is_authenticated())
     {
         providers.push(EditPredictionProvider::Copilot);
     };

crates/editor/src/editor.rs 🔗

@@ -354,32 +354,26 @@ pub fn init(cx: &mut App) {
 
     cx.on_action(move |_: &workspace::NewFile, cx| {
         let app_state = workspace::AppState::global(cx);
-        if let Some(app_state) = app_state.upgrade() {
-            workspace::open_new(
-                Default::default(),
-                app_state,
-                cx,
-                |workspace, window, cx| {
-                    Editor::new_file(workspace, &Default::default(), window, cx)
-                },
-            )
-            .detach_and_log_err(cx);
-        }
+        workspace::open_new(
+            Default::default(),
+            app_state,
+            cx,
+            |workspace, window, cx| Editor::new_file(workspace, &Default::default(), window, cx),
+        )
+        .detach_and_log_err(cx);
     })
     .on_action(move |_: &workspace::NewWindow, cx| {
         let app_state = workspace::AppState::global(cx);
-        if let Some(app_state) = app_state.upgrade() {
-            workspace::open_new(
-                Default::default(),
-                app_state,
-                cx,
-                |workspace, window, cx| {
-                    cx.activate(true);
-                    Editor::new_file(workspace, &Default::default(), window, cx)
-                },
-            )
-            .detach_and_log_err(cx);
-        }
+        workspace::open_new(
+            Default::default(),
+            app_state,
+            cx,
+            |workspace, window, cx| {
+                cx.activate(true);
+                Editor::new_file(workspace, &Default::default(), window, cx)
+            },
+        )
+        .detach_and_log_err(cx);
     });
     _ = ui_input::ERASED_EDITOR_FACTORY.set(|window, cx| {
         Arc::new(ErasedEditorImpl(

crates/recent_projects/src/remote_servers.rs 🔗

@@ -1852,7 +1852,6 @@ impl RemoteServerProjects {
         cx: &mut Context<Self>,
     ) {
         let replace_window = window.window_handle().downcast::<MultiWorkspace>();
-
         let app_state = Arc::downgrade(&app_state);
         cx.spawn_in(window, async move |entity, cx| {
             let (connection, starting_dir) =

crates/recent_projects/src/wsl_picker.rs 🔗

@@ -235,9 +235,6 @@ impl WslOpenModal {
         cx: &mut Context<Self>,
     ) {
         let app_state = workspace::AppState::global(cx);
-        let Some(app_state) = app_state.upgrade() else {
-            return;
-        };
 
         let connection_options = RemoteConnectionOptions::Wsl(WslConnectionOptions {
             distro_name: distro.to_string(),

crates/settings_ui/src/pages/edit_prediction_provider_setup.rs 🔗

@@ -710,12 +710,9 @@ fn render_github_copilot_provider(window: &mut Window, cx: &mut App) -> Option<i
     let configuration_view = window.use_state(cx, |_, cx| {
         copilot_ui::ConfigurationView::new(
             move |cx| {
-                if let Some(app_state) = AppState::global(cx).upgrade() {
-                    copilot::GlobalCopilotAuth::try_get_or_init(app_state, cx)
-                        .is_some_and(|copilot| copilot.0.read(cx).is_authenticated())
-                } else {
-                    false
-                }
+                let app_state = AppState::global(cx);
+                copilot::GlobalCopilotAuth::try_get_or_init(app_state, cx)
+                    .is_some_and(|copilot| copilot.0.read(cx).is_authenticated())
             },
             copilot_ui::ConfigurationMode::EditPrediction,
             cx,

crates/settings_ui/src/settings_ui.rs 🔗

@@ -1394,17 +1394,14 @@ impl PartialEq for ActionLink {
 }
 
 fn all_language_names(cx: &App) -> Vec<SharedString> {
-    workspace::AppState::global(cx)
-        .upgrade()
-        .map_or(vec![], |state| {
-            state
-                .languages
-                .language_names()
-                .into_iter()
-                .filter(|name| name.as_ref() != "Zed Keybind Context")
-                .map(Into::into)
-                .collect()
-        })
+    let state = workspace::AppState::global(cx);
+    state
+        .languages
+        .language_names()
+        .into_iter()
+        .filter(|name| name.as_ref() != "Zed Keybind Context")
+        .map(Into::into)
+        .collect()
 }
 
 #[allow(unused)]
@@ -1535,29 +1532,26 @@ impl SettingsWindow {
         })
         .detach();
 
-        if let Some(app_state) = AppState::global(cx).upgrade() {
-            let workspaces: Vec<Entity<Workspace>> = app_state
-                .workspace_store
-                .read(cx)
-                .workspaces()
-                .filter_map(|weak| weak.upgrade())
-                .collect();
+        let app_state = AppState::global(cx);
+        let workspaces: Vec<Entity<Workspace>> = app_state
+            .workspace_store
+            .read(cx)
+            .workspaces()
+            .filter_map(|weak| weak.upgrade())
+            .collect();
 
-            for workspace in workspaces {
-                let project = workspace.read(cx).project().clone();
-                cx.observe_release_in(&project, window, |this, _, window, cx| {
-                    this.fetch_files(window, cx)
-                })
-                .detach();
-                cx.subscribe_in(&project, window, Self::handle_project_event)
-                    .detach();
-                cx.observe_release_in(&workspace, window, |this, _, window, cx| {
-                    this.fetch_files(window, cx)
-                })
+        for workspace in workspaces {
+            let project = workspace.read(cx).project().clone();
+            cx.observe_release_in(&project, window, |this, _, window, cx| {
+                this.fetch_files(window, cx)
+            })
+            .detach();
+            cx.subscribe_in(&project, window, Self::handle_project_event)
                 .detach();
-            }
-        } else {
-            log::error!("App state doesn't exist when creating a new settings window");
+            cx.observe_release_in(&workspace, window, |this, _, window, cx| {
+                this.fetch_files(window, cx)
+            })
+            .detach();
         }
 
         let this_weak = cx.weak_entity();
@@ -3362,9 +3356,7 @@ impl SettingsWindow {
             }
             SettingsUiFile::Project((worktree_id, path)) => {
                 let settings_path = path.join(paths::local_settings_file_relative_path());
-                let Some(app_state) = workspace::AppState::global(cx).upgrade() else {
-                    return;
-                };
+                let app_state = workspace::AppState::global(cx);
 
                 let Some((workspace_window, worktree, corresponding_workspace)) = app_state
                     .workspace_store
@@ -3745,31 +3737,26 @@ fn all_projects(
     cx: &App,
 ) -> impl Iterator<Item = Entity<Project>> {
     let mut seen_project_ids = std::collections::HashSet::new();
-    workspace::AppState::global(cx)
-        .upgrade()
-        .map(|app_state| {
-            app_state
-                .workspace_store
-                .read(cx)
-                .workspaces()
-                .filter_map(|weak| weak.upgrade())
-                .map(|workspace: Entity<Workspace>| workspace.read(cx).project().clone())
-                .chain(
-                    window
-                        .and_then(|handle| handle.read(cx).ok())
-                        .into_iter()
-                        .flat_map(|multi_workspace| {
-                            multi_workspace
-                                .workspaces()
-                                .iter()
-                                .map(|workspace| workspace.read(cx).project().clone())
-                                .collect::<Vec<_>>()
-                        }),
-                )
-                .filter(move |project| seen_project_ids.insert(project.entity_id()))
-        })
-        .into_iter()
-        .flatten()
+    let app_state = workspace::AppState::global(cx);
+    app_state
+        .workspace_store
+        .read(cx)
+        .workspaces()
+        .filter_map(|weak| weak.upgrade())
+        .map(|workspace: Entity<Workspace>| workspace.read(cx).project().clone())
+        .chain(
+            window
+                .and_then(|handle| handle.read(cx).ok())
+                .into_iter()
+                .flat_map(|multi_workspace| {
+                    multi_workspace
+                        .workspaces()
+                        .iter()
+                        .map(|workspace| workspace.read(cx).project().clone())
+                        .collect::<Vec<_>>()
+                }),
+        )
+        .filter(move |project| seen_project_ids.insert(project.entity_id()))
 }
 
 fn open_user_settings_in_workspace(
@@ -4723,7 +4710,7 @@ pub mod test {
 
         let app_state = cx.update(|cx| {
             let app_state = AppState::test(cx);
-            AppState::set_global(Arc::downgrade(&app_state), cx);
+            AppState::set_global(app_state.clone(), cx);
             app_state
         });
 
@@ -4897,7 +4884,7 @@ pub mod test {
 
         let app_state = cx.update(|cx| {
             let app_state = AppState::test(cx);
-            AppState::set_global(Arc::downgrade(&app_state), cx);
+            AppState::set_global(app_state.clone(), cx);
             app_state
         });
 

crates/workspace/src/workspace.rs 🔗

@@ -124,7 +124,7 @@ use std::{
     process::ExitStatus,
     rc::Rc,
     sync::{
-        Arc, LazyLock, Weak,
+        Arc, LazyLock,
         atomic::{AtomicBool, AtomicUsize},
     },
     time::Duration,
@@ -732,40 +732,32 @@ pub fn init(app_state: Arc<AppState>, cx: &mut App) {
 
     cx.on_action(|_: &CloseWindow, cx| Workspace::close_global(cx))
         .on_action(|_: &Reload, cx| reload(cx))
-        .on_action({
-            let app_state = Arc::downgrade(&app_state);
-            move |_: &Open, cx: &mut App| {
-                if let Some(app_state) = app_state.upgrade() {
-                    prompt_and_open_paths(
-                        app_state,
-                        PathPromptOptions {
-                            files: true,
-                            directories: true,
-                            multiple: true,
-                            prompt: None,
-                        },
-                        cx,
-                    );
-                }
-            }
+        .on_action(|_: &Open, cx: &mut App| {
+            let app_state = AppState::global(cx);
+            prompt_and_open_paths(
+                app_state,
+                PathPromptOptions {
+                    files: true,
+                    directories: true,
+                    multiple: true,
+                    prompt: None,
+                },
+                cx,
+            );
         })
-        .on_action({
-            let app_state = Arc::downgrade(&app_state);
-            move |_: &OpenFiles, cx: &mut App| {
-                let directories = cx.can_select_mixed_files_and_dirs();
-                if let Some(app_state) = app_state.upgrade() {
-                    prompt_and_open_paths(
-                        app_state,
-                        PathPromptOptions {
-                            files: true,
-                            directories,
-                            multiple: true,
-                            prompt: None,
-                        },
-                        cx,
-                    );
-                }
-            }
+        .on_action(|_: &OpenFiles, cx: &mut App| {
+            let directories = cx.can_select_mixed_files_and_dirs();
+            let app_state = AppState::global(cx);
+            prompt_and_open_paths(
+                app_state,
+                PathPromptOptions {
+                    files: true,
+                    directories,
+                    multiple: true,
+                    prompt: None,
+                },
+                cx,
+            );
         });
 }
 
@@ -1074,7 +1066,7 @@ pub struct AppState {
     pub session: Entity<AppSession>,
 }
 
-struct GlobalAppState(Weak<AppState>);
+struct GlobalAppState(Arc<AppState>);
 
 impl Global for GlobalAppState {}
 
@@ -1110,14 +1102,14 @@ struct Follower {
 
 impl AppState {
     #[track_caller]
-    pub fn global(cx: &App) -> Weak<Self> {
+    pub fn global(cx: &App) -> Arc<Self> {
         cx.global::<GlobalAppState>().0.clone()
     }
-    pub fn try_global(cx: &App) -> Option<Weak<Self>> {
+    pub fn try_global(cx: &App) -> Option<Arc<Self>> {
         cx.try_global::<GlobalAppState>()
             .map(|state| state.0.clone())
     }
-    pub fn set_global(state: Weak<AppState>, cx: &mut App) {
+    pub fn set_global(state: Arc<AppState>, cx: &mut App) {
         cx.set_global(GlobalAppState(state));
     }
 
@@ -10320,15 +10312,13 @@ pub fn with_active_or_new_workspace(
         }
         None => {
             let app_state = AppState::global(cx);
-            if let Some(app_state) = app_state.upgrade() {
-                open_new(
-                    OpenOptions::default(),
-                    app_state,
-                    cx,
-                    move |workspace, window, cx| f(workspace, window, cx),
-                )
-                .detach_and_log_err(cx);
-            }
+            open_new(
+                OpenOptions::default(),
+                app_state,
+                cx,
+                move |workspace, window, cx| f(workspace, window, cx),
+            )
+            .detach_and_log_err(cx);
         }
     }
 }

crates/zed/src/main.rs 🔗

@@ -441,10 +441,8 @@ fn main() {
         }
     });
     app.on_reopen(move |cx| {
-        if let Some(app_state) = AppState::try_global(cx).and_then(|app_state| app_state.upgrade())
-        {
+        if let Some(app_state) = AppState::try_global(cx) {
             cx.spawn({
-                let app_state = app_state;
                 async move |cx| {
                     if let Err(e) = restore_or_create_workspace(app_state, cx).await {
                         fail_to_open_window_async(e, cx)
@@ -626,7 +624,7 @@ fn main() {
             node_runtime,
             session: app_session,
         });
-        AppState::set_global(Arc::downgrade(&app_state), cx);
+        AppState::set_global(app_state.clone(), cx);
 
         auto_update::init(client.clone(), cx);
         dap_adapters::init(cx);

crates/zed/src/visual_test_runner.rs 🔗

@@ -170,7 +170,7 @@ fn run_visual_tests(project_path: PathBuf, update_baseline: bool) -> Result<()>
 
     // Set the global app state so settings_ui and other subsystems can find it
     cx.update(|cx| {
-        AppState::set_global(Arc::downgrade(&app_state), cx);
+        AppState::set_global(app_state.clone(), cx);
     });
 
     // Initialize all Zed subsystems
@@ -978,7 +978,7 @@ fn init_app_state(cx: &mut App) -> Arc<AppState> {
         build_window_options: |_, _| Default::default(),
         session,
     });
-    AppState::set_global(Arc::downgrade(&app_state), cx);
+    AppState::set_global(app_state.clone(), cx);
     app_state
 }
 

crates/zed/src/zed.rs 🔗

@@ -1072,104 +1072,99 @@ fn register_actions(
             },
         )
         .register_action({
-            let app_state = Arc::downgrade(&app_state);
+            let app_state = app_state.clone();
             move |_, _: &NewWindow, _, cx| {
-                if let Some(app_state) = app_state.upgrade() {
-                    open_new(
-                        Default::default(),
-                        app_state,
-                        cx,
-                        |workspace, window, cx| {
-                            cx.activate(true);
-                            // Create buffer synchronously to avoid flicker
-                            let project = workspace.project().clone();
-                            let buffer = project.update(cx, |project, cx| {
-                                project.create_local_buffer("", None, true, cx)
-                            });
-                            let editor = cx.new(|cx| {
-                                Editor::for_buffer(buffer, Some(project), window, cx)
-                            });
-                            workspace.add_item_to_active_pane(
-                                Box::new(editor),
-                                None,
-                                true,
-                                window,
-                                cx,
-                            );
-                        },
-                    )
-                    .detach();
-                }
+                open_new(
+                    Default::default(),
+                    app_state.clone(),
+                    cx,
+                    |workspace, window, cx| {
+                        cx.activate(true);
+                        // Create buffer synchronously to avoid flicker
+                        let project = workspace.project().clone();
+                        let buffer = project.update(cx, |project, cx| {
+                            project.create_local_buffer("", None, true, cx)
+                        });
+                        let editor = cx.new(|cx| {
+                            Editor::for_buffer(buffer, Some(project), window, cx)
+                        });
+                        workspace.add_item_to_active_pane(
+                            Box::new(editor),
+                            None,
+                            true,
+                            window,
+                            cx,
+                        );
+                    },
+                )
+                .detach();
             }
         })
         .register_action({
-            let app_state = Arc::downgrade(&app_state);
+            let app_state = app_state.clone();
             move |_workspace, _: &CloseProject, window, cx| {
                 let Some(window_handle) = window.window_handle().downcast::<MultiWorkspace>() else {
                     return;
                 };
-                if let Some(app_state) = app_state.upgrade() {
-                    cx.spawn_in(window, async move |this, cx| {
-                        let should_continue = this
-                            .update_in(cx, |workspace, window, cx| {
-                                workspace.prepare_to_close(
-                                    CloseIntent::ReplaceWindow,
-                                    window,
-                                    cx,
-                                )
-                            })?
-                            .await?;
-                        if should_continue {
-                            let task = cx.update(|_window, cx| {
-                                open_new(
-                                    workspace::OpenOptions {
-                                        replace_window: Some(window_handle),
-                                        ..Default::default()
-                                    },
-                                    app_state,
-                                    cx,
-                                    |workspace, window, cx| {
-                                        cx.activate(true);
-                                        let project = workspace.project().clone();
-                                        let buffer = project.update(cx, |project, cx| {
-                                            project.create_local_buffer("", None, true, cx)
-                                        });
-                                        let editor = cx.new(|cx| {
-                                            Editor::for_buffer(buffer, Some(project), window, cx)
-                                        });
-                                        workspace.add_item_to_active_pane(
-                                            Box::new(editor),
-                                            None,
-                                            true,
-                                            window,
-                                            cx,
-                                        );
-                                    },
-                                )
-                            })?;
-                            task.await
-                        } else {
-                            Ok(())
-                        }
-                    })
-                    .detach_and_log_err(cx);
-                }
+                let app_state = app_state.clone();
+                cx.spawn_in(window, async move |this, cx| {
+                    let should_continue = this
+                        .update_in(cx, |workspace, window, cx| {
+                            workspace.prepare_to_close(
+                                CloseIntent::ReplaceWindow,
+                                window,
+                                cx,
+                            )
+                        })?
+                        .await?;
+                    if should_continue {
+                        let task = cx.update(|_window, cx| {
+                            open_new(
+                                workspace::OpenOptions {
+                                    replace_window: Some(window_handle),
+                                    ..Default::default()
+                                },
+                                app_state,
+                                cx,
+                                |workspace, window, cx| {
+                                    cx.activate(true);
+                                    let project = workspace.project().clone();
+                                    let buffer = project.update(cx, |project, cx| {
+                                        project.create_local_buffer("", None, true, cx)
+                                    });
+                                    let editor = cx.new(|cx| {
+                                        Editor::for_buffer(buffer, Some(project), window, cx)
+                                    });
+                                    workspace.add_item_to_active_pane(
+                                        Box::new(editor),
+                                        None,
+                                        true,
+                                        window,
+                                        cx,
+                                    );
+                                },
+                            )
+                        })?;
+                        task.await
+                    } else {
+                        Ok(())
+                    }
+                })
+                .detach_and_log_err(cx);
             }
         })
         .register_action({
-            let app_state = Arc::downgrade(&app_state);
+            let app_state = app_state.clone();
             move |_, _: &NewFile, _, cx| {
-                if let Some(app_state) = app_state.upgrade() {
-                    open_new(
-                        Default::default(),
-                        app_state,
-                        cx,
-                        |workspace, window, cx| {
-                            Editor::new_file(workspace, &Default::default(), window, cx)
-                        },
-                    )
-                    .detach_and_log_err(cx);
-                }
+                open_new(
+                    Default::default(),
+                    app_state.clone(),
+                    cx,
+                    |workspace, window, cx| {
+                        Editor::new_file(workspace, &Default::default(), window, cx)
+                    },
+                )
+                .detach_and_log_err(cx);
             }
         });