feature_flags.rs

  1// Makes the derive macro's reference to `::feature_flags::FeatureFlagValue`
  2// resolve when the macro is invoked inside this crate itself.
  3extern crate self as feature_flags;
  4
  5mod flags;
  6mod settings;
  7mod store;
  8
  9use std::cell::RefCell;
 10use std::rc::Rc;
 11use std::sync::LazyLock;
 12
 13use gpui::{App, Context, Global, Subscription, Window};
 14
 15pub use feature_flags_macros::EnumFeatureFlag;
 16pub use flags::*;
 17pub use settings::{FeatureFlagsSettings, generate_feature_flags_schema};
 18pub use store::*;
 19
 20pub static ZED_DISABLE_STAFF: LazyLock<bool> = LazyLock::new(|| {
 21    std::env::var("ZED_DISABLE_STAFF").is_ok_and(|value| !value.is_empty() && value != "0")
 22});
 23
 24impl Global for FeatureFlagStore {}
 25
 26pub trait FeatureFlagValue:
 27    Sized + Clone + Eq + Default + std::fmt::Debug + Send + Sync + 'static
 28{
 29    /// Every possible value for this flag, in the order the UI should display them.
 30    fn all_variants() -> &'static [Self];
 31
 32    /// A stable identifier for this variant used when persisting overrides.
 33    fn override_key(&self) -> &'static str;
 34
 35    fn from_wire(wire: &str) -> Option<Self>;
 36
 37    /// Human-readable label for use in the configuration UI.
 38    fn label(&self) -> &'static str {
 39        self.override_key()
 40    }
 41
 42    /// The variant that represents "on" — what the store resolves to when
 43    /// staff rules, `enabled_for_all`, or a server announcement apply.
 44    ///
 45    /// For enum flags this is usually the same as [`Default::default`] (the
 46    /// variant marked `#[default]` in the derive). [`PresenceFlag`] overrides
 47    /// this so that `default() == Off` (the "unconfigured" state) but
 48    /// `on_variant() == On` (the "enabled" state).
 49    fn on_variant() -> Self {
 50        Self::default()
 51    }
 52}
 53
 54/// Default value type for simple on/off feature flags.
 55///
 56/// The fallback value is [`PresenceFlag::Off`] so that an absent / unknown
 57/// flag reads as disabled; the `on_variant` override pins the "enabled"
 58/// state to [`PresenceFlag::On`] so staff / server / `enabled_for_all`
 59/// resolution still lights the flag up.
 60#[derive(Clone, Copy, Default, PartialEq, Eq, Debug)]
 61pub enum PresenceFlag {
 62    On,
 63    #[default]
 64    Off,
 65}
 66
 67/// Presence flags deref to a `bool` so call sites can use `if *flag` without
 68/// spelling out the enum variant — or pass them anywhere a `&bool` is wanted.
 69impl std::ops::Deref for PresenceFlag {
 70    type Target = bool;
 71
 72    fn deref(&self) -> &bool {
 73        match self {
 74            PresenceFlag::On => &true,
 75            PresenceFlag::Off => &false,
 76        }
 77    }
 78}
 79
 80impl FeatureFlagValue for PresenceFlag {
 81    fn all_variants() -> &'static [Self] {
 82        &[PresenceFlag::On, PresenceFlag::Off]
 83    }
 84
 85    fn override_key(&self) -> &'static str {
 86        match self {
 87            PresenceFlag::On => "on",
 88            PresenceFlag::Off => "off",
 89        }
 90    }
 91
 92    fn label(&self) -> &'static str {
 93        match self {
 94            PresenceFlag::On => "On",
 95            PresenceFlag::Off => "Off",
 96        }
 97    }
 98
 99    fn from_wire(_: &str) -> Option<Self> {
100        Some(PresenceFlag::On)
101    }
102
103    fn on_variant() -> Self {
104        PresenceFlag::On
105    }
106}
107
108/// To create a feature flag, implement this trait on a trivial type and use it as
109/// a generic parameter when called [`FeatureFlagAppExt::has_flag`].
110///
111/// Feature flags are enabled for members of Zed staff by default. To disable this behavior
112/// so you can test flags being disabled, set ZED_DISABLE_STAFF=1 in your environment,
113/// which will force Zed to treat the current user as non-staff.
114pub trait FeatureFlag {
115    const NAME: &'static str;
116
117    /// The type of value this flag can hold. Use [`PresenceFlag`] for simple
118    /// on/off flags.
119    type Value: FeatureFlagValue;
120
121    /// Returns whether this feature flag is enabled for Zed staff.
122    fn enabled_for_staff() -> bool {
123        true
124    }
125
126    /// Returns whether this feature flag is enabled for everyone.
127    ///
128    /// This is generally done on the server, but we provide this as a way to entirely enable a feature flag client-side
129    /// without needing to remove all of the call sites.
130    fn enabled_for_all() -> bool {
131        false
132    }
133
134    /// Subscribes the current view to changes in the feature flag store, so
135    /// that any mutation of flags or overrides will trigger a re-render.
136    ///
137    /// The returned subscription is immediately detached; use [`observe_flag`]
138    /// directly if you need to hold onto the subscription.
139    fn watch<V: 'static>(cx: &mut Context<V>) {
140        cx.observe_global::<FeatureFlagStore>(|_, cx| cx.notify())
141            .detach();
142    }
143}
144
145pub trait FeatureFlagViewExt<V: 'static> {
146    /// Fires the callback whenever the resolved [`T::Value`] transitions.
147    fn observe_flag<T: FeatureFlag, F>(&mut self, window: &Window, callback: F) -> Subscription
148    where
149        F: Fn(T::Value, &mut V, &mut Window, &mut Context<V>) + Send + Sync + 'static;
150
151    fn when_flag_enabled<T: FeatureFlag>(
152        &mut self,
153        window: &mut Window,
154        callback: impl Fn(&mut V, &mut Window, &mut Context<V>) + Send + Sync + 'static,
155    );
156}
157
158impl<V> FeatureFlagViewExt<V> for Context<'_, V>
159where
160    V: 'static,
161{
162    fn observe_flag<T: FeatureFlag, F>(&mut self, window: &Window, callback: F) -> Subscription
163    where
164        F: Fn(T::Value, &mut V, &mut Window, &mut Context<V>) + 'static,
165    {
166        let mut last_value: Option<T::Value> = None;
167        self.observe_global_in::<FeatureFlagStore>(window, move |v, window, cx| {
168            let value = cx.flag_value::<T>();
169            if last_value.as_ref() == Some(&value) {
170                return;
171            }
172            last_value = Some(value.clone());
173            callback(value, v, window, cx);
174        })
175    }
176
177    fn when_flag_enabled<T: FeatureFlag>(
178        &mut self,
179        window: &mut Window,
180        callback: impl Fn(&mut V, &mut Window, &mut Context<V>) + Send + Sync + 'static,
181    ) {
182        if self
183            .try_global::<FeatureFlagStore>()
184            .is_some_and(|f| f.has_flag::<T>(self))
185        {
186            self.defer_in(window, move |view, window, cx| {
187                callback(view, window, cx);
188            });
189            return;
190        }
191        let subscription = Rc::new(RefCell::new(None));
192        let inner = self.observe_global_in::<FeatureFlagStore>(window, {
193            let subscription = subscription.clone();
194            move |v, window, cx| {
195                let has_flag = cx.global::<FeatureFlagStore>().has_flag::<T>(cx);
196                if has_flag {
197                    callback(v, window, cx);
198                    subscription.take();
199                }
200            }
201        });
202        subscription.borrow_mut().replace(inner);
203    }
204}
205
206#[derive(Debug)]
207pub struct OnFlagsReady {
208    pub is_staff: bool,
209}
210
211pub trait FeatureFlagAppExt {
212    fn update_flags(&mut self, staff: bool, flags: Vec<String>);
213    fn set_staff(&mut self, staff: bool);
214    fn has_flag<T: FeatureFlag>(&self) -> bool;
215    fn flag_value<T: FeatureFlag>(&self) -> T::Value;
216    fn is_staff(&self) -> bool;
217
218    fn on_flags_ready<F>(&mut self, callback: F) -> Subscription
219    where
220        F: FnMut(OnFlagsReady, &mut App) + 'static;
221
222    fn observe_flag<T: FeatureFlag, F>(&mut self, callback: F) -> Subscription
223    where
224        F: FnMut(T::Value, &mut App) + 'static;
225}
226
227impl FeatureFlagAppExt for App {
228    fn update_flags(&mut self, staff: bool, flags: Vec<String>) {
229        let store = self.default_global::<FeatureFlagStore>();
230        store.update_server_flags(staff, flags);
231    }
232
233    fn set_staff(&mut self, staff: bool) {
234        let store = self.default_global::<FeatureFlagStore>();
235        store.set_staff(staff);
236    }
237
238    fn has_flag<T: FeatureFlag>(&self) -> bool {
239        self.try_global::<FeatureFlagStore>()
240            .map(|store| store.has_flag::<T>(self))
241            .unwrap_or_else(|| FeatureFlagStore::has_flag_default::<T>())
242    }
243
244    fn flag_value<T: FeatureFlag>(&self) -> T::Value {
245        self.try_global::<FeatureFlagStore>()
246            .and_then(|store| store.try_flag_value::<T>(self))
247            .unwrap_or_default()
248    }
249
250    fn is_staff(&self) -> bool {
251        self.try_global::<FeatureFlagStore>()
252            .map(|store| store.is_staff())
253            .unwrap_or(false)
254    }
255
256    fn on_flags_ready<F>(&mut self, mut callback: F) -> Subscription
257    where
258        F: FnMut(OnFlagsReady, &mut App) + 'static,
259    {
260        self.observe_global::<FeatureFlagStore>(move |cx| {
261            let store = cx.global::<FeatureFlagStore>();
262            callback(
263                OnFlagsReady {
264                    is_staff: store.is_staff(),
265                },
266                cx,
267            );
268        })
269    }
270
271    fn observe_flag<T: FeatureFlag, F>(&mut self, mut callback: F) -> Subscription
272    where
273        F: FnMut(T::Value, &mut App) + 'static,
274    {
275        let mut last_value: Option<T::Value> = None;
276        self.observe_global::<FeatureFlagStore>(move |cx| {
277            let value = cx.flag_value::<T>();
278            if last_value.as_ref() == Some(&value) {
279                return;
280            }
281            last_value = Some(value.clone());
282            callback(value, cx);
283        })
284    }
285}