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 if store.server_flags_received() {
263 callback(
264 OnFlagsReady {
265 is_staff: store.is_staff(),
266 },
267 cx,
268 );
269 }
270 })
271 }
272
273 fn observe_flag<T: FeatureFlag, F>(&mut self, mut callback: F) -> Subscription
274 where
275 F: FnMut(T::Value, &mut App) + 'static,
276 {
277 let mut last_value: Option<T::Value> = None;
278 self.observe_global::<FeatureFlagStore>(move |cx| {
279 let value = cx.flag_value::<T>();
280 if last_value.as_ref() == Some(&value) {
281 return;
282 }
283 last_value = Some(value.clone());
284 callback(value, cx);
285 })
286 }
287}