use futures::channel::oneshot; use futures::{FutureExt, select_biased}; use gpui::{App, Context, Global, Subscription, Task, Window}; use std::cell::RefCell; use std::rc::Rc; use std::sync::LazyLock; use std::time::Duration; use std::{future::Future, pin::Pin, task::Poll}; #[derive(Default)] struct FeatureFlags { flags: Vec, staff: bool, } pub static ZED_DISABLE_STAFF: LazyLock = LazyLock::new(|| { std::env::var("ZED_DISABLE_STAFF").is_ok_and(|value| !value.is_empty() && value != "0") }); impl FeatureFlags { fn has_flag(&self) -> bool { if T::enabled_for_all() { return true; } if self.staff && T::enabled_for_staff() { return true; } self.flags.iter().any(|f| f.as_str() == T::NAME) } } impl Global for FeatureFlags {} /// To create a feature flag, implement this trait on a trivial type and use it as /// a generic parameter when called [`FeatureFlagAppExt::has_flag`]. /// /// Feature flags are enabled for members of Zed staff by default. To disable this behavior /// so you can test flags being disabled, set ZED_DISABLE_STAFF=1 in your environment, /// which will force Zed to treat the current user as non-staff. pub trait FeatureFlag { const NAME: &'static str; /// Returns whether this feature flag is enabled for Zed staff. fn enabled_for_staff() -> bool { true } /// Returns whether this feature flag is enabled for everyone. /// /// This is generally done on the server, but we provide this as a way to entirely enable a feature flag client-side /// without needing to remove all of the call sites. fn enabled_for_all() -> bool { false } } pub struct PredictEditsRateCompletionsFeatureFlag; impl FeatureFlag for PredictEditsRateCompletionsFeatureFlag { const NAME: &'static str = "predict-edits-rate-completions"; } pub struct LlmClosedBetaFeatureFlag {} impl FeatureFlag for LlmClosedBetaFeatureFlag { const NAME: &'static str = "llm-closed-beta"; } pub struct ZedProFeatureFlag {} impl FeatureFlag for ZedProFeatureFlag { const NAME: &'static str = "zed-pro"; } pub struct NotebookFeatureFlag; impl FeatureFlag for NotebookFeatureFlag { const NAME: &'static str = "notebooks"; } pub struct PanicFeatureFlag; impl FeatureFlag for PanicFeatureFlag { const NAME: &'static str = "panic"; } pub struct JjUiFeatureFlag {} impl FeatureFlag for JjUiFeatureFlag { const NAME: &'static str = "jj-ui"; } pub struct AcpFeatureFlag; impl FeatureFlag for AcpFeatureFlag { const NAME: &'static str = "acp"; } pub struct ClaudeCodeFeatureFlag; impl FeatureFlag for ClaudeCodeFeatureFlag { const NAME: &'static str = "claude-code"; } pub trait FeatureFlagViewExt { fn observe_flag(&mut self, window: &Window, callback: F) -> Subscription where F: Fn(bool, &mut V, &mut Window, &mut Context) + Send + Sync + 'static; fn when_flag_enabled( &mut self, window: &mut Window, callback: impl Fn(&mut V, &mut Window, &mut Context) + Send + Sync + 'static, ); } impl FeatureFlagViewExt for Context<'_, V> where V: 'static, { fn observe_flag(&mut self, window: &Window, callback: F) -> Subscription where F: Fn(bool, &mut V, &mut Window, &mut Context) + 'static, { self.observe_global_in::(window, move |v, window, cx| { let feature_flags = cx.global::(); callback(feature_flags.has_flag::(), v, window, cx); }) } fn when_flag_enabled( &mut self, window: &mut Window, callback: impl Fn(&mut V, &mut Window, &mut Context) + Send + Sync + 'static, ) { if self .try_global::() .is_some_and(|f| f.has_flag::()) { self.defer_in(window, move |view, window, cx| { callback(view, window, cx); }); return; } let subscription = Rc::new(RefCell::new(None)); let inner = self.observe_global_in::(window, { let subscription = subscription.clone(); move |v, window, cx| { let feature_flags = cx.global::(); if feature_flags.has_flag::() { callback(v, window, cx); subscription.take(); } } }); subscription.borrow_mut().replace(inner); } } #[derive(Debug)] pub struct OnFlagsReady { pub is_staff: bool, } pub trait FeatureFlagAppExt { fn wait_for_flag(&mut self) -> WaitForFlag; /// Waits for the specified feature flag to resolve, up to the given timeout. fn wait_for_flag_or_timeout(&mut self, timeout: Duration) -> Task; fn update_flags(&mut self, staff: bool, flags: Vec); fn set_staff(&mut self, staff: bool); fn has_flag(&self) -> bool; fn is_staff(&self) -> bool; fn on_flags_ready(&mut self, callback: F) -> Subscription where F: FnMut(OnFlagsReady, &mut App) + 'static; fn observe_flag(&mut self, callback: F) -> Subscription where F: FnMut(bool, &mut App) + 'static; } impl FeatureFlagAppExt for App { fn update_flags(&mut self, staff: bool, flags: Vec) { let feature_flags = self.default_global::(); feature_flags.staff = staff; feature_flags.flags = flags; } fn set_staff(&mut self, staff: bool) { let feature_flags = self.default_global::(); feature_flags.staff = staff; } fn has_flag(&self) -> bool { self.try_global::() .map(|flags| flags.has_flag::()) .unwrap_or(false) } fn is_staff(&self) -> bool { self.try_global::() .map(|flags| flags.staff) .unwrap_or(false) } fn on_flags_ready(&mut self, mut callback: F) -> Subscription where F: FnMut(OnFlagsReady, &mut App) + 'static, { self.observe_global::(move |cx| { let feature_flags = cx.global::(); callback( OnFlagsReady { is_staff: feature_flags.staff, }, cx, ); }) } fn observe_flag(&mut self, mut callback: F) -> Subscription where F: FnMut(bool, &mut App) + 'static, { self.observe_global::(move |cx| { let feature_flags = cx.global::(); callback(feature_flags.has_flag::(), cx); }) } fn wait_for_flag(&mut self) -> WaitForFlag { let (tx, rx) = oneshot::channel::(); let mut tx = Some(tx); let subscription: Option; match self.try_global::() { Some(feature_flags) => { subscription = None; tx.take().unwrap().send(feature_flags.has_flag::()).ok(); } None => { subscription = Some(self.observe_global::(move |cx| { let feature_flags = cx.global::(); if let Some(tx) = tx.take() { tx.send(feature_flags.has_flag::()).ok(); } })); } } WaitForFlag(rx, subscription) } fn wait_for_flag_or_timeout(&mut self, timeout: Duration) -> Task { let wait_for_flag = self.wait_for_flag::(); self.spawn(async move |_cx| { let mut wait_for_flag = wait_for_flag.fuse(); let mut timeout = FutureExt::fuse(smol::Timer::after(timeout)); select_biased! { is_enabled = wait_for_flag => is_enabled, _ = timeout => false, } }) } } pub struct WaitForFlag(oneshot::Receiver, Option); impl Future for WaitForFlag { type Output = bool; fn poll(mut self: Pin<&mut Self>, cx: &mut core::task::Context<'_>) -> Poll { self.0.poll_unpin(cx).map(|result| { self.1.take(); result.unwrap_or(false) }) } }