feature_flags.rs

  1mod flags;
  2
  3use std::cell::RefCell;
  4use std::rc::Rc;
  5use std::sync::LazyLock;
  6use std::time::Duration;
  7use std::{future::Future, pin::Pin, task::Poll};
  8
  9use futures::channel::oneshot;
 10use futures::{FutureExt, select_biased};
 11use gpui::{App, Context, Global, Subscription, Task, Window};
 12
 13pub use flags::*;
 14
 15#[derive(Default)]
 16struct FeatureFlags {
 17    flags: Vec<String>,
 18    staff: bool,
 19}
 20
 21pub static ZED_DISABLE_STAFF: LazyLock<bool> = LazyLock::new(|| {
 22    std::env::var("ZED_DISABLE_STAFF").is_ok_and(|value| !value.is_empty() && value != "0")
 23});
 24
 25impl FeatureFlags {
 26    fn has_flag<T: FeatureFlag>(&self) -> bool {
 27        if T::enabled_for_all() {
 28            return true;
 29        }
 30
 31        if (cfg!(debug_assertions) || self.staff) && !*ZED_DISABLE_STAFF && T::enabled_for_staff() {
 32            return true;
 33        }
 34
 35        self.flags.iter().any(|f| f.as_str() == T::NAME)
 36    }
 37}
 38
 39impl Global for FeatureFlags {}
 40
 41/// To create a feature flag, implement this trait on a trivial type and use it as
 42/// a generic parameter when called [`FeatureFlagAppExt::has_flag`].
 43///
 44/// Feature flags are enabled for members of Zed staff by default. To disable this behavior
 45/// so you can test flags being disabled, set ZED_DISABLE_STAFF=1 in your environment,
 46/// which will force Zed to treat the current user as non-staff.
 47pub trait FeatureFlag {
 48    const NAME: &'static str;
 49
 50    /// Returns whether this feature flag is enabled for Zed staff.
 51    fn enabled_for_staff() -> bool {
 52        true
 53    }
 54
 55    /// Returns whether this feature flag is enabled for everyone.
 56    ///
 57    /// This is generally done on the server, but we provide this as a way to entirely enable a feature flag client-side
 58    /// without needing to remove all of the call sites.
 59    fn enabled_for_all() -> bool {
 60        false
 61    }
 62}
 63
 64pub trait FeatureFlagViewExt<V: 'static> {
 65    fn observe_flag<T: FeatureFlag, F>(&mut self, window: &Window, callback: F) -> Subscription
 66    where
 67        F: Fn(bool, &mut V, &mut Window, &mut Context<V>) + Send + Sync + 'static;
 68
 69    fn when_flag_enabled<T: FeatureFlag>(
 70        &mut self,
 71        window: &mut Window,
 72        callback: impl Fn(&mut V, &mut Window, &mut Context<V>) + Send + Sync + 'static,
 73    );
 74}
 75
 76impl<V> FeatureFlagViewExt<V> for Context<'_, V>
 77where
 78    V: 'static,
 79{
 80    fn observe_flag<T: FeatureFlag, F>(&mut self, window: &Window, callback: F) -> Subscription
 81    where
 82        F: Fn(bool, &mut V, &mut Window, &mut Context<V>) + 'static,
 83    {
 84        self.observe_global_in::<FeatureFlags>(window, move |v, window, cx| {
 85            let feature_flags = cx.global::<FeatureFlags>();
 86            callback(feature_flags.has_flag::<T>(), v, window, cx);
 87        })
 88    }
 89
 90    fn when_flag_enabled<T: FeatureFlag>(
 91        &mut self,
 92        window: &mut Window,
 93        callback: impl Fn(&mut V, &mut Window, &mut Context<V>) + Send + Sync + 'static,
 94    ) {
 95        if self
 96            .try_global::<FeatureFlags>()
 97            .is_some_and(|f| f.has_flag::<T>())
 98        {
 99            self.defer_in(window, move |view, window, cx| {
100                callback(view, window, cx);
101            });
102            return;
103        }
104        let subscription = Rc::new(RefCell::new(None));
105        let inner = self.observe_global_in::<FeatureFlags>(window, {
106            let subscription = subscription.clone();
107            move |v, window, cx| {
108                let feature_flags = cx.global::<FeatureFlags>();
109                if feature_flags.has_flag::<T>() {
110                    callback(v, window, cx);
111                    subscription.take();
112                }
113            }
114        });
115        subscription.borrow_mut().replace(inner);
116    }
117}
118
119#[derive(Debug)]
120pub struct OnFlagsReady {
121    pub is_staff: bool,
122}
123
124pub trait FeatureFlagAppExt {
125    fn wait_for_flag<T: FeatureFlag>(&mut self) -> WaitForFlag;
126
127    /// Waits for the specified feature flag to resolve, up to the given timeout.
128    fn wait_for_flag_or_timeout<T: FeatureFlag>(&mut self, timeout: Duration) -> Task<bool>;
129
130    fn update_flags(&mut self, staff: bool, flags: Vec<String>);
131    fn set_staff(&mut self, staff: bool);
132    fn has_flag<T: FeatureFlag>(&self) -> bool;
133    fn is_staff(&self) -> bool;
134
135    fn on_flags_ready<F>(&mut self, callback: F) -> Subscription
136    where
137        F: FnMut(OnFlagsReady, &mut App) + 'static;
138
139    fn observe_flag<T: FeatureFlag, F>(&mut self, callback: F) -> Subscription
140    where
141        F: FnMut(bool, &mut App) + 'static;
142}
143
144impl FeatureFlagAppExt for App {
145    fn update_flags(&mut self, staff: bool, flags: Vec<String>) {
146        let feature_flags = self.default_global::<FeatureFlags>();
147        feature_flags.staff = staff;
148        feature_flags.flags = flags;
149    }
150
151    fn set_staff(&mut self, staff: bool) {
152        let feature_flags = self.default_global::<FeatureFlags>();
153        feature_flags.staff = staff;
154    }
155
156    fn has_flag<T: FeatureFlag>(&self) -> bool {
157        self.try_global::<FeatureFlags>()
158            .map(|flags| flags.has_flag::<T>())
159            .unwrap_or_else(|| {
160                (cfg!(debug_assertions) && T::enabled_for_staff() && !*ZED_DISABLE_STAFF)
161                    || T::enabled_for_all()
162            })
163    }
164
165    fn is_staff(&self) -> bool {
166        self.try_global::<FeatureFlags>()
167            .map(|flags| flags.staff)
168            .unwrap_or(false)
169    }
170
171    fn on_flags_ready<F>(&mut self, mut callback: F) -> Subscription
172    where
173        F: FnMut(OnFlagsReady, &mut App) + 'static,
174    {
175        self.observe_global::<FeatureFlags>(move |cx| {
176            let feature_flags = cx.global::<FeatureFlags>();
177            callback(
178                OnFlagsReady {
179                    is_staff: feature_flags.staff,
180                },
181                cx,
182            );
183        })
184    }
185
186    fn observe_flag<T: FeatureFlag, F>(&mut self, mut callback: F) -> Subscription
187    where
188        F: FnMut(bool, &mut App) + 'static,
189    {
190        self.observe_global::<FeatureFlags>(move |cx| {
191            let feature_flags = cx.global::<FeatureFlags>();
192            callback(feature_flags.has_flag::<T>(), cx);
193        })
194    }
195
196    fn wait_for_flag<T: FeatureFlag>(&mut self) -> WaitForFlag {
197        let (tx, rx) = oneshot::channel::<bool>();
198        let mut tx = Some(tx);
199        let subscription: Option<Subscription>;
200
201        match self.try_global::<FeatureFlags>() {
202            Some(feature_flags) => {
203                subscription = None;
204                tx.take().unwrap().send(feature_flags.has_flag::<T>()).ok();
205            }
206            None => {
207                subscription = Some(self.observe_global::<FeatureFlags>(move |cx| {
208                    let feature_flags = cx.global::<FeatureFlags>();
209                    if let Some(tx) = tx.take() {
210                        tx.send(feature_flags.has_flag::<T>()).ok();
211                    }
212                }));
213            }
214        }
215
216        WaitForFlag(rx, subscription)
217    }
218
219    fn wait_for_flag_or_timeout<T: FeatureFlag>(&mut self, timeout: Duration) -> Task<bool> {
220        let wait_for_flag = self.wait_for_flag::<T>();
221
222        self.spawn(async move |_cx| {
223            let mut wait_for_flag = wait_for_flag.fuse();
224            let mut timeout = FutureExt::fuse(smol::Timer::after(timeout));
225
226            select_biased! {
227                is_enabled = wait_for_flag => is_enabled,
228                _ = timeout => false,
229            }
230        })
231    }
232}
233
234pub struct WaitForFlag(oneshot::Receiver<bool>, Option<Subscription>);
235
236impl Future for WaitForFlag {
237    type Output = bool;
238
239    fn poll(mut self: Pin<&mut Self>, cx: &mut core::task::Context<'_>) -> Poll<Self::Output> {
240        self.0.poll_unpin(cx).map(|result| {
241            self.1.take();
242            result.unwrap_or(false)
243        })
244    }
245}