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}