gpui: Add Global marker trait (#7095)

Piotr Osiewicz , Marshall , and Marshall Bowers created

This should prevent a class of bugs where one queries the wrong type of
global, which results in oddities at runtime.

Release Notes:

- N/A

---------

Co-authored-by: Marshall <marshall@zed.dev>
Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>

Change summary

Cargo.lock                                     | 24 +++++
Cargo.toml                                     |  1 
crates/audio/src/assets.rs                     | 12 ++
crates/audio/src/audio.rs                      |  6 +
crates/auto_update/Cargo.toml                  |  1 
crates/auto_update/src/auto_update.rs          | 34 ++++----
crates/auto_update/src/update_notification.rs  |  4 
crates/call/src/call.rs                        | 17 +++-
crates/channel/Cargo.toml                      |  1 
crates/channel/src/channel_store.rs            | 14 ++-
crates/client/Cargo.toml                       |  1 
crates/client/src/client.rs                    | 24 ++++-
crates/client/src/telemetry.rs                 |  8 +-
crates/collections/src/collections.rs          |  9 --
crates/command_palette/Cargo.toml              |  3 
crates/command_palette/src/command_palette.rs  | 29 ++++--
crates/copilot/src/copilot.rs                  | 28 ++++++-
crates/db/Cargo.toml                           |  1 
crates/db/src/db.rs                            | 15 ++-
crates/editor/src/editor.rs                    |  5 
crates/editor/src/editor_tests.rs              |  8 +-
crates/editor/src/scroll.rs                    |  4 
crates/feature_flags/src/feature_flags.rs      |  4 
crates/feedback/Cargo.toml                     |  1 
crates/feedback/src/feedback_modal.rs          |  2 
crates/feedback/src/system_specs.rs            |  4 
crates/gpui/src/app.rs                         | 32 ++++----
crates/gpui/src/app/async_context.rs           | 14 +-
crates/gpui/src/app/model_context.rs           |  4 
crates/gpui/src/app/test_context.rs            | 14 +-
crates/gpui/src/elements/div.rs                | 12 +-
crates/gpui/src/gpui.rs                        |  9 +
crates/gpui/src/style.rs                       |  6 +
crates/gpui/src/window.rs                      | 10 +-
crates/notifications/src/notification_store.rs | 12 ++
crates/project_panel/src/file_associations.rs  |  4 
crates/release_channel/Cargo.toml              | 10 ++
crates/release_channel/LICENSE-GPL             |  1 
crates/release_channel/src/lib.rs              | 55 +++++++++++--
crates/search/src/project_search.rs            | 10 +-
crates/semantic_index/Cargo.toml               |  1 
crates/semantic_index/src/semantic_index.rs    | 19 +++-
crates/settings/Cargo.toml                     |  1 
crates/settings/src/settings_store.rs          | 18 +++-
crates/theme/src/registry.rs                   | 12 +-
crates/theme/src/settings.rs                   |  5 +
crates/theme/src/theme.rs                      |  2 
crates/theme_selector/src/theme_selector.rs    | 18 ----
crates/util/src/util.rs                        |  1 
crates/vim/Cargo.toml                          |  2 
crates/vim/src/vim.rs                          | 15 ++-
crates/workspace/Cargo.toml                    |  1 
crates/workspace/src/notifications.rs          |  4 
crates/workspace/src/workspace.rs              | 79 ++++++++++++++-----
crates/zed/Cargo.toml                          |  1 
crates/zed/src/main.rs                         | 19 ++--
crates/zed/src/only_instance.rs                |  8 +-
crates/zed/src/open_listener.rs                | 16 +++
crates/zed/src/zed.rs                          | 11 +-
59 files changed, 449 insertions(+), 237 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -661,6 +661,7 @@ dependencies = [
  "log",
  "menu",
  "project",
+ "release_channel",
  "schemars",
  "serde",
  "serde_derive",
@@ -1188,6 +1189,7 @@ dependencies = [
  "parking_lot 0.11.2",
  "postage",
  "rand 0.8.5",
+ "release_channel",
  "rpc",
  "schemars",
  "serde",
@@ -1361,6 +1363,7 @@ dependencies = [
  "parking_lot 0.11.2",
  "postage",
  "rand 0.8.5",
+ "release_channel",
  "rpc",
  "schemars",
  "serde",
@@ -1596,6 +1599,7 @@ dependencies = [
  "anyhow",
  "client",
  "collections",
+ "copilot",
  "ctor",
  "editor",
  "env_logger",
@@ -1606,6 +1610,7 @@ dependencies = [
  "menu",
  "picker",
  "project",
+ "release_channel",
  "serde",
  "serde_json",
  "settings",
@@ -2040,6 +2045,7 @@ dependencies = [
  "lazy_static",
  "log",
  "parking_lot 0.11.2",
+ "release_channel",
  "serde",
  "serde_derive",
  "smol",
@@ -2512,6 +2518,7 @@ dependencies = [
  "postage",
  "project",
  "regex",
+ "release_channel",
  "serde",
  "serde_derive",
  "serde_json",
@@ -4907,9 +4914,9 @@ dependencies = [
 
 [[package]]
 name = "once_cell"
-version = "1.18.0"
+version = "1.19.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
+checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
 
 [[package]]
 name = "opaque-debug"
@@ -6108,6 +6115,14 @@ version = "0.8.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
 
+[[package]]
+name = "release_channel"
+version = "0.1.0"
+dependencies = [
+ "gpui",
+ "once_cell",
+]
+
 [[package]]
 name = "rend"
 version = "0.4.0"
@@ -6849,6 +6864,7 @@ dependencies = [
  "pretty_assertions",
  "project",
  "rand 0.8.5",
+ "release_channel",
  "rpc",
  "rusqlite",
  "rust-embed",
@@ -6988,6 +7004,7 @@ dependencies = [
  "lazy_static",
  "postage",
  "pretty_assertions",
+ "release_channel",
  "rust-embed",
  "schemars",
  "serde",
@@ -9139,6 +9156,7 @@ dependencies = [
  "async-trait",
  "collections",
  "command_palette",
+ "copilot",
  "diagnostics",
  "editor",
  "futures 0.3.28",
@@ -9687,6 +9705,7 @@ dependencies = [
  "client",
  "collections",
  "db",
+ "derive_more",
  "env_logger",
  "fs",
  "futures 0.3.28",
@@ -9840,6 +9859,7 @@ dependencies = [
  "rand 0.8.5",
  "recent_projects",
  "regex",
+ "release_channel",
  "rope",
  "rpc",
  "rsa 0.4.0",

Cargo.toml 🔗

@@ -59,6 +59,7 @@ members = [
     "crates/project_symbols",
     "crates/quick_action_bar",
     "crates/recent_projects",
+    "crates/release_channel",
     "crates/rope",
     "crates/rpc",
     "crates/search",

crates/audio/src/assets.rs 🔗

@@ -2,7 +2,7 @@ use std::{io::Cursor, sync::Arc};
 
 use anyhow::Result;
 use collections::HashMap;
-use gpui::{AppContext, AssetSource};
+use gpui::{AppContext, AssetSource, Global};
 use rodio::{
     source::{Buffered, SamplesConverter},
     Decoder, Source,
@@ -15,6 +15,10 @@ pub struct SoundRegistry {
     assets: Box<dyn AssetSource>,
 }
 
+struct GlobalSoundRegistry(Arc<SoundRegistry>);
+
+impl Global for GlobalSoundRegistry {}
+
 impl SoundRegistry {
     pub fn new(source: impl AssetSource) -> Arc<Self> {
         Arc::new(Self {
@@ -24,7 +28,11 @@ impl SoundRegistry {
     }
 
     pub fn global(cx: &AppContext) -> Arc<Self> {
-        cx.global::<Arc<Self>>().clone()
+        cx.global::<GlobalSoundRegistry>().0.clone()
+    }
+
+    pub(crate) fn set_global(source: impl AssetSource, cx: &mut AppContext) {
+        cx.set_global(GlobalSoundRegistry(SoundRegistry::new(source)));
     }
 
     pub fn get(&self, name: &str) -> Result<impl Source<Item = f32>> {

crates/audio/src/audio.rs 🔗

@@ -1,12 +1,12 @@
 use assets::SoundRegistry;
-use gpui::{AppContext, AssetSource};
+use gpui::{AppContext, AssetSource, Global};
 use rodio::{OutputStream, OutputStreamHandle};
 use util::ResultExt;
 
 mod assets;
 
 pub fn init(source: impl AssetSource, cx: &mut AppContext) {
-    cx.set_global(SoundRegistry::new(source));
+    SoundRegistry::set_global(source, cx);
     cx.set_global(Audio::new());
 }
 
@@ -37,6 +37,8 @@ pub struct Audio {
     output_handle: Option<OutputStreamHandle>,
 }
 
+impl Global for Audio {}
+
 impl Audio {
     pub fn new() -> Self {
         Self {

crates/auto_update/Cargo.toml 🔗

@@ -15,6 +15,7 @@ client = { path = "../client" }
 gpui = { path = "../gpui" }
 menu = { path = "../menu" }
 project = { path = "../project" }
+release_channel = { path = "../release_channel" }
 settings = { path = "../settings" }
 theme = { path = "../theme" }
 workspace = { path = "../workspace" }

crates/auto_update/src/auto_update.rs 🔗

@@ -5,8 +5,8 @@ use client::{Client, TelemetrySettings, ZED_APP_PATH, ZED_APP_VERSION};
 use db::kvp::KEY_VALUE_STORE;
 use db::RELEASE_CHANNEL;
 use gpui::{
-    actions, AppContext, AsyncAppContext, Context as _, Model, ModelContext, SemanticVersion, Task,
-    ViewContext, VisualContext, WindowContext,
+    actions, AppContext, AsyncAppContext, Context as _, Global, Model, ModelContext,
+    SemanticVersion, Task, ViewContext, VisualContext, WindowContext,
 };
 use isahc::AsyncBody;
 
@@ -18,6 +18,7 @@ use smol::io::AsyncReadExt;
 use settings::{Settings, SettingsStore};
 use smol::{fs::File, process::Command};
 
+use release_channel::{AppCommitSha, ReleaseChannel};
 use std::{
     env::consts::{ARCH, OS},
     ffi::OsString,
@@ -25,11 +26,7 @@ use std::{
     time::Duration,
 };
 use update_notification::UpdateNotification;
-use util::http::HttpClient;
-use util::{
-    channel::{AppCommitSha, ReleaseChannel},
-    http::ZedHttpClient,
-};
+use util::http::{HttpClient, ZedHttpClient};
 use workspace::Workspace;
 
 const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification";
@@ -94,6 +91,11 @@ impl Settings for AutoUpdateSetting {
     }
 }
 
+#[derive(Default)]
+struct GlobalAutoUpdate(Option<Model<AutoUpdater>>);
+
+impl Global for GlobalAutoUpdate {}
+
 pub fn init(http_client: Arc<ZedHttpClient>, cx: &mut AppContext) {
     AutoUpdateSetting::register(cx);
 
@@ -127,7 +129,7 @@ pub fn init(http_client: Arc<ZedHttpClient>, cx: &mut AppContext) {
 
             updater
         });
-        cx.set_global(Some(auto_updater));
+        cx.set_global(GlobalAutoUpdate(Some(auto_updater)));
     }
 }
 
@@ -146,7 +148,7 @@ pub fn check(_: &Check, cx: &mut WindowContext) {
 
 pub fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) -> Option<()> {
     let auto_updater = AutoUpdater::get(cx)?;
-    let release_channel = cx.try_global::<ReleaseChannel>()?;
+    let release_channel = ReleaseChannel::try_global(cx)?;
 
     if matches!(
         release_channel,
@@ -191,7 +193,7 @@ pub fn notify_of_any_new_update(cx: &mut ViewContext<Workspace>) -> Option<()> {
 
 impl AutoUpdater {
     pub fn get(cx: &mut AppContext) -> Option<Model<Self>> {
-        cx.default_global::<Option<Model<Self>>>().clone()
+        cx.default_global::<GlobalAutoUpdate>().0.clone()
     }
 
     fn new(current_version: SemanticVersion, http_client: Arc<ZedHttpClient>) -> Self {
@@ -253,8 +255,7 @@ impl AutoUpdater {
             OS, ARCH
         ));
         cx.update(|cx| {
-            if let Some(param) = cx
-                .try_global::<ReleaseChannel>()
+            if let Some(param) = ReleaseChannel::try_global(cx)
                 .map(|release_channel| release_channel.release_query_param())
                 .flatten()
             {
@@ -276,7 +277,9 @@ impl AutoUpdater {
 
         let should_download = match *RELEASE_CHANNEL {
             ReleaseChannel::Nightly => cx
-                .try_read_global::<AppCommitSha, _>(|sha, _| release.version != sha.0)
+                .update(|cx| AppCommitSha::try_global(cx).map(|sha| release.version != sha.0))
+                .ok()
+                .flatten()
                 .unwrap_or(true),
             _ => release.version.parse::<SemanticVersion>()? > current_version,
         };
@@ -311,9 +314,8 @@ impl AutoUpdater {
         let mut dmg_file = File::create(&dmg_path).await?;
 
         let (installation_id, release_channel, telemetry) = cx.update(|cx| {
-            let installation_id = cx.global::<Arc<Client>>().telemetry().installation_id();
-            let release_channel = cx
-                .try_global::<ReleaseChannel>()
+            let installation_id = Client::global(cx).telemetry().installation_id();
+            let release_channel = ReleaseChannel::try_global(cx)
                 .map(|release_channel| release_channel.display_name());
             let telemetry = TelemetrySettings::get_global(cx).metrics;
 

crates/auto_update/src/update_notification.rs 🔗

@@ -3,7 +3,7 @@ use gpui::{
     SemanticVersion, StatefulInteractiveElement, Styled, ViewContext,
 };
 use menu::Cancel;
-use util::channel::ReleaseChannel;
+use release_channel::ReleaseChannel;
 use workspace::ui::{h_flex, v_flex, Icon, IconName, Label, StyledExt};
 
 pub struct UpdateNotification {
@@ -14,7 +14,7 @@ impl EventEmitter<DismissEvent> for UpdateNotification {}
 
 impl Render for UpdateNotification {
     fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
-        let app_name = cx.global::<ReleaseChannel>().display_name();
+        let app_name = ReleaseChannel::global(cx).display_name();
 
         v_flex()
             .on_action(cx.listener(UpdateNotification::dismiss))

crates/call/src/call.rs 🔗

@@ -9,8 +9,8 @@ use client::{proto, Client, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE};
 use collections::HashSet;
 use futures::{channel::oneshot, future::Shared, Future, FutureExt};
 use gpui::{
-    AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Subscription, Task,
-    WeakModel,
+    AppContext, AsyncAppContext, Context, EventEmitter, Global, Model, ModelContext, Subscription,
+    Task, WeakModel,
 };
 use postage::watch;
 use project::Project;
@@ -21,11 +21,15 @@ use std::sync::Arc;
 pub use participant::ParticipantLocation;
 pub use room::Room;
 
+struct GlobalActiveCall(Model<ActiveCall>);
+
+impl Global for GlobalActiveCall {}
+
 pub fn init(client: Arc<Client>, user_store: Model<UserStore>, cx: &mut AppContext) {
     CallSettings::register(cx);
 
     let active_call = cx.new_model(|cx| ActiveCall::new(client, user_store, cx));
-    cx.set_global(active_call);
+    cx.set_global(GlobalActiveCall(active_call));
 }
 
 pub struct OneAtATime {
@@ -154,7 +158,12 @@ impl ActiveCall {
     }
 
     pub fn global(cx: &AppContext) -> Model<Self> {
-        cx.global::<Model<Self>>().clone()
+        cx.global::<GlobalActiveCall>().0.clone()
+    }
+
+    pub fn try_global(cx: &AppContext) -> Option<Model<Self>> {
+        cx.try_global::<GlobalActiveCall>()
+            .map(|call| call.0.clone())
     }
 
     pub fn invite(

crates/channel/Cargo.toml 🔗

@@ -21,6 +21,7 @@ util = { path = "../util" }
 rpc = { path = "../rpc" }
 text = { path = "../text" }
 language = { path = "../language" }
+release_channel = { path = "../release_channel" }
 settings = { path = "../settings" }
 feature_flags = { path = "../feature_flags" }
 sum_tree = { path = "../sum_tree" }

crates/channel/src/channel_store.rs 🔗

@@ -5,13 +5,13 @@ use anyhow::{anyhow, Result};
 use channel_index::ChannelIndex;
 use client::{Client, Subscription, User, UserId, UserStore};
 use collections::{hash_map, HashMap, HashSet};
-use db::RELEASE_CHANNEL;
 use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt};
 use gpui::{
-    AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, SharedString, Task,
-    WeakModel,
+    AppContext, AsyncAppContext, Context, EventEmitter, Global, Model, ModelContext, SharedString,
+    Task, WeakModel,
 };
 use language::Capability;
+use release_channel::RELEASE_CHANNEL;
 use rpc::{
     proto::{self, ChannelRole, ChannelVisibility},
     TypedEnvelope,
@@ -22,7 +22,7 @@ use util::{async_maybe, maybe, ResultExt};
 pub fn init(client: &Arc<Client>, user_store: Model<UserStore>, cx: &mut AppContext) {
     let channel_store =
         cx.new_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
-    cx.set_global(channel_store);
+    cx.set_global(GlobalChannelStore(channel_store));
 }
 
 pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
@@ -143,9 +143,13 @@ enum OpenedModelHandle<E> {
     Loading(Shared<Task<Result<Model<E>, Arc<anyhow::Error>>>>),
 }
 
+struct GlobalChannelStore(Model<ChannelStore>);
+
+impl Global for GlobalChannelStore {}
+
 impl ChannelStore {
     pub fn global(cx: &AppContext) -> Model<Self> {
-        cx.global::<Model<Self>>().clone()
+        cx.global::<GlobalChannelStore>().0.clone()
     }
 
     pub fn new(

crates/client/Cargo.toml 🔗

@@ -18,6 +18,7 @@ collections = { path = "../collections" }
 db = { path = "../db" }
 gpui = { path = "../gpui" }
 util = { path = "../util" }
+release_channel = { path = "../release_channel" }
 rpc = { path = "../rpc" }
 text = { path = "../text" }
 settings = { path = "../settings" }

crates/client/src/client.rs 🔗

@@ -15,13 +15,14 @@ use futures::{
     TryFutureExt as _, TryStreamExt,
 };
 use gpui::{
-    actions, AnyModel, AnyWeakModel, AppContext, AsyncAppContext, Model, SemanticVersion, Task,
-    WeakModel,
+    actions, AnyModel, AnyWeakModel, AppContext, AsyncAppContext, Global, Model, SemanticVersion,
+    Task, WeakModel,
 };
 use lazy_static::lazy_static;
 use parking_lot::RwLock;
 use postage::watch;
 use rand::prelude::*;
+use release_channel::ReleaseChannel;
 use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, PeerId, RequestMessage};
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
@@ -41,8 +42,7 @@ use std::{
 use telemetry::Telemetry;
 use thiserror::Error;
 use url::Url;
-use util::http::HttpClient;
-use util::{channel::ReleaseChannel, http::ZedHttpClient};
+use util::http::{HttpClient, ZedHttpClient};
 use util::{ResultExt, TryFutureExt};
 
 pub use rpc::*;
@@ -149,6 +149,10 @@ pub fn init(client: &Arc<Client>, cx: &mut AppContext) {
     });
 }
 
+struct GlobalClient(Arc<Client>);
+
+impl Global for GlobalClient {}
+
 pub struct Client {
     id: AtomicU64,
     peer: Arc<Peer>,
@@ -483,6 +487,13 @@ impl Client {
         self
     }
 
+    pub fn global(cx: &AppContext) -> Arc<Self> {
+        cx.global::<GlobalClient>().0.clone()
+    }
+    pub fn set_global(client: Arc<Client>, cx: &mut AppContext) {
+        cx.set_global(GlobalClient(client))
+    }
+
     pub fn user_id(&self) -> Option<u64> {
         self.state
             .read()
@@ -996,7 +1007,10 @@ impl Client {
         credentials: &Credentials,
         cx: &AsyncAppContext,
     ) -> Task<Result<Connection, EstablishConnectionError>> {
-        let release_channel = cx.try_read_global(|channel: &ReleaseChannel, _| *channel);
+        let release_channel = cx
+            .update(|cx| ReleaseChannel::try_global(cx))
+            .ok()
+            .flatten();
 
         let request = Request::builder()
             .header(

crates/client/src/telemetry.rs 🔗

@@ -5,6 +5,7 @@ use chrono::{DateTime, Utc};
 use futures::Future;
 use gpui::{AppContext, AppMetadata, BackgroundExecutor, Task};
 use parking_lot::Mutex;
+use release_channel::ReleaseChannel;
 use serde::Serialize;
 use settings::{Settings, SettingsStore};
 use std::{env, io::Write, mem, path::PathBuf, sync::Arc, time::Duration};
@@ -15,7 +16,7 @@ use tempfile::NamedTempFile;
 use util::http::{HttpClient, ZedHttpClient};
 #[cfg(not(debug_assertions))]
 use util::ResultExt;
-use util::{channel::ReleaseChannel, TryFutureExt};
+use util::TryFutureExt;
 
 use self::event_coalescer::EventCoalescer;
 
@@ -143,9 +144,8 @@ const FLUSH_INTERVAL: Duration = Duration::from_secs(60 * 5);
 
 impl Telemetry {
     pub fn new(client: Arc<ZedHttpClient>, cx: &mut AppContext) -> Arc<Self> {
-        let release_channel = cx
-            .try_global::<ReleaseChannel>()
-            .map(|release_channel| release_channel.display_name());
+        let release_channel =
+            ReleaseChannel::try_global(cx).map(|release_channel| release_channel.display_name());
 
         TelemetrySettings::register(cx);
 

crates/collections/src/collections.rs 🔗

@@ -11,13 +11,4 @@ pub type HashMap<K, V> = std::collections::HashMap<K, V>;
 pub type HashSet<T> = std::collections::HashSet<T>;
 
 pub use rustc_hash::{FxHashMap, FxHashSet};
-use std::any::TypeId;
 pub use std::collections::*;
-
-// NEW TYPES
-
-#[derive(Default)]
-pub struct CommandPaletteFilter {
-    pub hidden_namespaces: HashSet<&'static str>,
-    pub hidden_action_types: HashSet<TypeId>,
-}

crates/command_palette/Cargo.toml 🔗

@@ -12,11 +12,14 @@ doctest = false
 [dependencies]
 client = { path = "../client" }
 collections = { path = "../collections" }
+# HACK: We're only depending on `copilot` here for `CommandPaletteFilter`.  See the attached comment on that type.
+copilot = { path = "../copilot" }
 editor = { path = "../editor" }
 fuzzy = {  path = "../fuzzy" }
 gpui = { path = "../gpui" }
 picker = { path = "../picker" }
 project = { path = "../project" }
+release_channel = { path = "../release_channel" }
 settings = { path = "../settings" }
 theme = { path = "../theme" }
 ui = { path = "../ui" }

crates/command_palette/src/command_palette.rs 🔗

@@ -4,19 +4,18 @@ use std::{
 };
 
 use client::telemetry::Telemetry;
-use collections::{CommandPaletteFilter, HashMap};
+use collections::HashMap;
+use copilot::CommandPaletteFilter;
 use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{
-    actions, Action, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView,
+    actions, Action, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Global,
     ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView,
 };
 use picker::{Picker, PickerDelegate};
 
+use release_channel::{parse_zed_link, ReleaseChannel};
 use ui::{h_flex, prelude::*, v_flex, HighlightedLabel, KeyBinding, ListItem, ListItemSpacing};
-use util::{
-    channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL},
-    ResultExt,
-};
+use util::ResultExt;
 use workspace::{ModalView, Workspace};
 use zed_actions::OpenZedUrl;
 
@@ -100,8 +99,11 @@ impl Render for CommandPalette {
     }
 }
 
-pub type CommandPaletteInterceptor =
-    Box<dyn Fn(&str, &AppContext) -> Option<CommandInterceptResult>>;
+pub struct CommandPaletteInterceptor(
+    pub Box<dyn Fn(&str, &AppContext) -> Option<CommandInterceptResult>>,
+);
+
+impl Global for CommandPaletteInterceptor {}
 
 pub struct CommandInterceptResult {
     pub action: Box<dyn Action>,
@@ -139,6 +141,8 @@ impl Clone for Command {
 #[derive(Default)]
 struct HitCounts(HashMap<String, usize>);
 
+impl Global for HitCounts {}
+
 impl CommandPaletteDelegate {
     fn new(
         command_palette: WeakView<CommandPalette>,
@@ -229,11 +233,14 @@ impl PickerDelegate for CommandPaletteDelegate {
 
             let mut intercept_result = cx
                 .try_read_global(|interceptor: &CommandPaletteInterceptor, cx| {
-                    (interceptor)(&query, cx)
+                    (interceptor.0)(&query, cx)
                 })
                 .flatten();
-
-            if *RELEASE_CHANNEL == ReleaseChannel::Dev {
+            let release_channel = cx
+                .update(|cx| ReleaseChannel::try_global(cx))
+                .ok()
+                .flatten();
+            if release_channel == Some(ReleaseChannel::Dev) {
                 if parse_zed_link(&query).is_some() {
                     intercept_result = Some(CommandInterceptResult {
                         action: OpenZedUrl { url: query.clone() }.boxed_clone(),

crates/copilot/src/copilot.rs 🔗

@@ -5,7 +5,7 @@ use async_tar::Archive;
 use collections::{HashMap, HashSet};
 use futures::{channel::oneshot, future::Shared, Future, FutureExt, TryFutureExt};
 use gpui::{
-    actions, AppContext, AsyncAppContext, Context, Entity, EntityId, EventEmitter, Model,
+    actions, AppContext, AsyncAppContext, Context, Entity, EntityId, EventEmitter, Global, Model,
     ModelContext, Task, WeakModel,
 };
 use language::{
@@ -32,6 +32,17 @@ use util::{
     ResultExt,
 };
 
+// HACK: This type is only defined in `copilot` since it is the earliest ancestor
+// of the crates that use it.
+//
+// This is not great. Let's find a better place for it to live.
+#[derive(Default)]
+pub struct CommandPaletteFilter {
+    pub hidden_namespaces: HashSet<&'static str>,
+    pub hidden_action_types: HashSet<TypeId>,
+}
+
+impl Global for CommandPaletteFilter {}
 actions!(
     copilot,
     [
@@ -54,7 +65,7 @@ pub fn init(
         let node_runtime = node_runtime.clone();
         move |cx| Copilot::start(new_server_id, http, node_runtime, cx)
     });
-    cx.set_global(copilot.clone());
+    Copilot::set_global(copilot.clone(), cx);
     cx.observe(&copilot, |handle, cx| {
         let copilot_action_types = [
             TypeId::of::<Suggest>(),
@@ -65,7 +76,7 @@ pub fn init(
         let copilot_auth_action_types = [TypeId::of::<SignOut>()];
         let copilot_no_auth_action_types = [TypeId::of::<SignIn>()];
         let status = handle.read(cx).status();
-        let filter = cx.default_global::<collections::CommandPaletteFilter>();
+        let filter = cx.default_global::<CommandPaletteFilter>();
 
         match status {
             Status::Disabled => {
@@ -307,9 +318,18 @@ pub enum Event {
 
 impl EventEmitter<Event> for Copilot {}
 
+struct GlobalCopilot(Model<Copilot>);
+
+impl Global for GlobalCopilot {}
+
 impl Copilot {
     pub fn global(cx: &AppContext) -> Option<Model<Self>> {
-        cx.try_global::<Model<Self>>().map(|model| model.clone())
+        cx.try_global::<GlobalCopilot>()
+            .map(|model| model.0.clone())
+    }
+
+    pub fn set_global(copilot: Model<Self>, cx: &mut AppContext) {
+        cx.set_global(GlobalCopilot(copilot));
     }
 
     fn start(

crates/db/Cargo.toml 🔗

@@ -15,6 +15,7 @@ test-support = []
 [dependencies]
 collections = { path = "../collections" }
 gpui = { path = "../gpui" }
+release_channel = { path = "../release_channel" }
 sqlez = { path = "../sqlez" }
 sqlez_macros = { path = "../sqlez_macros" }
 util = { path = "../util" }

crates/db/src/db.rs 🔗

@@ -10,16 +10,16 @@ pub use lazy_static;
 pub use smol;
 pub use sqlez;
 pub use sqlez_macros;
-pub use util::channel::{RELEASE_CHANNEL, RELEASE_CHANNEL_NAME};
 pub use util::paths::DB_DIR;
 
+use release_channel::ReleaseChannel;
+pub use release_channel::RELEASE_CHANNEL;
 use sqlez::domain::Migrator;
 use sqlez::thread_safe_connection::ThreadSafeConnection;
 use sqlez_macros::sql;
 use std::future::Future;
 use std::path::{Path, PathBuf};
 use std::sync::atomic::{AtomicBool, Ordering};
-use util::channel::ReleaseChannel;
 use util::{async_maybe, ResultExt};
 
 const CONNECTION_INITIALIZE_QUERY: &'static str = sql!(
@@ -223,7 +223,7 @@ mod tests {
             .prefix("DbTests")
             .tempdir()
             .unwrap();
-        let _bad_db = open_db::<BadDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
+        let _bad_db = open_db::<BadDB>(tempdir.path(), &release_channel::ReleaseChannel::Dev).await;
     }
 
     /// Test that DB exists but corrupted (causing recreate)
@@ -261,11 +261,12 @@ mod tests {
             .unwrap();
         {
             let corrupt_db =
-                open_db::<CorruptedDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
+                open_db::<CorruptedDB>(tempdir.path(), &release_channel::ReleaseChannel::Dev).await;
             assert!(corrupt_db.persistent());
         }
 
-        let good_db = open_db::<GoodDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
+        let good_db =
+            open_db::<GoodDB>(tempdir.path(), &release_channel::ReleaseChannel::Dev).await;
         assert!(
             good_db.select_row::<usize>("SELECT * FROM test2").unwrap()()
                 .unwrap()
@@ -309,7 +310,7 @@ mod tests {
         {
             // Setup the bad database
             let corrupt_db =
-                open_db::<CorruptedDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
+                open_db::<CorruptedDB>(tempdir.path(), &release_channel::ReleaseChannel::Dev).await;
             assert!(corrupt_db.persistent());
         }
 
@@ -320,7 +321,7 @@ mod tests {
             let guard = thread::spawn(move || {
                 let good_db = smol::block_on(open_db::<GoodDB>(
                     tmp_path.as_path(),
-                    &util::channel::ReleaseChannel::Dev,
+                    &release_channel::ReleaseChannel::Dev,
                 ));
                 assert!(
                     good_db.select_row::<usize>("SELECT * FROM test2").unwrap()()

crates/editor/src/editor.rs 🔗

@@ -104,7 +104,6 @@ use std::{
     ops::{ControlFlow, Deref, DerefMut, Range, RangeInclusive},
     path::Path,
     sync::Arc,
-    sync::Weak,
     time::{Duration, Instant},
 };
 pub use sum_tree::Bias;
@@ -241,7 +240,7 @@ pub fn init(cx: &mut AppContext) {
     .detach();
 
     cx.on_action(move |_: &workspace::NewFile, cx| {
-        let app_state = cx.global::<Weak<workspace::AppState>>();
+        let app_state = workspace::AppState::global(cx);
         if let Some(app_state) = app_state.upgrade() {
             workspace::open_new(&app_state, cx, |workspace, cx| {
                 Editor::new_file(workspace, &Default::default(), cx)
@@ -250,7 +249,7 @@ pub fn init(cx: &mut AppContext) {
         }
     });
     cx.on_action(move |_: &workspace::NewWindow, cx| {
-        let app_state = cx.global::<Weak<workspace::AppState>>();
+        let app_state = workspace::AppState::global(cx);
         if let Some(app_state) = app_state.upgrade() {
             workspace::open_new(&app_state, cx, |workspace, cx| {
                 Editor::new_file(workspace, &Default::default(), cx)

crates/editor/src/editor_tests.rs 🔗

@@ -7226,7 +7226,7 @@ async fn test_copilot(executor: BackgroundExecutor, cx: &mut gpui::TestAppContex
     init_test(cx, |_| {});
 
     let (copilot, copilot_lsp) = Copilot::fake(cx);
-    _ = cx.update(|cx| cx.set_global(copilot));
+    _ = cx.update(|cx| Copilot::set_global(copilot, cx));
     let mut cx = EditorLspTestContext::new_rust(
         lsp::ServerCapabilities {
             completion_provider: Some(lsp::CompletionOptions {
@@ -7479,7 +7479,7 @@ async fn test_copilot_completion_invalidation(
     init_test(cx, |_| {});
 
     let (copilot, copilot_lsp) = Copilot::fake(cx);
-    _ = cx.update(|cx| cx.set_global(copilot));
+    _ = cx.update(|cx| Copilot::set_global(copilot, cx));
     let mut cx = EditorLspTestContext::new_rust(
         lsp::ServerCapabilities {
             completion_provider: Some(lsp::CompletionOptions {
@@ -7543,7 +7543,7 @@ async fn test_copilot_multibuffer(executor: BackgroundExecutor, cx: &mut gpui::T
     init_test(cx, |_| {});
 
     let (copilot, copilot_lsp) = Copilot::fake(cx);
-    _ = cx.update(|cx| cx.set_global(copilot));
+    _ = cx.update(|cx| Copilot::set_global(copilot, cx));
 
     let buffer_1 = cx.new_model(|cx| {
         Buffer::new(
@@ -7660,7 +7660,7 @@ async fn test_copilot_disabled_globs(executor: BackgroundExecutor, cx: &mut gpui
     });
 
     let (copilot, copilot_lsp) = Copilot::fake(cx);
-    _ = cx.update(|cx| cx.set_global(copilot));
+    _ = cx.update(|cx| Copilot::set_global(copilot, cx));
 
     let fs = FakeFs::new(cx.executor());
     fs.insert_tree(

crates/editor/src/scroll.rs 🔗

@@ -10,7 +10,7 @@ use crate::{
     MultiBufferSnapshot, ToPoint,
 };
 pub use autoscroll::{Autoscroll, AutoscrollStrategy};
-use gpui::{point, px, AppContext, Entity, Pixels, Task, ViewContext};
+use gpui::{point, px, AppContext, Entity, Global, Pixels, Task, ViewContext};
 use language::{Bias, Point};
 pub use scroll_amount::ScrollAmount;
 use std::{
@@ -27,6 +27,8 @@ const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
 #[derive(Default)]
 pub struct ScrollbarAutoHide(pub bool);
 
+impl Global for ScrollbarAutoHide {}
+
 #[derive(Clone, Copy, Debug, PartialEq)]
 pub struct ScrollAnchor {
     pub offset: gpui::Point<f32>,

crates/feature_flags/src/feature_flags.rs 🔗

@@ -1,4 +1,4 @@
-use gpui::{AppContext, Subscription, ViewContext};
+use gpui::{AppContext, Global, Subscription, ViewContext};
 
 #[derive(Default)]
 struct FeatureFlags {
@@ -12,6 +12,8 @@ impl FeatureFlags {
     }
 }
 
+impl Global for FeatureFlags {}
+
 pub trait FeatureFlag {
     const NAME: &'static str;
 }

crates/feedback/Cargo.toml 🔗

@@ -19,6 +19,7 @@ gpui = { path = "../gpui" }
 language = { path = "../language" }
 menu = { path = "../menu" }
 project = { path = "../project" }
+release_channel = { path = "../release_channel" }
 settings = { path = "../settings" }
 theme = { path = "../theme" }
 ui = { path = "../ui" }

crates/feedback/src/feedback_modal.rs 🔗

@@ -225,7 +225,7 @@ impl FeedbackModal {
             None,
             &["Yes, Submit!", "No"],
         );
-        let client = cx.global::<Arc<Client>>().clone();
+        let client = Client::global(cx).clone();
         let specs = self.system_specs.clone();
         cx.spawn(|this, mut cx| async move {
             let answer = answer.await.ok();

crates/feedback/src/system_specs.rs 🔗

@@ -1,10 +1,10 @@
 use client::ZED_APP_VERSION;
 use gpui::AppContext;
 use human_bytes::human_bytes;
+use release_channel::ReleaseChannel;
 use serde::Serialize;
 use std::{env, fmt::Display};
 use sysinfo::{RefreshKind, System, SystemExt};
-use util::channel::ReleaseChannel;
 
 #[derive(Clone, Debug, Serialize)]
 pub struct SystemSpecs {
@@ -21,7 +21,7 @@ impl SystemSpecs {
         let app_version = ZED_APP_VERSION
             .or_else(|| cx.app_metadata().app_version)
             .map(|v| v.to_string());
-        let release_channel = cx.global::<ReleaseChannel>().display_name();
+        let release_channel = ReleaseChannel::global(cx).display_name();
         let os_name = cx.app_metadata().os_name;
         let system = System::new_with_specifics(RefreshKind::new().with_memory());
         let memory = system.total_memory();

crates/gpui/src/app.rs 🔗

@@ -17,7 +17,7 @@ use time::UtcOffset;
 use crate::{
     current_platform, image_cache::ImageCache, init_app_menus, Action, ActionRegistry, Any,
     AnyView, AnyWindowHandle, AppMetadata, AssetSource, BackgroundExecutor, ClipboardItem, Context,
-    DispatchPhase, DisplayId, Entity, EventEmitter, ForegroundExecutor, KeyBinding, Keymap,
+    DispatchPhase, DisplayId, Entity, EventEmitter, ForegroundExecutor, Global, KeyBinding, Keymap,
     Keystroke, LayoutId, Menu, PathPromptOptions, Pixels, Platform, PlatformDisplay, Point, Render,
     SharedString, SubscriberSet, Subscription, SvgRenderer, Task, TextStyle, TextStyleRefinement,
     TextSystem, View, ViewContext, Window, WindowContext, WindowHandle, WindowId,
@@ -823,13 +823,13 @@ impl AppContext {
     }
 
     /// Check whether a global of the given type has been assigned.
-    pub fn has_global<G: 'static>(&self) -> bool {
+    pub fn has_global<G: Global>(&self) -> bool {
         self.globals_by_type.contains_key(&TypeId::of::<G>())
     }
 
     /// Access the global of the given type. Panics if a global for that type has not been assigned.
     #[track_caller]
-    pub fn global<G: 'static>(&self) -> &G {
+    pub fn global<G: Global>(&self) -> &G {
         self.globals_by_type
             .get(&TypeId::of::<G>())
             .map(|any_state| any_state.downcast_ref::<G>().unwrap())
@@ -838,7 +838,7 @@ impl AppContext {
     }
 
     /// Access the global of the given type if a value has been assigned.
-    pub fn try_global<G: 'static>(&self) -> Option<&G> {
+    pub fn try_global<G: Global>(&self) -> Option<&G> {
         self.globals_by_type
             .get(&TypeId::of::<G>())
             .map(|any_state| any_state.downcast_ref::<G>().unwrap())
@@ -846,7 +846,7 @@ impl AppContext {
 
     /// Access the global of the given type mutably. Panics if a global for that type has not been assigned.
     #[track_caller]
-    pub fn global_mut<G: 'static>(&mut self) -> &mut G {
+    pub fn global_mut<G: Global>(&mut self) -> &mut G {
         let global_type = TypeId::of::<G>();
         self.push_effect(Effect::NotifyGlobalObservers { global_type });
         self.globals_by_type
@@ -858,7 +858,7 @@ impl AppContext {
 
     /// Access the global of the given type mutably. A default value is assigned if a global of this type has not
     /// yet been assigned.
-    pub fn default_global<G: 'static + Default>(&mut self) -> &mut G {
+    pub fn default_global<G: Global + Default>(&mut self) -> &mut G {
         let global_type = TypeId::of::<G>();
         self.push_effect(Effect::NotifyGlobalObservers { global_type });
         self.globals_by_type
@@ -869,7 +869,7 @@ impl AppContext {
     }
 
     /// Sets the value of the global of the given type.
-    pub fn set_global<G: Any>(&mut self, global: G) {
+    pub fn set_global<G: Global>(&mut self, global: G) {
         let global_type = TypeId::of::<G>();
         self.push_effect(Effect::NotifyGlobalObservers { global_type });
         self.globals_by_type.insert(global_type, Box::new(global));
@@ -882,7 +882,7 @@ impl AppContext {
     }
 
     /// Remove the global of the given type from the app context. Does not notify global observers.
-    pub fn remove_global<G: Any>(&mut self) -> G {
+    pub fn remove_global<G: Global>(&mut self) -> G {
         let global_type = TypeId::of::<G>();
         self.push_effect(Effect::NotifyGlobalObservers { global_type });
         *self
@@ -895,7 +895,7 @@ impl AppContext {
 
     /// Updates the global of the given type with a closure. Unlike `global_mut`, this method provides
     /// your closure with mutable access to the `AppContext` and the global simultaneously.
-    pub fn update_global<G: 'static, R>(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R {
+    pub fn update_global<G: Global, R>(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R {
         self.update(|cx| {
             let mut global = cx.lease_global::<G>();
             let result = f(&mut global, cx);
@@ -905,7 +905,7 @@ impl AppContext {
     }
 
     /// Register a callback to be invoked when a global of the given type is updated.
-    pub fn observe_global<G: 'static>(
+    pub fn observe_global<G: Global>(
         &mut self,
         mut f: impl FnMut(&mut Self) + 'static,
     ) -> Subscription {
@@ -921,7 +921,7 @@ impl AppContext {
     }
 
     /// Move the global of the given type to the stack.
-    pub(crate) fn lease_global<G: 'static>(&mut self) -> GlobalLease<G> {
+    pub(crate) fn lease_global<G: Global>(&mut self) -> GlobalLease<G> {
         GlobalLease::new(
             self.globals_by_type
                 .remove(&TypeId::of::<G>())
@@ -931,7 +931,7 @@ impl AppContext {
     }
 
     /// Restore the global of the given type after it is moved to the stack.
-    pub(crate) fn end_global_lease<G: 'static>(&mut self, lease: GlobalLease<G>) {
+    pub(crate) fn end_global_lease<G: Global>(&mut self, lease: GlobalLease<G>) {
         let global_type = TypeId::of::<G>();
         self.push_effect(Effect::NotifyGlobalObservers { global_type });
         self.globals_by_type.insert(global_type, lease.global);
@@ -1293,12 +1293,12 @@ pub(crate) enum Effect {
 }
 
 /// Wraps a global variable value during `update_global` while the value has been moved to the stack.
-pub(crate) struct GlobalLease<G: 'static> {
+pub(crate) struct GlobalLease<G: Global> {
     global: Box<dyn Any>,
     global_type: PhantomData<G>,
 }
 
-impl<G: 'static> GlobalLease<G> {
+impl<G: Global> GlobalLease<G> {
     fn new(global: Box<dyn Any>) -> Self {
         GlobalLease {
             global,
@@ -1307,7 +1307,7 @@ impl<G: 'static> GlobalLease<G> {
     }
 }
 
-impl<G: 'static> Deref for GlobalLease<G> {
+impl<G: Global> Deref for GlobalLease<G> {
     type Target = G;
 
     fn deref(&self) -> &Self::Target {
@@ -1315,7 +1315,7 @@ impl<G: 'static> Deref for GlobalLease<G> {
     }
 }
 
-impl<G: 'static> DerefMut for GlobalLease<G> {
+impl<G: Global> DerefMut for GlobalLease<G> {
     fn deref_mut(&mut self) -> &mut Self::Target {
         self.global.downcast_mut().unwrap()
     }

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

@@ -1,6 +1,6 @@
 use crate::{
     AnyView, AnyWindowHandle, AppCell, AppContext, BackgroundExecutor, Context, DismissEvent,
-    FocusableView, ForegroundExecutor, Model, ModelContext, Render, Result, Task, View,
+    FocusableView, ForegroundExecutor, Global, Model, ModelContext, Render, Result, Task, View,
     ViewContext, VisualContext, WindowContext, WindowHandle,
 };
 use anyhow::{anyhow, Context as _};
@@ -144,7 +144,7 @@ impl AsyncAppContext {
 
     /// Determine whether global state of the specified type has been assigned.
     /// Returns an error if the `AppContext` has been dropped.
-    pub fn has_global<G: 'static>(&self) -> Result<bool> {
+    pub fn has_global<G: Global>(&self) -> Result<bool> {
         let app = self
             .app
             .upgrade()
@@ -157,7 +157,7 @@ impl AsyncAppContext {
     ///
     /// Panics if no global state of the specified type has been assigned.
     /// Returns an error if the `AppContext` has been dropped.
-    pub fn read_global<G: 'static, R>(&self, read: impl FnOnce(&G, &AppContext) -> R) -> Result<R> {
+    pub fn read_global<G: Global, R>(&self, read: impl FnOnce(&G, &AppContext) -> R) -> Result<R> {
         let app = self
             .app
             .upgrade()
@@ -172,7 +172,7 @@ impl AsyncAppContext {
     /// if no state of the specified type has been assigned.
     ///
     /// Returns an error if no state of the specified type has been assigned the `AppContext` has been dropped.
-    pub fn try_read_global<G: 'static, R>(
+    pub fn try_read_global<G: Global, R>(
         &self,
         read: impl FnOnce(&G, &AppContext) -> R,
     ) -> Option<R> {
@@ -183,7 +183,7 @@ impl AsyncAppContext {
 
     /// A convenience method for [AppContext::update_global]
     /// for updating the global state of the specified type.
-    pub fn update_global<G: 'static, R>(
+    pub fn update_global<G: Global, R>(
         &mut self,
         update: impl FnOnce(&mut G, &mut AppContext) -> R,
     ) -> Result<R> {
@@ -235,7 +235,7 @@ impl AsyncWindowContext {
     }
 
     /// A convenience method for [`AppContext::global`].
-    pub fn read_global<G: 'static, R>(
+    pub fn read_global<G: Global, R>(
         &mut self,
         read: impl FnOnce(&G, &WindowContext) -> R,
     ) -> Result<R> {
@@ -249,7 +249,7 @@ impl AsyncWindowContext {
         update: impl FnOnce(&mut G, &mut WindowContext) -> R,
     ) -> Result<R>
     where
-        G: 'static,
+        G: Global,
     {
         self.window.update(self, |_, cx| cx.update_global(update))
     }

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

@@ -1,6 +1,6 @@
 use crate::{
     AnyView, AnyWindowHandle, AppContext, AsyncAppContext, Context, Effect, Entity, EntityId,
-    EventEmitter, Model, Subscription, Task, View, WeakModel, WindowContext, WindowHandle,
+    EventEmitter, Global, Model, Subscription, Task, View, WeakModel, WindowContext, WindowHandle,
 };
 use anyhow::Result;
 use derive_more::{Deref, DerefMut};
@@ -193,7 +193,7 @@ impl<'a, T: 'static> ModelContext<'a, T> {
     /// Updates the given global
     pub fn update_global<G, R>(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R
     where
-        G: 'static,
+        G: Global,
     {
         let mut global = self.app.lease_global::<G>();
         let result = f(&mut global, self);

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

@@ -1,8 +1,8 @@
 use crate::{
     Action, AnyElement, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext,
     AvailableSpace, BackgroundExecutor, Bounds, ClipboardItem, Context, Entity, EventEmitter,
-    ForegroundExecutor, InputEvent, Keystroke, Model, ModelContext, Pixels, Platform, Point,
-    Render, Result, Size, Task, TestDispatcher, TestPlatform, TestWindow, TextSystem, View,
+    ForegroundExecutor, Global, InputEvent, Keystroke, Model, ModelContext, Pixels, Platform,
+    Point, Render, Result, Size, Task, TestDispatcher, TestPlatform, TestWindow, TextSystem, View,
     ViewContext, VisualContext, WindowContext, WindowHandle, WindowOptions,
 };
 use anyhow::{anyhow, bail};
@@ -256,20 +256,20 @@ impl TestAppContext {
     }
 
     /// true if the given global is defined
-    pub fn has_global<G: 'static>(&self) -> bool {
+    pub fn has_global<G: Global>(&self) -> bool {
         let app = self.app.borrow();
         app.has_global::<G>()
     }
 
     /// runs the given closure with a reference to the global
     /// panics if `has_global` would return false.
-    pub fn read_global<G: 'static, R>(&self, read: impl FnOnce(&G, &AppContext) -> R) -> R {
+    pub fn read_global<G: Global, R>(&self, read: impl FnOnce(&G, &AppContext) -> R) -> R {
         let app = self.app.borrow();
         read(app.global(), &app)
     }
 
     /// runs the given closure with a reference to the global (if set)
-    pub fn try_read_global<G: 'static, R>(
+    pub fn try_read_global<G: Global, R>(
         &self,
         read: impl FnOnce(&G, &AppContext) -> R,
     ) -> Option<R> {
@@ -278,13 +278,13 @@ impl TestAppContext {
     }
 
     /// sets the global in this context.
-    pub fn set_global<G: 'static>(&mut self, global: G) {
+    pub fn set_global<G: Global>(&mut self, global: G) {
         let mut lock = self.app.borrow_mut();
         lock.set_global(global);
     }
 
     /// updates the global in this context. (panics if `has_global` would return false)
-    pub fn update_global<G: 'static, R>(
+    pub fn update_global<G: Global, R>(
         &mut self,
         update: impl FnOnce(&mut G, &mut AppContext) -> R,
     ) -> R {

crates/gpui/src/elements/div.rs 🔗

@@ -17,11 +17,11 @@
 
 use crate::{
     point, px, size, Action, AnyDrag, AnyElement, AnyTooltip, AnyView, AppContext, Bounds,
-    ClickEvent, DispatchPhase, Element, ElementContext, ElementId, FocusHandle, IntoElement,
-    IsZero, KeyContext, KeyDownEvent, KeyUpEvent, LayoutId, MouseButton, MouseDownEvent,
-    MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Point, Render, ScrollWheelEvent,
-    SharedString, Size, StackingOrder, Style, StyleRefinement, Styled, Task, View, Visibility,
-    WindowContext,
+    ClickEvent, DispatchPhase, Element, ElementContext, ElementId, FocusHandle, Global,
+    IntoElement, IsZero, KeyContext, KeyDownEvent, KeyUpEvent, LayoutId, MouseButton,
+    MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Point, Render,
+    ScrollWheelEvent, SharedString, Size, StackingOrder, Style, StyleRefinement, Styled, Task,
+    View, Visibility, WindowContext,
 };
 
 use collections::HashMap;
@@ -2070,6 +2070,8 @@ impl ElementClickedState {
 #[derive(Default)]
 pub(crate) struct GroupBounds(HashMap<SharedString, SmallVec<[Bounds<Pixels>; 1]>>);
 
+impl Global for GroupBounds {}
+
 impl GroupBounds {
     pub fn get(name: &SharedString, cx: &mut AppContext) -> Option<Bounds<Pixels>> {
         cx.default_global::<Self>()

crates/gpui/src/gpui.rs 🔗

@@ -258,14 +258,14 @@ pub trait EventEmitter<E: Any>: 'static {}
 /// can be used interchangeably.
 pub trait BorrowAppContext {
     /// Set a global value on the context.
-    fn set_global<T: 'static>(&mut self, global: T);
+    fn set_global<T: Global>(&mut self, global: T);
 }
 
 impl<C> BorrowAppContext for C
 where
     C: BorrowMut<AppContext>,
 {
-    fn set_global<G: 'static>(&mut self, global: G) {
+    fn set_global<G: Global>(&mut self, global: G) {
         self.borrow_mut().set_global(global)
     }
 }
@@ -287,3 +287,8 @@ impl<T> Flatten<T> for Result<T> {
         self
     }
 }
+
+/// A marker trait for types that can be stored in GPUI's global state.
+///
+/// Implement this on types you want to store in the context as a global.
+pub trait Global: 'static {}

crates/gpui/src/style.rs 🔗

@@ -3,8 +3,8 @@ use std::{iter, mem, ops::Range};
 use crate::{
     black, phi, point, quad, rems, AbsoluteLength, Bounds, ContentMask, Corners, CornersRefinement,
     CursorStyle, DefiniteLength, Edges, EdgesRefinement, ElementContext, Font, FontFeatures,
-    FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rgba, SharedString, Size,
-    SizeRefinement, Styled, TextRun,
+    FontStyle, FontWeight, Global, Hsla, Length, Pixels, Point, PointRefinement, Rgba,
+    SharedString, Size, SizeRefinement, Styled, TextRun,
 };
 use collections::HashSet;
 use refineable::Refineable;
@@ -20,6 +20,8 @@ pub use taffy::style::{
 /// GPUI.
 pub struct DebugBelow;
 
+impl Global for DebugBelow {}
+
 /// The CSS styling that can be applied to an element via the `Styled` trait
 #[derive(Clone, Refineable, Debug)]
 #[refineable(Debug)]

crates/gpui/src/window.rs 🔗

@@ -2,7 +2,7 @@ use crate::{
     px, size, transparent_black, Action, AnyDrag, AnyView, AppContext, Arena, AsyncWindowContext,
     AvailableSpace, Bounds, Context, Corners, CursorStyle, DispatchActionListener, DispatchNodeId,
     DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter, FileDropEvent, Flatten,
-    GlobalElementId, Hsla, KeyBinding, KeyContext, KeyDownEvent, KeyMatch, KeymatchMode,
+    Global, GlobalElementId, Hsla, KeyBinding, KeyContext, KeyDownEvent, KeyMatch, KeymatchMode,
     KeymatchResult, Keystroke, KeystrokeEvent, Model, ModelContext, Modifiers, MouseButton,
     MouseMoveEvent, MouseUpEvent, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput,
     PlatformWindow, Point, PromptLevel, Render, ScaledPixels, SharedString, Size, SubscriberSet,
@@ -708,7 +708,7 @@ impl<'a> WindowContext<'a> {
     /// access both to the global and the context.
     pub fn update_global<G, R>(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R
     where
-        G: 'static,
+        G: Global,
     {
         let mut global = self.app.lease_global::<G>();
         let result = f(&mut global, self);
@@ -1441,7 +1441,7 @@ impl<'a> WindowContext<'a> {
 
     /// Register the given handler to be invoked whenever the global of the given type
     /// is updated.
-    pub fn observe_global<G: 'static>(
+    pub fn observe_global<G: Global>(
         &mut self,
         f: impl Fn(&mut WindowContext<'_>) + 'static,
     ) -> Subscription {
@@ -2198,7 +2198,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
     /// Updates the global state of the given type.
     pub fn update_global<G, R>(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R
     where
-        G: 'static,
+        G: Global,
     {
         let mut global = self.app.lease_global::<G>();
         let result = f(&mut global, self);
@@ -2207,7 +2207,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
     }
 
     /// Register a callback to be invoked when the given global state changes.
-    pub fn observe_global<G: 'static>(
+    pub fn observe_global<G: Global>(
         &mut self,
         mut f: impl FnMut(&mut V, &mut ViewContext<'_, V>) + 'static,
     ) -> Subscription {

crates/notifications/src/notification_store.rs 🔗

@@ -3,7 +3,9 @@ use channel::{ChannelMessage, ChannelMessageId, ChannelStore};
 use client::{Client, UserStore};
 use collections::HashMap;
 use db::smol::stream::StreamExt;
-use gpui::{AppContext, AsyncAppContext, Context as _, EventEmitter, Model, ModelContext, Task};
+use gpui::{
+    AppContext, AsyncAppContext, Context as _, EventEmitter, Global, Model, ModelContext, Task,
+};
 use rpc::{proto, Notification, TypedEnvelope};
 use std::{ops::Range, sync::Arc};
 use sum_tree::{Bias, SumTree};
@@ -12,9 +14,13 @@ use util::ResultExt;
 
 pub fn init(client: Arc<Client>, user_store: Model<UserStore>, cx: &mut AppContext) {
     let notification_store = cx.new_model(|cx| NotificationStore::new(client, user_store, cx));
-    cx.set_global(notification_store);
+    cx.set_global(GlobalNotificationStore(notification_store));
 }
 
+struct GlobalNotificationStore(Model<NotificationStore>);
+
+impl Global for GlobalNotificationStore {}
+
 pub struct NotificationStore {
     client: Arc<Client>,
     user_store: Model<UserStore>,
@@ -70,7 +76,7 @@ struct NotificationId(u64);
 
 impl NotificationStore {
     pub fn global(cx: &AppContext) -> Model<Self> {
-        cx.global::<Model<Self>>().clone()
+        cx.global::<GlobalNotificationStore>().0.clone()
     }
 
     pub fn new(

crates/project_panel/src/file_associations.rs 🔗

@@ -2,7 +2,7 @@ use std::{path::Path, str, sync::Arc};
 
 use collections::HashMap;
 
-use gpui::{AppContext, AssetSource};
+use gpui::{AppContext, AssetSource, Global};
 use serde_derive::Deserialize;
 use util::{maybe, paths::PathExt};
 
@@ -17,6 +17,8 @@ pub struct FileAssociations {
     types: HashMap<String, TypeConfig>,
 }
 
+impl Global for FileAssociations {}
+
 const COLLAPSED_DIRECTORY_TYPE: &'static str = "collapsed_folder";
 const EXPANDED_DIRECTORY_TYPE: &'static str = "expanded_folder";
 const COLLAPSED_CHEVRON_TYPE: &'static str = "collapsed_chevron";

crates/release_channel/Cargo.toml 🔗

@@ -0,0 +1,10 @@
+[package]
+name = "release_channel"
+version = "0.1.0"
+edition = "2021"
+publish = false
+license = "GPL-3.0-or-later"
+
+[dependencies]
+gpui = { path = "../gpui" }
+once_cell = "1.19.0"

crates/util/src/channel.rs → crates/release_channel/src/lib.rs 🔗

@@ -1,24 +1,44 @@
-use lazy_static::lazy_static;
+use gpui::{AppContext, Global};
+use once_cell::sync::Lazy;
 use std::env;
 
-lazy_static! {
-    pub static ref RELEASE_CHANNEL_NAME: String = if cfg!(debug_assertions) {
+#[doc(hidden)]
+pub static RELEASE_CHANNEL_NAME: Lazy<String> = if cfg!(debug_assertions) {
+    Lazy::new(|| {
         env::var("ZED_RELEASE_CHANNEL")
             .unwrap_or_else(|_| include_str!("../../zed/RELEASE_CHANNEL").to_string())
-    } else {
-        include_str!("../../zed/RELEASE_CHANNEL").to_string()
-    };
-    pub static ref RELEASE_CHANNEL: ReleaseChannel = match RELEASE_CHANNEL_NAME.as_str().trim() {
+    })
+} else {
+    Lazy::new(|| include_str!("../../zed/RELEASE_CHANNEL").to_string())
+};
+#[doc(hidden)]
+pub static RELEASE_CHANNEL: Lazy<ReleaseChannel> =
+    Lazy::new(|| match RELEASE_CHANNEL_NAME.as_str().trim() {
         "dev" => ReleaseChannel::Dev,
         "nightly" => ReleaseChannel::Nightly,
         "preview" => ReleaseChannel::Preview,
         "stable" => ReleaseChannel::Stable,
         _ => panic!("invalid release channel {}", *RELEASE_CHANNEL_NAME),
-    };
-}
+    });
 
+#[derive(Clone)]
 pub struct AppCommitSha(pub String);
 
+struct GlobalAppCommitSha(AppCommitSha);
+
+impl Global for GlobalAppCommitSha {}
+
+impl AppCommitSha {
+    pub fn try_global(cx: &AppContext) -> Option<AppCommitSha> {
+        cx.try_global::<GlobalAppCommitSha>()
+            .map(|sha| sha.0.clone())
+    }
+
+    pub fn set_global(sha: AppCommitSha, cx: &mut AppContext) {
+        cx.set_global(GlobalAppCommitSha(sha))
+    }
+}
+
 #[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
 pub enum ReleaseChannel {
     #[default]
@@ -28,7 +48,24 @@ pub enum ReleaseChannel {
     Stable,
 }
 
+struct GlobalReleaseChannel(ReleaseChannel);
+
+impl Global for GlobalReleaseChannel {}
+
 impl ReleaseChannel {
+    pub fn init(cx: &mut AppContext) {
+        cx.set_global(GlobalReleaseChannel(*RELEASE_CHANNEL))
+    }
+
+    pub fn global(cx: &AppContext) -> Self {
+        cx.global::<GlobalReleaseChannel>().0
+    }
+
+    pub fn try_global(cx: &AppContext) -> Option<Self> {
+        cx.try_global::<GlobalReleaseChannel>()
+            .map(|channel| channel.0)
+    }
+
     pub fn display_name(&self) -> &'static str {
         match self {
             ReleaseChannel::Dev => "Zed Dev",

crates/search/src/project_search.rs 🔗

@@ -13,10 +13,10 @@ use editor::{
 use editor::{EditorElement, EditorStyle};
 use gpui::{
     actions, div, Action, AnyElement, AnyView, AppContext, Context as _, Element, EntityId,
-    EventEmitter, FocusHandle, FocusableView, FontStyle, FontWeight, Hsla, InteractiveElement,
-    IntoElement, KeyContext, Model, ModelContext, ParentElement, PromptLevel, Render, SharedString,
-    Styled, Subscription, Task, TextStyle, View, ViewContext, VisualContext, WeakModel, WeakView,
-    WhiteSpace, WindowContext,
+    EventEmitter, FocusHandle, FocusableView, FontStyle, FontWeight, Global, Hsla,
+    InteractiveElement, IntoElement, KeyContext, Model, ModelContext, ParentElement, PromptLevel,
+    Render, SharedString, Styled, Subscription, Task, TextStyle, View, ViewContext, VisualContext,
+    WeakModel, WeakView, WhiteSpace, WindowContext,
 };
 use menu::Confirm;
 use project::{
@@ -58,6 +58,8 @@ actions!(
 #[derive(Default)]
 struct ActiveSettings(HashMap<WeakModel<Project>, ProjectSearchSettings>);
 
+impl Global for ActiveSettings {}
+
 pub fn init(cx: &mut AppContext) {
     cx.set_global(ActiveSettings::default());
     cx.observe_new_views(|workspace: &mut Workspace, _cx| {

crates/semantic_index/Cargo.toml 🔗

@@ -17,6 +17,7 @@ language = { path = "../language" }
 project = { path = "../project" }
 workspace = { path = "../workspace" }
 util = { path = "../util" }
+release_channel = { path = "../release_channel" }
 rpc = { path = "../rpc" }
 settings = { path = "../settings" }
 anyhow.workspace = true

crates/semantic_index/src/semantic_index.rs 🔗

@@ -15,8 +15,8 @@ use db::VectorDatabase;
 use embedding_queue::{EmbeddingQueue, FileToEmbed};
 use futures::{future, FutureExt, StreamExt};
 use gpui::{
-    AppContext, AsyncAppContext, BorrowWindow, Context, Model, ModelContext, Task, ViewContext,
-    WeakModel,
+    AppContext, AsyncAppContext, BorrowWindow, Context, Global, Model, ModelContext, Task,
+    ViewContext, WeakModel,
 };
 use language::{Anchor, Bias, Buffer, Language, LanguageRegistry};
 use lazy_static::lazy_static;
@@ -25,6 +25,7 @@ use parking_lot::Mutex;
 use parsing::{CodeContextRetriever, Span, SpanDigest, PARSEABLE_ENTIRE_FILE_TYPES};
 use postage::watch;
 use project::{Fs, PathChange, Project, ProjectEntryId, Worktree, WorktreeId};
+use release_channel::ReleaseChannel;
 use settings::Settings;
 use smol::channel;
 use std::{
@@ -38,7 +39,7 @@ use std::{
     time::{Duration, Instant, SystemTime},
 };
 use util::paths::PathMatcher;
-use util::{channel::RELEASE_CHANNEL_NAME, http::HttpClient, paths::EMBEDDINGS_DIR, ResultExt};
+use util::{http::HttpClient, paths::EMBEDDINGS_DIR, ResultExt};
 use workspace::Workspace;
 
 const SEMANTIC_INDEX_VERSION: usize = 11;
@@ -58,7 +59,7 @@ pub fn init(
     SemanticIndexSettings::register(cx);
 
     let db_file_path = EMBEDDINGS_DIR
-        .join(Path::new(RELEASE_CHANNEL_NAME.as_str()))
+        .join(Path::new(ReleaseChannel::global(cx).dev_name()))
         .join("embeddings_db");
 
     cx.observe_new_views(
@@ -101,7 +102,7 @@ pub fn init(
         )
         .await?;
 
-        cx.update(|cx| cx.set_global(semantic_index.clone()))?;
+        cx.update(|cx| cx.set_global(GlobalSemanticIndex(semantic_index.clone())))?;
 
         anyhow::Ok(())
     })
@@ -130,6 +131,10 @@ pub struct SemanticIndex {
     projects: HashMap<WeakModel<Project>, ProjectState>,
 }
 
+struct GlobalSemanticIndex(Model<SemanticIndex>);
+
+impl Global for GlobalSemanticIndex {}
+
 struct ProjectState {
     worktrees: HashMap<WorktreeId, WorktreeState>,
     pending_file_count_rx: watch::Receiver<usize>,
@@ -274,8 +279,8 @@ pub struct SearchResult {
 
 impl SemanticIndex {
     pub fn global(cx: &mut AppContext) -> Option<Model<SemanticIndex>> {
-        cx.try_global::<Model<Self>>()
-            .map(|semantic_index| semantic_index.clone())
+        cx.try_global::<GlobalSemanticIndex>()
+            .map(|semantic_index| semantic_index.0.clone())
     }
 
     pub fn authenticate(&mut self, cx: &mut AppContext) -> Task<bool> {

crates/settings/Cargo.toml 🔗

@@ -17,6 +17,7 @@ collections = { path = "../collections" }
 gpui = { path = "../gpui" }
 fs = { path = "../fs" }
 feature_flags = { path = "../feature_flags" }
+release_channel = { path = "../release_channel" }
 util = { path = "../util" }
 
 anyhow.workspace = true

crates/settings/src/settings_store.rs 🔗

@@ -1,6 +1,6 @@
 use anyhow::{anyhow, Context, Result};
 use collections::{btree_map, hash_map, BTreeMap, HashMap};
-use gpui::{AppContext, AsyncAppContext};
+use gpui::{AppContext, AsyncAppContext, Global};
 use lazy_static::lazy_static;
 use schemars::{gen::SchemaGenerator, schema::RootSchema, JsonSchema};
 use serde::{de::DeserializeOwned, Deserialize as _, Serialize};
@@ -13,9 +13,7 @@ use std::{
     str,
     sync::Arc,
 };
-use util::{
-    channel::RELEASE_CHANNEL_NAME, merge_non_null_json_value_into, RangeExt, ResultExt as _,
-};
+use util::{merge_non_null_json_value_into, RangeExt, ResultExt as _};
 
 /// A value that can be defined as a user setting.
 ///
@@ -139,6 +137,8 @@ pub struct SettingsStore {
     )>,
 }
 
+impl Global for SettingsStore {}
+
 impl Default for SettingsStore {
     fn default() -> Self {
         SettingsStore {
@@ -207,7 +207,10 @@ impl SettingsStore {
                 user_values_stack = vec![user_settings];
             }
 
-            if let Some(release_settings) = &self.raw_user_settings.get(&*RELEASE_CHANNEL_NAME) {
+            if let Some(release_settings) = &self
+                .raw_user_settings
+                .get(&*release_channel::RELEASE_CHANNEL_NAME)
+            {
                 if let Some(release_settings) = setting_value
                     .deserialize_setting(&release_settings)
                     .log_err()
@@ -537,7 +540,10 @@ impl SettingsStore {
                 paths_stack.push(None);
             }
 
-            if let Some(release_settings) = &self.raw_user_settings.get(&*RELEASE_CHANNEL_NAME) {
+            if let Some(release_settings) = &self
+                .raw_user_settings
+                .get(&*release_channel::RELEASE_CHANNEL_NAME)
+            {
                 if let Some(release_settings) = setting_value
                     .deserialize_setting(&release_settings)
                     .log_err()

crates/theme/src/registry.rs 🔗

@@ -6,7 +6,7 @@ use anyhow::{anyhow, Context, Result};
 use derive_more::{Deref, DerefMut};
 use fs::Fs;
 use futures::StreamExt;
-use gpui::{AppContext, AssetSource, HighlightStyle, SharedString};
+use gpui::{AppContext, AssetSource, Global, HighlightStyle, SharedString};
 use parking_lot::RwLock;
 use refineable::Refineable;
 use util::ResultExt;
@@ -32,10 +32,7 @@ pub struct ThemeMeta {
 #[derive(Default, Deref, DerefMut)]
 struct GlobalThemeRegistry(Arc<ThemeRegistry>);
 
-/// Initializes the theme registry.
-pub fn init(assets: Box<dyn AssetSource>, cx: &mut AppContext) {
-    cx.set_global(GlobalThemeRegistry(Arc::new(ThemeRegistry::new(assets))));
-}
+impl Global for GlobalThemeRegistry {}
 
 struct ThemeRegistryState {
     themes: HashMap<SharedString, Arc<Theme>>,
@@ -59,6 +56,11 @@ impl ThemeRegistry {
         cx.default_global::<GlobalThemeRegistry>().0.clone()
     }
 
+    /// Sets the global [`ThemeRegistry`].
+    pub(crate) fn set_global(assets: Box<dyn AssetSource>, cx: &mut AppContext) {
+        cx.set_global(GlobalThemeRegistry(Arc::new(ThemeRegistry::new(assets))));
+    }
+
     pub fn new(assets: Box<dyn AssetSource>) -> Self {
         let registry = Self {
             state: RwLock::new(ThemeRegistryState {

crates/theme/src/settings.rs 🔗

@@ -2,7 +2,8 @@ use crate::one_themes::one_dark;
 use crate::{SyntaxTheme, Theme, ThemeRegistry, ThemeStyleContent};
 use anyhow::Result;
 use gpui::{
-    px, AppContext, Font, FontFeatures, FontStyle, FontWeight, Pixels, Subscription, ViewContext,
+    px, AppContext, Font, FontFeatures, FontStyle, FontWeight, Global, Pixels, Subscription,
+    ViewContext,
 };
 use refineable::Refineable;
 use schemars::{
@@ -34,6 +35,8 @@ pub struct ThemeSettings {
 #[derive(Default)]
 pub(crate) struct AdjustedBufferFontSize(Pixels);
 
+impl Global for AdjustedBufferFontSize {}
+
 #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
 pub struct ThemeSettingsContent {
     #[serde(default)]

crates/theme/src/theme.rs 🔗

@@ -60,7 +60,7 @@ pub fn init(themes_to_load: LoadThemes, cx: &mut AppContext) {
         LoadThemes::JustBase => (Box::new(()) as Box<dyn AssetSource>, false),
         LoadThemes::All(assets) => (assets, true),
     };
-    registry::init(assets, cx);
+    ThemeRegistry::set_global(assets, cx);
 
     if load_user_themes {
         ThemeRegistry::global(cx).load_bundled_themes();

crates/theme_selector/src/theme_selector.rs 🔗

@@ -36,24 +36,6 @@ pub fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Worksp
     });
 }
 
-#[cfg(debug_assertions)]
-pub fn reload(cx: &mut AppContext) {
-    let current_theme_name = cx.theme().name.clone();
-    let current_theme = cx.update_global(|registry: &mut ThemeRegistry, _cx| {
-        registry.clear();
-        registry.get(&current_theme_name)
-    });
-    match current_theme {
-        Ok(theme) => {
-            ThemeSelectorDelegate::set_theme(theme, cx);
-            log::info!("reloaded theme {}", current_theme_name);
-        }
-        Err(error) => {
-            log::error!("failed to load theme {}: {:?}", current_theme_name, error)
-        }
-    }
-}
-
 impl ModalView for ThemeSelector {}
 
 pub struct ThemeSelector {

crates/vim/Cargo.toml 🔗

@@ -28,6 +28,8 @@ regex.workspace = true
 
 collections = { path = "../collections" }
 command_palette = { path = "../command_palette" }
+# HACK: We're only depending on `copilot` here for `CommandPaletteFilter`.  See the attached comment on that type.
+copilot = { path = "../copilot" }
 editor = { path = "../editor" }
 gpui = { path = "../gpui" }
 language = { path = "../language" }

crates/vim/src/vim.rs 🔗

@@ -15,11 +15,12 @@ mod utils;
 mod visual;
 
 use anyhow::Result;
-use collections::{CommandPaletteFilter, HashMap};
+use collections::HashMap;
 use command_palette::CommandPaletteInterceptor;
+use copilot::CommandPaletteFilter;
 use editor::{movement, Editor, EditorEvent, EditorMode};
 use gpui::{
-    actions, impl_actions, Action, AppContext, EntityId, KeyContext, Subscription, View,
+    actions, impl_actions, Action, AppContext, EntityId, Global, KeyContext, Subscription, View,
     ViewContext, WeakView, WindowContext,
 };
 use language::{CursorShape, Point, Selection, SelectionGoal};
@@ -171,9 +172,9 @@ pub fn observe_keystrokes(cx: &mut WindowContext) {
     .detach()
 }
 
-/// The state pertaining to Vim mode. Stored as a global.
+/// The state pertaining to Vim mode.
 #[derive(Default)]
-pub struct Vim {
+struct Vim {
     active_editor: Option<WeakView<Editor>>,
     editor_subscription: Option<Subscription>,
     enabled: bool,
@@ -182,6 +183,8 @@ pub struct Vim {
     default_state: EditorState,
 }
 
+impl Global for Vim {}
+
 impl Vim {
     fn read(cx: &mut AppContext) -> &Self {
         cx.global::<Self>()
@@ -512,7 +515,9 @@ impl Vim {
             });
 
             if self.enabled {
-                cx.set_global::<CommandPaletteInterceptor>(Box::new(command::command_interceptor));
+                cx.set_global::<CommandPaletteInterceptor>(CommandPaletteInterceptor(Box::new(
+                    command::command_interceptor,
+                )));
             } else if cx.has_global::<CommandPaletteInterceptor>() {
                 let _ = cx.remove_global::<CommandPaletteInterceptor>();
             }

crates/workspace/Cargo.toml 🔗

@@ -43,6 +43,7 @@ async-recursion = "1.0.0"
 itertools = "0.10"
 bincode = "1.2.1"
 anyhow.workspace = true
+derive_more.workspace = true
 futures.workspace = true
 lazy_static.workspace = true
 log.workspace = true

crates/workspace/src/notifications.rs 🔗

@@ -1,7 +1,7 @@
 use crate::{Toast, Workspace};
 use collections::HashMap;
 use gpui::{
-    AnyView, AppContext, AsyncWindowContext, DismissEvent, Entity, EntityId, EventEmitter,
+    AnyView, AppContext, AsyncWindowContext, DismissEvent, Entity, EntityId, EventEmitter, Global,
     PromptLevel, Render, Task, View, ViewContext, VisualContext, WindowContext,
 };
 use std::{any::TypeId, ops::DerefMut};
@@ -39,6 +39,8 @@ pub(crate) struct NotificationTracker {
     notifications_sent: HashMap<TypeId, Vec<usize>>,
 }
 
+impl Global for NotificationTracker {}
+
 impl std::ops::Deref for NotificationTracker {
     type Target = HashMap<TypeId, Vec<usize>>;
 

crates/workspace/src/workspace.rs 🔗

@@ -18,6 +18,7 @@ use client::{
     Client, ErrorExt, Status, TypedEnvelope, UserStore,
 };
 use collections::{hash_map, HashMap, HashSet};
+use derive_more::{Deref, DerefMut};
 use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle};
 use futures::{
     channel::{mpsc, oneshot},
@@ -28,7 +29,7 @@ use gpui::{
     actions, canvas, div, impl_actions, point, px, size, Action, AnyElement, AnyModel, AnyView,
     AnyWeakView, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Context, Div,
     DragMoveEvent, Element, ElementContext, Entity, EntityId, EventEmitter, FocusHandle,
-    FocusableView, GlobalPixels, InteractiveElement, IntoElement, KeyContext, LayoutId,
+    FocusableView, Global, GlobalPixels, InteractiveElement, IntoElement, KeyContext, LayoutId,
     ManagedView, Model, ModelContext, ParentElement, PathPromptOptions, Pixels, Point, PromptLevel,
     Render, SharedString, Size, Styled, Subscription, Task, View, ViewContext, VisualContext,
     WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions,
@@ -59,6 +60,7 @@ use std::{
     borrow::Cow,
     cmp, env,
     path::{Path, PathBuf},
+    sync::Weak,
     sync::{atomic::AtomicUsize, Arc},
     time::Duration,
 };
@@ -256,8 +258,13 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
     });
 }
 
-type ProjectItemBuilders =
-    HashMap<TypeId, fn(Model<Project>, AnyModel, &mut ViewContext<Pane>) -> Box<dyn ItemHandle>>;
+#[derive(Clone, Default, Deref, DerefMut)]
+struct ProjectItemBuilders(
+    HashMap<TypeId, fn(Model<Project>, AnyModel, &mut ViewContext<Pane>) -> Box<dyn ItemHandle>>,
+);
+
+impl Global for ProjectItemBuilders {}
+
 pub fn register_project_item<I: ProjectItem>(cx: &mut AppContext) {
     let builders = cx.default_global::<ProjectItemBuilders>();
     builders.insert(TypeId::of::<I::Item>(), |project, model, cx| {
@@ -273,13 +280,20 @@ type FollowableItemBuilder = fn(
     &mut Option<proto::view::Variant>,
     &mut WindowContext,
 ) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>;
-type FollowableItemBuilders = HashMap<
-    TypeId,
-    (
-        FollowableItemBuilder,
-        fn(&AnyView) -> Box<dyn FollowableItemHandle>,
-    ),
->;
+
+#[derive(Default, Deref, DerefMut)]
+struct FollowableItemBuilders(
+    HashMap<
+        TypeId,
+        (
+            FollowableItemBuilder,
+            fn(&AnyView) -> Box<dyn FollowableItemHandle>,
+        ),
+    >,
+);
+
+impl Global for FollowableItemBuilders {}
+
 pub fn register_followable_item<I: FollowableItem>(cx: &mut AppContext) {
     let builders = cx.default_global::<FollowableItemBuilders>();
     builders.insert(
@@ -296,16 +310,22 @@ pub fn register_followable_item<I: FollowableItem>(cx: &mut AppContext) {
     );
 }
 
-type ItemDeserializers = HashMap<
-    Arc<str>,
-    fn(
-        Model<Project>,
-        WeakView<Workspace>,
-        WorkspaceId,
-        ItemId,
-        &mut ViewContext<Pane>,
-    ) -> Task<Result<Box<dyn ItemHandle>>>,
->;
+#[derive(Default, Deref, DerefMut)]
+struct ItemDeserializers(
+    HashMap<
+        Arc<str>,
+        fn(
+            Model<Project>,
+            WeakView<Workspace>,
+            WorkspaceId,
+            ItemId,
+            &mut ViewContext<Pane>,
+        ) -> Task<Result<Box<dyn ItemHandle>>>,
+    >,
+);
+
+impl Global for ItemDeserializers {}
+
 pub fn register_deserializable_item<I: Item>(cx: &mut AppContext) {
     if let Some(serialized_item_kind) = I::serialized_item_kind() {
         let deserializers = cx.default_global::<ItemDeserializers>();
@@ -331,6 +351,10 @@ pub struct AppState {
     pub node_runtime: Arc<dyn NodeRuntime>,
 }
 
+struct GlobalAppState(Weak<AppState>);
+
+impl Global for GlobalAppState {}
+
 pub struct WorkspaceStore {
     workspaces: HashSet<WindowHandle<Workspace>>,
     followers: Vec<Follower>,
@@ -345,6 +369,17 @@ struct Follower {
 }
 
 impl AppState {
+    pub fn global(cx: &AppContext) -> Weak<Self> {
+        cx.global::<GlobalAppState>().0.clone()
+    }
+    pub fn try_global(cx: &AppContext) -> Option<Weak<Self>> {
+        cx.try_global::<GlobalAppState>()
+            .map(|state| state.0.clone())
+    }
+    pub fn set_global(state: Weak<AppState>, cx: &mut AppContext) {
+        cx.set_global(GlobalAppState(state));
+    }
+
     #[cfg(any(test, feature = "test-support"))]
     pub fn test(cx: &mut AppContext) -> Arc<Self> {
         use node_runtime::FakeNodeRuntime;
@@ -616,7 +651,7 @@ impl Workspace {
         let modal_layer = cx.new_view(|_| ModalLayer::new());
 
         let mut active_call = None;
-        if let Some(call) = cx.try_global::<Model<ActiveCall>>() {
+        if let Some(call) = ActiveCall::try_global(cx) {
             let call = call.clone();
             let mut subscriptions = Vec::new();
             subscriptions.push(cx.subscribe(&call, Self::on_active_call_event));
@@ -3657,7 +3692,7 @@ impl WorkspaceStore {
         update: proto::update_followers::Variant,
         cx: &AppContext,
     ) -> Option<()> {
-        let active_call = cx.try_global::<Model<ActiveCall>>()?;
+        let active_call = ActiveCall::try_global(cx)?;
         let room_id = active_call.read(cx).room()?.read(cx).id();
         let follower_ids: Vec<_> = self
             .followers

crates/zed/Cargo.toml 🔗

@@ -59,6 +59,7 @@ project_panel = { path = "../project_panel" }
 project_symbols = { path = "../project_symbols" }
 quick_action_bar = { path = "../quick_action_bar" }
 recent_projects = { path = "../recent_projects" }
+release_channel = { path = "../release_channel" }
 rope = { path = "../rope"}
 rpc = { path = "../rpc" }
 settings = { path = "../settings" }

crates/zed/src/main.rs 🔗

@@ -19,6 +19,7 @@ use log::LevelFilter;
 use assets::Assets;
 use node_runtime::RealNodeRuntime;
 use parking_lot::Mutex;
+use release_channel::{parse_zed_link, AppCommitSha, ReleaseChannel, RELEASE_CHANNEL};
 use serde::{Deserialize, Serialize};
 use settings::{
     default_settings, handle_settings_file_changes, watch_config_file, Settings, SettingsStore,
@@ -34,14 +35,13 @@ use std::{
     path::{Path, PathBuf},
     sync::{
         atomic::{AtomicU32, Ordering},
-        Arc, Weak,
+        Arc,
     },
     thread,
 };
 use theme::{ActiveTheme, ThemeRegistry, ThemeSettings};
 use util::{
     async_maybe,
-    channel::{parse_zed_link, AppCommitSha, ReleaseChannel, RELEASE_CHANNEL},
     http::{self, HttpClient, ZedHttpClient},
     paths::{self, CRASHES_DIR, CRASHES_RETIRED_DIR},
     ResultExt,
@@ -102,8 +102,7 @@ fn main() {
     let open_listener = listener.clone();
     app.on_open_urls(move |urls, _| open_listener.open_urls(&urls));
     app.on_reopen(move |cx| {
-        if let Some(app_state) = cx
-            .try_global::<Weak<AppState>>()
+        if let Some(app_state) = AppState::try_global(cx)
             .map(|app_state| app_state.upgrade())
             .flatten()
         {
@@ -115,12 +114,12 @@ fn main() {
     });
 
     app.run(move |cx| {
-        cx.set_global(*RELEASE_CHANNEL);
+        ReleaseChannel::init(cx);
         if let Some(build_sha) = option_env!("ZED_COMMIT_SHA") {
-            cx.set_global(AppCommitSha(build_sha.into()))
+            AppCommitSha::set_global(AppCommitSha(build_sha.into()), cx);
         }
 
-        cx.set_global(listener.clone());
+        OpenListener::set_global(listener.clone(), cx);
 
         load_embedded_fonts(cx);
 
@@ -148,7 +147,7 @@ fn main() {
         let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
         let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx));
 
-        cx.set_global(client.clone());
+        Client::set_global(client.clone(), cx);
 
         zed::init(cx);
         theme::init(theme::LoadThemes::All(Box::new(Assets)), cx);
@@ -242,7 +241,7 @@ fn main() {
             workspace_store,
             node_runtime,
         });
-        cx.set_global(Arc::downgrade(&app_state));
+        AppState::set_global(Arc::downgrade(&app_state), cx);
 
         audio::init(Assets, cx);
         auto_update::init(http.clone(), cx);
@@ -565,7 +564,7 @@ fn init_panic_hook(app: &App, installation_id: Option<String>, session_id: Strin
             .or_else(|| info.payload().downcast_ref::<String>().map(|s| s.clone()))
             .unwrap_or_else(|| "Box<Any>".to_string());
 
-        if *util::channel::RELEASE_CHANNEL == ReleaseChannel::Dev {
+        if *release_channel::RELEASE_CHANNEL == ReleaseChannel::Dev {
             let location = info.location().unwrap();
             let backtrace = Backtrace::new();
             eprintln!(

crates/zed/src/only_instance.rs 🔗

@@ -5,7 +5,7 @@ use std::{
     time::Duration,
 };
 
-use util::channel::ReleaseChannel;
+use release_channel::ReleaseChannel;
 
 const LOCALHOST: Ipv4Addr = Ipv4Addr::new(127, 0, 0, 1);
 const CONNECT_TIMEOUT: Duration = Duration::from_millis(10);
@@ -13,7 +13,7 @@ const RECEIVE_TIMEOUT: Duration = Duration::from_millis(35);
 const SEND_TIMEOUT: Duration = Duration::from_millis(20);
 
 fn address() -> SocketAddr {
-    let port = match *util::channel::RELEASE_CHANNEL {
+    let port = match *release_channel::RELEASE_CHANNEL {
         ReleaseChannel::Dev => 43737,
         ReleaseChannel::Preview => 43738,
         ReleaseChannel::Stable => 43739,
@@ -24,7 +24,7 @@ fn address() -> SocketAddr {
 }
 
 fn instance_handshake() -> &'static str {
-    match *util::channel::RELEASE_CHANNEL {
+    match *release_channel::RELEASE_CHANNEL {
         ReleaseChannel::Dev => "Zed Editor Dev Instance Running",
         ReleaseChannel::Nightly => "Zed Editor Nightly Instance Running",
         ReleaseChannel::Preview => "Zed Editor Preview Instance Running",
@@ -39,7 +39,7 @@ pub enum IsOnlyInstance {
 }
 
 pub fn ensure_only_instance() -> IsOnlyInstance {
-    if *db::ZED_STATELESS || *util::channel::RELEASE_CHANNEL == ReleaseChannel::Dev {
+    if *db::ZED_STATELESS || *release_channel::RELEASE_CHANNEL == ReleaseChannel::Dev {
         return IsOnlyInstance::Yes;
     }
 

crates/zed/src/open_listener.rs 🔗

@@ -6,8 +6,9 @@ use editor::Editor;
 use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender};
 use futures::channel::{mpsc, oneshot};
 use futures::{FutureExt, SinkExt, StreamExt};
-use gpui::AsyncAppContext;
+use gpui::{AppContext, AsyncAppContext, Global};
 use language::{Bias, Point};
+use release_channel::parse_zed_link;
 use std::collections::HashMap;
 use std::ffi::OsStr;
 use std::os::unix::prelude::OsStrExt;
@@ -17,7 +18,6 @@ use std::sync::Arc;
 use std::thread;
 use std::time::Duration;
 use std::{path::PathBuf, sync::atomic::AtomicBool};
-use util::channel::parse_zed_link;
 use util::paths::PathLikeWithPosition;
 use util::ResultExt;
 use workspace::AppState;
@@ -42,7 +42,19 @@ pub struct OpenListener {
     pub triggered: AtomicBool,
 }
 
+struct GlobalOpenListener(Arc<OpenListener>);
+
+impl Global for GlobalOpenListener {}
+
 impl OpenListener {
+    pub fn global(cx: &AppContext) -> Arc<Self> {
+        cx.global::<GlobalOpenListener>().0.clone()
+    }
+
+    pub fn set_global(listener: Arc<OpenListener>, cx: &mut AppContext) {
+        cx.set_global(GlobalOpenListener(listener))
+    }
+
     pub fn new() -> (Self, UnboundedReceiver<OpenRequest>) {
         let (tx, rx) = mpsc::unbounded();
         (

crates/zed/src/zed.rs 🔗

@@ -20,6 +20,7 @@ use assets::Assets;
 use futures::{channel::mpsc, select_biased, StreamExt};
 use project_panel::ProjectPanel;
 use quick_action_bar::QuickActionBar;
+use release_channel::{AppCommitSha, ReleaseChannel};
 use rope::Rope;
 use search::project_search::ProjectSearchBar;
 use settings::{initial_local_settings_content, KeymapFile, Settings, SettingsStore};
@@ -27,7 +28,6 @@ use std::{borrow::Cow, ops::Deref, path::Path, sync::Arc};
 use terminal_view::terminal_panel::{self, TerminalPanel};
 use util::{
     asset_str,
-    channel::{AppCommitSha, ReleaseChannel},
     paths::{self, LOCAL_SETTINGS_RELATIVE_PATH},
     ResultExt,
 };
@@ -202,8 +202,7 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
                 cx.toggle_full_screen();
             })
             .register_action(|_, action: &OpenZedUrl, cx| {
-                cx.global::<Arc<OpenListener>>()
-                    .open_urls(&[action.url.clone()])
+                OpenListener::global(cx).open_urls(&[action.url.clone()])
             })
             .register_action(|_, action: &OpenBrowser, cx| cx.open_url(&action.url))
             .register_action(move |_, _: &IncreaseBufferFontSize, cx| {
@@ -370,12 +369,12 @@ fn initialize_pane(workspace: &mut Workspace, pane: &View<Pane>, cx: &mut ViewCo
 }
 
 fn about(_: &mut Workspace, _: &About, cx: &mut gpui::ViewContext<Workspace>) {
-    let app_name = cx.global::<ReleaseChannel>().display_name();
+    let app_name = ReleaseChannel::global(cx).display_name();
     let version = env!("CARGO_PKG_VERSION");
     let message = format!("{app_name} {version}");
-    let detail = cx.try_global::<AppCommitSha>().map(|sha| sha.0.as_ref());
+    let detail = AppCommitSha::try_global(cx).map(|sha| sha.0.clone());
 
-    let prompt = cx.prompt(PromptLevel::Info, &message, detail, &["OK"]);
+    let prompt = cx.prompt(PromptLevel::Info, &message, detail.as_deref(), &["OK"]);
     cx.foreground_executor()
         .spawn(async {
             prompt.await.ok();