store.rs

  1use std::any::TypeId;
  2use std::sync::Arc;
  3
  4use collections::HashMap;
  5use fs::Fs;
  6use gpui::{App, BorrowAppContext, Subscription};
  7use settings::{Settings, SettingsStore, update_settings_file};
  8
  9use crate::{FeatureFlag, FeatureFlagValue, FeatureFlagsSettings, ZED_DISABLE_STAFF};
 10
 11pub struct FeatureFlagDescriptor {
 12    pub name: &'static str,
 13    pub variants: fn() -> Vec<FeatureFlagVariant>,
 14    pub on_variant_key: fn() -> &'static str,
 15    pub default_variant_key: fn() -> &'static str,
 16    pub enabled_for_all: fn() -> bool,
 17    pub enabled_for_staff: fn() -> bool,
 18    pub type_id: fn() -> TypeId,
 19}
 20
 21#[derive(Clone, Debug, PartialEq, Eq)]
 22pub struct FeatureFlagVariant {
 23    pub override_key: &'static str,
 24    pub label: &'static str,
 25}
 26
 27inventory::collect!(FeatureFlagDescriptor);
 28
 29#[doc(hidden)]
 30pub mod __private {
 31    pub use inventory;
 32}
 33
 34/// Submits a [`FeatureFlagDescriptor`] for this flag so it shows up in the
 35/// configuration UI and in `FeatureFlagStore::known_flags()`.
 36#[macro_export]
 37macro_rules! register_feature_flag {
 38    ($flag:ty) => {
 39        $crate::__private::inventory::submit! {
 40            $crate::FeatureFlagDescriptor {
 41                name: <$flag as $crate::FeatureFlag>::NAME,
 42                variants: || {
 43                    <<$flag as $crate::FeatureFlag>::Value as $crate::FeatureFlagValue>::all_variants()
 44                        .iter()
 45                        .map(|v| $crate::FeatureFlagVariant {
 46                            override_key: <<$flag as $crate::FeatureFlag>::Value as $crate::FeatureFlagValue>::override_key(v),
 47                            label: <<$flag as $crate::FeatureFlag>::Value as $crate::FeatureFlagValue>::label(v),
 48                        })
 49                        .collect()
 50                },
 51                on_variant_key: || {
 52                    <<$flag as $crate::FeatureFlag>::Value as $crate::FeatureFlagValue>::override_key(
 53                        &<<$flag as $crate::FeatureFlag>::Value as $crate::FeatureFlagValue>::on_variant(),
 54                    )
 55                },
 56                default_variant_key: || {
 57                    <<$flag as $crate::FeatureFlag>::Value as $crate::FeatureFlagValue>::override_key(
 58                        &<<$flag as $crate::FeatureFlag>::Value as ::std::default::Default>::default(),
 59                    )
 60                },
 61                enabled_for_all: <$flag as $crate::FeatureFlag>::enabled_for_all,
 62                enabled_for_staff: <$flag as $crate::FeatureFlag>::enabled_for_staff,
 63                type_id: || std::any::TypeId::of::<$flag>(),
 64            }
 65        }
 66    };
 67}
 68
 69#[derive(Default)]
 70pub struct FeatureFlagStore {
 71    staff: bool,
 72    server_flags: HashMap<String, String>,
 73    server_flags_received: bool,
 74
 75    _settings_subscription: Option<Subscription>,
 76}
 77
 78impl FeatureFlagStore {
 79    pub fn init(cx: &mut App) {
 80        let subscription = cx.observe_global::<SettingsStore>(|cx| {
 81            // Touch the global so anything observing `FeatureFlagStore` re-runs
 82            cx.update_default_global::<FeatureFlagStore, _>(|_, _| {});
 83        });
 84
 85        cx.update_default_global::<FeatureFlagStore, _>(|store, _| {
 86            store._settings_subscription = Some(subscription);
 87        });
 88    }
 89
 90    pub fn known_flags() -> impl Iterator<Item = &'static FeatureFlagDescriptor> {
 91        let mut seen = collections::HashSet::default();
 92        inventory::iter::<FeatureFlagDescriptor>().filter(move |d| seen.insert((d.type_id)()))
 93    }
 94
 95    pub fn is_staff(&self) -> bool {
 96        self.staff
 97    }
 98
 99    pub fn server_flags_received(&self) -> bool {
100        self.server_flags_received
101    }
102
103    pub fn set_staff(&mut self, staff: bool) {
104        self.staff = staff;
105    }
106
107    pub fn update_server_flags(&mut self, staff: bool, flags: Vec<String>) {
108        self.staff = staff;
109        self.server_flags_received = true;
110        self.server_flags.clear();
111        for flag in flags {
112            self.server_flags.insert(flag.clone(), flag);
113        }
114    }
115
116    /// The user's override key for this flag, read directly from
117    /// [`FeatureFlagsSettings`].
118    pub fn override_for<'a>(flag_name: &str, cx: &'a App) -> Option<&'a str> {
119        FeatureFlagsSettings::get_global(cx)
120            .overrides
121            .get(flag_name)
122            .map(String::as_str)
123    }
124
125    /// Applies an override by writing to `settings.json`. The store's own
126    /// `overrides` field will be updated when the settings-store observer
127    /// fires. Pass the [`FeatureFlagValue::override_key`] of the variant
128    /// you want forced.
129    pub fn set_override(flag_name: &str, override_key: String, fs: Arc<dyn Fs>, cx: &App) {
130        let flag_name = flag_name.to_owned();
131        update_settings_file(fs, cx, move |content, _| {
132            content
133                .feature_flags
134                .get_or_insert_default()
135                .insert(flag_name, override_key);
136        });
137    }
138
139    /// Removes any override for the given flag from `settings.json`. Leaves
140    /// an empty `"feature_flags"` object rather than removing the key
141    /// entirely so the user can see it's still a meaningful settings surface.
142    pub fn clear_override(flag_name: &str, fs: Arc<dyn Fs>, cx: &App) {
143        let flag_name = flag_name.to_owned();
144        update_settings_file(fs, cx, move |content, _| {
145            if let Some(map) = content.feature_flags.as_mut() {
146                map.remove(&flag_name);
147            }
148        });
149    }
150
151    /// The resolved value of the flag for the current user, taking overrides,
152    /// `enabled_for_all`, staff rules, and server flags into account in that
153    /// order of precedence. Overrides are read directly from
154    /// [`FeatureFlagsSettings`].
155    pub fn try_flag_value<T: FeatureFlag>(&self, cx: &App) -> Option<T::Value> {
156        // `enabled_for_all` always wins, including over user overrides.
157        if T::enabled_for_all() {
158            return Some(T::Value::on_variant());
159        }
160
161        if let Some(override_key) = FeatureFlagsSettings::get_global(cx).overrides.get(T::NAME) {
162            return variant_from_key::<T::Value>(override_key);
163        }
164
165        // Staff default: resolve to the enabled variant.
166        if (cfg!(debug_assertions) || self.staff) && !*ZED_DISABLE_STAFF && T::enabled_for_staff() {
167            return Some(T::Value::on_variant());
168        }
169
170        // Server-delivered flag.
171        if let Some(wire) = self.server_flags.get(T::NAME) {
172            return T::Value::from_wire(wire);
173        }
174
175        None
176    }
177
178    /// Whether the flag resolves to its "on" value. Best for presence-style
179    /// flags. For enum flags with meaningful non-default variants, prefer
180    /// [`crate::FeatureFlagAppExt::flag_value`].
181    pub fn has_flag<T: FeatureFlag>(&self, cx: &App) -> bool {
182        self.try_flag_value::<T>(cx)
183            .is_some_and(|v| v == T::Value::on_variant())
184    }
185
186    /// Mirrors the resolution order of [`Self::try_flag_value`], but falls
187    /// back to the [`Default`] variant when no rule applies so the UI always
188    /// shows *something* selected — matching what
189    /// [`crate::FeatureFlagAppExt::flag_value`] would return.
190    pub fn resolved_key(&self, descriptor: &FeatureFlagDescriptor, cx: &App) -> &'static str {
191        let on_variant_key = (descriptor.on_variant_key)();
192
193        if (descriptor.enabled_for_all)() {
194            return on_variant_key;
195        }
196
197        if let Some(requested) = FeatureFlagsSettings::get_global(cx)
198            .overrides
199            .get(descriptor.name)
200        {
201            if let Some(variant) = (descriptor.variants)()
202                .into_iter()
203                .find(|v| v.override_key == requested.as_str())
204            {
205                return variant.override_key;
206            }
207        }
208
209        if (cfg!(debug_assertions) || self.staff)
210            && !*ZED_DISABLE_STAFF
211            && (descriptor.enabled_for_staff)()
212        {
213            return on_variant_key;
214        }
215
216        if self.server_flags.contains_key(descriptor.name) {
217            return on_variant_key;
218        }
219
220        (descriptor.default_variant_key)()
221    }
222
223    /// Whether this flag is forced on by `enabled_for_all` and therefore not
224    /// user-overridable. The UI uses this to render the row as disabled.
225    pub fn is_forced_on(descriptor: &FeatureFlagDescriptor) -> bool {
226        (descriptor.enabled_for_all)()
227    }
228
229    /// Fallback used when the store isn't installed as a global yet (e.g. very
230    /// early in startup). Matches the pre-existing default behavior.
231    pub fn has_flag_default<T: FeatureFlag>() -> bool {
232        if T::enabled_for_all() {
233            return true;
234        }
235        cfg!(debug_assertions) && T::enabled_for_staff() && !*ZED_DISABLE_STAFF
236    }
237}
238
239fn variant_from_key<V: FeatureFlagValue>(key: &str) -> Option<V> {
240    V::all_variants()
241        .iter()
242        .find(|v| v.override_key() == key)
243        .cloned()
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249    use crate::{EnumFeatureFlag, FeatureFlag, PresenceFlag};
250    use gpui::UpdateGlobal;
251    use settings::SettingsStore;
252
253    struct DemoFlag;
254    impl FeatureFlag for DemoFlag {
255        const NAME: &'static str = "demo";
256        type Value = PresenceFlag;
257        fn enabled_for_staff() -> bool {
258            false
259        }
260    }
261
262    #[derive(Clone, Copy, PartialEq, Eq, Debug, EnumFeatureFlag)]
263    enum Intensity {
264        #[default]
265        Low,
266        High,
267    }
268
269    struct IntensityFlag;
270    impl FeatureFlag for IntensityFlag {
271        const NAME: &'static str = "intensity";
272        type Value = Intensity;
273        fn enabled_for_all() -> bool {
274            true
275        }
276    }
277
278    fn init_settings_store(cx: &mut App) {
279        let store = SettingsStore::test(cx);
280        cx.set_global(store);
281        SettingsStore::update_global(cx, |store, _| {
282            store.register_setting::<FeatureFlagsSettings>();
283        });
284    }
285
286    fn set_override(name: &str, value: &str, cx: &mut App) {
287        SettingsStore::update_global(cx, |store: &mut SettingsStore, cx| {
288            store.update_user_settings(cx, |content| {
289                content
290                    .feature_flags
291                    .get_or_insert_default()
292                    .insert(name.to_string(), value.to_string());
293            });
294        });
295    }
296
297    #[gpui::test]
298    fn server_flag_enables_presence(cx: &mut App) {
299        init_settings_store(cx);
300        let mut store = FeatureFlagStore::default();
301        assert!(!store.has_flag::<DemoFlag>(cx));
302        store.update_server_flags(false, vec!["demo".to_string()]);
303        assert!(store.has_flag::<DemoFlag>(cx));
304    }
305
306    #[gpui::test]
307    fn off_override_beats_server_flag(cx: &mut App) {
308        init_settings_store(cx);
309        let mut store = FeatureFlagStore::default();
310        store.update_server_flags(false, vec!["demo".to_string()]);
311        set_override(DemoFlag::NAME, "off", cx);
312        assert!(!store.has_flag::<DemoFlag>(cx));
313        assert_eq!(
314            store.try_flag_value::<DemoFlag>(cx),
315            Some(PresenceFlag::Off)
316        );
317    }
318
319    #[gpui::test]
320    fn enabled_for_all_wins_over_override(cx: &mut App) {
321        init_settings_store(cx);
322        let store = FeatureFlagStore::default();
323        set_override(IntensityFlag::NAME, "high", cx);
324        assert_eq!(
325            store.try_flag_value::<IntensityFlag>(cx),
326            Some(Intensity::Low)
327        );
328    }
329
330    #[gpui::test]
331    fn enum_override_selects_specific_variant(cx: &mut App) {
332        init_settings_store(cx);
333        let store = FeatureFlagStore::default();
334        // Staff path would normally resolve to `Low`; the override pushes
335        // us to `High` instead.
336        set_override("enum-demo", "high", cx);
337
338        struct EnumDemo;
339        impl FeatureFlag for EnumDemo {
340            const NAME: &'static str = "enum-demo";
341            type Value = Intensity;
342        }
343
344        assert_eq!(store.try_flag_value::<EnumDemo>(cx), Some(Intensity::High));
345    }
346
347    #[gpui::test]
348    fn unknown_variant_key_resolves_to_none(cx: &mut App) {
349        init_settings_store(cx);
350        let store = FeatureFlagStore::default();
351        set_override("enum-demo", "nonsense", cx);
352
353        struct EnumDemo;
354        impl FeatureFlag for EnumDemo {
355            const NAME: &'static str = "enum-demo";
356            type Value = Intensity;
357        }
358
359        assert_eq!(store.try_flag_value::<EnumDemo>(cx), None);
360    }
361
362    #[gpui::test]
363    fn on_override_enables_without_server_or_staff(cx: &mut App) {
364        init_settings_store(cx);
365        let store = FeatureFlagStore::default();
366        set_override(DemoFlag::NAME, "on", cx);
367        assert!(store.has_flag::<DemoFlag>(cx));
368    }
369
370    /// No rule applies, so the store's `try_flag_value` returns `None`. The
371    /// `FeatureFlagAppExt::flag_value` path (used by most callers) falls
372    /// back to [`Default`], which for `PresenceFlag` is `Off`.
373    #[gpui::test]
374    fn presence_flag_defaults_to_off(cx: &mut App) {
375        init_settings_store(cx);
376        let store = FeatureFlagStore::default();
377        assert_eq!(store.try_flag_value::<DemoFlag>(cx), None);
378        assert_eq!(PresenceFlag::default(), PresenceFlag::Off);
379    }
380
381    #[gpui::test]
382    fn on_flags_ready_waits_for_server_flags(cx: &mut gpui::TestAppContext) {
383        use crate::FeatureFlagAppExt;
384        use std::cell::Cell;
385        use std::rc::Rc;
386
387        cx.update(|cx| {
388            init_settings_store(cx);
389            FeatureFlagStore::init(cx);
390        });
391
392        let fired = Rc::new(Cell::new(false));
393        cx.update({
394            let fired = fired.clone();
395            |cx| cx.on_flags_ready(move |_, _| fired.set(true)).detach()
396        });
397
398        // Settings-triggered no-op touch must not fire on_flags_ready.
399        cx.update(|cx| cx.update_default_global::<FeatureFlagStore, _>(|_, _| {}));
400        cx.run_until_parked();
401        assert!(!fired.get());
402
403        // Server flags arrive — now it should fire.
404        cx.update(|cx| cx.update_flags(true, vec![]));
405        cx.run_until_parked();
406        assert!(fired.get());
407    }
408}