logiguard fork v3: full patch set on verified 8c74db0 tree
Includes prior-session patches (carry forward so the app compiles): - crates/gpui/build.rs: cross-compile manifest fix - crates/gpui/src/platform.rs: PlatformWindow::activate_with_token trait method - crates/gpui/src/window.rs: Window::activate_with_token public API - crates/gpui_linux/src/linux/wayland/window.rs: WaylandWindow::activate_with_token + activate() keyboard-serial fix Plus the focus-serial fix: - serial.rs: SerialKind::KeyboardEnter - client.rs: store wl_keyboard.enter serial; latest_serial_of() Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
26
crates/feature_flags/Cargo.toml
Normal file
26
crates/feature_flags/Cargo.toml
Normal file
@@ -0,0 +1,26 @@
|
||||
[package]
|
||||
name = "feature_flags"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/feature_flags.rs"
|
||||
|
||||
[dependencies]
|
||||
collections.workspace = true
|
||||
feature_flags_macros.workspace = true
|
||||
fs.workspace = true
|
||||
gpui.workspace = true
|
||||
inventory.workspace = true
|
||||
schemars.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
1
crates/feature_flags/LICENSE-GPL
Symbolic link
1
crates/feature_flags/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
287
crates/feature_flags/src/feature_flags.rs
Normal file
287
crates/feature_flags/src/feature_flags.rs
Normal file
@@ -0,0 +1,287 @@
|
||||
// Makes the derive macro's reference to `::feature_flags::FeatureFlagValue`
|
||||
// resolve when the macro is invoked inside this crate itself.
|
||||
extern crate self as feature_flags;
|
||||
|
||||
mod flags;
|
||||
mod settings;
|
||||
mod store;
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use gpui::{App, Context, Global, Subscription, Window};
|
||||
|
||||
pub use feature_flags_macros::EnumFeatureFlag;
|
||||
pub use flags::*;
|
||||
pub use settings::{FeatureFlagsSettings, generate_feature_flags_schema};
|
||||
pub use store::*;
|
||||
|
||||
pub static ZED_DISABLE_STAFF: LazyLock<bool> = LazyLock::new(|| {
|
||||
std::env::var("ZED_DISABLE_STAFF").is_ok_and(|value| !value.is_empty() && value != "0")
|
||||
});
|
||||
|
||||
impl Global for FeatureFlagStore {}
|
||||
|
||||
pub trait FeatureFlagValue:
|
||||
Sized + Clone + Eq + Default + std::fmt::Debug + Send + Sync + 'static
|
||||
{
|
||||
/// Every possible value for this flag, in the order the UI should display them.
|
||||
fn all_variants() -> &'static [Self];
|
||||
|
||||
/// A stable identifier for this variant used when persisting overrides.
|
||||
fn override_key(&self) -> &'static str;
|
||||
|
||||
fn from_wire(wire: &str) -> Option<Self>;
|
||||
|
||||
/// Human-readable label for use in the configuration UI.
|
||||
fn label(&self) -> &'static str {
|
||||
self.override_key()
|
||||
}
|
||||
|
||||
/// The variant that represents "on" — what the store resolves to when
|
||||
/// staff rules, `enabled_for_all`, or a server announcement apply.
|
||||
///
|
||||
/// For enum flags this is usually the same as [`Default::default`] (the
|
||||
/// variant marked `#[default]` in the derive). [`PresenceFlag`] overrides
|
||||
/// this so that `default() == Off` (the "unconfigured" state) but
|
||||
/// `on_variant() == On` (the "enabled" state).
|
||||
fn on_variant() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Default value type for simple on/off feature flags.
|
||||
///
|
||||
/// The fallback value is [`PresenceFlag::Off`] so that an absent / unknown
|
||||
/// flag reads as disabled; the `on_variant` override pins the "enabled"
|
||||
/// state to [`PresenceFlag::On`] so staff / server / `enabled_for_all`
|
||||
/// resolution still lights the flag up.
|
||||
#[derive(Clone, Copy, Default, PartialEq, Eq, Debug)]
|
||||
pub enum PresenceFlag {
|
||||
On,
|
||||
#[default]
|
||||
Off,
|
||||
}
|
||||
|
||||
/// Presence flags deref to a `bool` so call sites can use `if *flag` without
|
||||
/// spelling out the enum variant — or pass them anywhere a `&bool` is wanted.
|
||||
impl std::ops::Deref for PresenceFlag {
|
||||
type Target = bool;
|
||||
|
||||
fn deref(&self) -> &bool {
|
||||
match self {
|
||||
PresenceFlag::On => &true,
|
||||
PresenceFlag::Off => &false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FeatureFlagValue for PresenceFlag {
|
||||
fn all_variants() -> &'static [Self] {
|
||||
&[PresenceFlag::On, PresenceFlag::Off]
|
||||
}
|
||||
|
||||
fn override_key(&self) -> &'static str {
|
||||
match self {
|
||||
PresenceFlag::On => "on",
|
||||
PresenceFlag::Off => "off",
|
||||
}
|
||||
}
|
||||
|
||||
fn label(&self) -> &'static str {
|
||||
match self {
|
||||
PresenceFlag::On => "On",
|
||||
PresenceFlag::Off => "Off",
|
||||
}
|
||||
}
|
||||
|
||||
fn from_wire(_: &str) -> Option<Self> {
|
||||
Some(PresenceFlag::On)
|
||||
}
|
||||
|
||||
fn on_variant() -> Self {
|
||||
PresenceFlag::On
|
||||
}
|
||||
}
|
||||
|
||||
/// To create a feature flag, implement this trait on a trivial type and use it as
|
||||
/// a generic parameter when called [`FeatureFlagAppExt::has_flag`].
|
||||
///
|
||||
/// Feature flags are enabled for members of Zed staff by default. To disable this behavior
|
||||
/// so you can test flags being disabled, set ZED_DISABLE_STAFF=1 in your environment,
|
||||
/// which will force Zed to treat the current user as non-staff.
|
||||
pub trait FeatureFlag {
|
||||
const NAME: &'static str;
|
||||
|
||||
/// The type of value this flag can hold. Use [`PresenceFlag`] for simple
|
||||
/// on/off flags.
|
||||
type Value: FeatureFlagValue;
|
||||
|
||||
/// Returns whether this feature flag is enabled for Zed staff.
|
||||
fn enabled_for_staff() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Returns whether this feature flag is enabled for everyone.
|
||||
///
|
||||
/// This is generally done on the server, but we provide this as a way to entirely enable a feature flag client-side
|
||||
/// without needing to remove all of the call sites.
|
||||
fn enabled_for_all() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Subscribes the current view to changes in the feature flag store, so
|
||||
/// that any mutation of flags or overrides will trigger a re-render.
|
||||
///
|
||||
/// The returned subscription is immediately detached; use [`observe_flag`]
|
||||
/// directly if you need to hold onto the subscription.
|
||||
fn watch<V: 'static>(cx: &mut Context<V>) {
|
||||
cx.observe_global::<FeatureFlagStore>(|_, cx| cx.notify())
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
pub trait FeatureFlagViewExt<V: 'static> {
|
||||
/// Fires the callback whenever the resolved [`T::Value`] transitions.
|
||||
fn observe_flag<T: FeatureFlag, F>(&mut self, window: &Window, callback: F) -> Subscription
|
||||
where
|
||||
F: Fn(T::Value, &mut V, &mut Window, &mut Context<V>) + Send + Sync + 'static;
|
||||
|
||||
fn when_flag_enabled<T: FeatureFlag>(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
callback: impl Fn(&mut V, &mut Window, &mut Context<V>) + Send + Sync + 'static,
|
||||
);
|
||||
}
|
||||
|
||||
impl<V> FeatureFlagViewExt<V> for Context<'_, V>
|
||||
where
|
||||
V: 'static,
|
||||
{
|
||||
fn observe_flag<T: FeatureFlag, F>(&mut self, window: &Window, callback: F) -> Subscription
|
||||
where
|
||||
F: Fn(T::Value, &mut V, &mut Window, &mut Context<V>) + 'static,
|
||||
{
|
||||
let mut last_value: Option<T::Value> = None;
|
||||
self.observe_global_in::<FeatureFlagStore>(window, move |v, window, cx| {
|
||||
let value = cx.flag_value::<T>();
|
||||
if last_value.as_ref() == Some(&value) {
|
||||
return;
|
||||
}
|
||||
last_value = Some(value.clone());
|
||||
callback(value, v, window, cx);
|
||||
})
|
||||
}
|
||||
|
||||
fn when_flag_enabled<T: FeatureFlag>(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
callback: impl Fn(&mut V, &mut Window, &mut Context<V>) + Send + Sync + 'static,
|
||||
) {
|
||||
if self
|
||||
.try_global::<FeatureFlagStore>()
|
||||
.is_some_and(|f| f.has_flag::<T>(self))
|
||||
{
|
||||
self.defer_in(window, move |view, window, cx| {
|
||||
callback(view, window, cx);
|
||||
});
|
||||
return;
|
||||
}
|
||||
let subscription = Rc::new(RefCell::new(None));
|
||||
let inner = self.observe_global_in::<FeatureFlagStore>(window, {
|
||||
let subscription = subscription.clone();
|
||||
move |v, window, cx| {
|
||||
let has_flag = cx.global::<FeatureFlagStore>().has_flag::<T>(cx);
|
||||
if has_flag {
|
||||
callback(v, window, cx);
|
||||
subscription.take();
|
||||
}
|
||||
}
|
||||
});
|
||||
subscription.borrow_mut().replace(inner);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct OnFlagsReady {
|
||||
pub is_staff: bool,
|
||||
}
|
||||
|
||||
pub trait FeatureFlagAppExt {
|
||||
fn update_flags(&mut self, staff: bool, flags: Vec<String>);
|
||||
fn set_staff(&mut self, staff: bool);
|
||||
fn has_flag<T: FeatureFlag>(&self) -> bool;
|
||||
fn flag_value<T: FeatureFlag>(&self) -> T::Value;
|
||||
fn is_staff(&self) -> bool;
|
||||
|
||||
fn on_flags_ready<F>(&mut self, callback: F) -> Subscription
|
||||
where
|
||||
F: FnMut(OnFlagsReady, &mut App) + 'static;
|
||||
|
||||
fn observe_flag<T: FeatureFlag, F>(&mut self, callback: F) -> Subscription
|
||||
where
|
||||
F: FnMut(T::Value, &mut App) + 'static;
|
||||
}
|
||||
|
||||
impl FeatureFlagAppExt for App {
|
||||
fn update_flags(&mut self, staff: bool, flags: Vec<String>) {
|
||||
let store = self.default_global::<FeatureFlagStore>();
|
||||
store.update_server_flags(staff, flags);
|
||||
}
|
||||
|
||||
fn set_staff(&mut self, staff: bool) {
|
||||
let store = self.default_global::<FeatureFlagStore>();
|
||||
store.set_staff(staff);
|
||||
}
|
||||
|
||||
fn has_flag<T: FeatureFlag>(&self) -> bool {
|
||||
self.try_global::<FeatureFlagStore>()
|
||||
.map(|store| store.has_flag::<T>(self))
|
||||
.unwrap_or_else(|| FeatureFlagStore::has_flag_default::<T>())
|
||||
}
|
||||
|
||||
fn flag_value<T: FeatureFlag>(&self) -> T::Value {
|
||||
self.try_global::<FeatureFlagStore>()
|
||||
.and_then(|store| store.try_flag_value::<T>(self))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn is_staff(&self) -> bool {
|
||||
self.try_global::<FeatureFlagStore>()
|
||||
.map(|store| store.is_staff())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn on_flags_ready<F>(&mut self, mut callback: F) -> Subscription
|
||||
where
|
||||
F: FnMut(OnFlagsReady, &mut App) + 'static,
|
||||
{
|
||||
self.observe_global::<FeatureFlagStore>(move |cx| {
|
||||
let store = cx.global::<FeatureFlagStore>();
|
||||
if store.server_flags_received() {
|
||||
callback(
|
||||
OnFlagsReady {
|
||||
is_staff: store.is_staff(),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn observe_flag<T: FeatureFlag, F>(&mut self, mut callback: F) -> Subscription
|
||||
where
|
||||
F: FnMut(T::Value, &mut App) + 'static,
|
||||
{
|
||||
let mut last_value: Option<T::Value> = None;
|
||||
self.observe_global::<FeatureFlagStore>(move |cx| {
|
||||
let value = cx.flag_value::<T>();
|
||||
if last_value.as_ref() == Some(&value) {
|
||||
return;
|
||||
}
|
||||
last_value = Some(value.clone());
|
||||
callback(value, cx);
|
||||
})
|
||||
}
|
||||
}
|
||||
145
crates/feature_flags/src/flags.rs
Normal file
145
crates/feature_flags/src/flags.rs
Normal file
@@ -0,0 +1,145 @@
|
||||
use crate::{EnumFeatureFlag, FeatureFlag, PresenceFlag, register_feature_flag};
|
||||
|
||||
pub struct NotebookFeatureFlag;
|
||||
|
||||
impl FeatureFlag for NotebookFeatureFlag {
|
||||
const NAME: &'static str = "notebooks";
|
||||
type Value = PresenceFlag;
|
||||
}
|
||||
register_feature_flag!(NotebookFeatureFlag);
|
||||
|
||||
pub struct PanicFeatureFlag;
|
||||
|
||||
impl FeatureFlag for PanicFeatureFlag {
|
||||
const NAME: &'static str = "panic";
|
||||
type Value = PresenceFlag;
|
||||
}
|
||||
register_feature_flag!(PanicFeatureFlag);
|
||||
|
||||
/// A feature flag for granting access to beta ACP features.
|
||||
///
|
||||
/// We reuse this feature flag for new betas, so don't delete it if it is not currently in use.
|
||||
pub struct AcpBetaFeatureFlag;
|
||||
|
||||
impl FeatureFlag for AcpBetaFeatureFlag {
|
||||
const NAME: &'static str = "acp-beta";
|
||||
type Value = PresenceFlag;
|
||||
}
|
||||
register_feature_flag!(AcpBetaFeatureFlag);
|
||||
|
||||
pub struct AgentSharingFeatureFlag;
|
||||
|
||||
impl FeatureFlag for AgentSharingFeatureFlag {
|
||||
const NAME: &'static str = "agent-sharing";
|
||||
type Value = PresenceFlag;
|
||||
}
|
||||
register_feature_flag!(AgentSharingFeatureFlag);
|
||||
|
||||
pub struct AgentPanelTerminalFeatureFlag;
|
||||
|
||||
impl FeatureFlag for AgentPanelTerminalFeatureFlag {
|
||||
const NAME: &'static str = "agent-panel-terminal";
|
||||
type Value = PresenceFlag;
|
||||
}
|
||||
register_feature_flag!(AgentPanelTerminalFeatureFlag);
|
||||
|
||||
pub struct DiffReviewFeatureFlag;
|
||||
|
||||
impl FeatureFlag for DiffReviewFeatureFlag {
|
||||
const NAME: &'static str = "diff-review";
|
||||
type Value = PresenceFlag;
|
||||
|
||||
fn enabled_for_staff() -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
register_feature_flag!(DiffReviewFeatureFlag);
|
||||
|
||||
pub struct UpdatePlanToolFeatureFlag;
|
||||
|
||||
impl FeatureFlag for UpdatePlanToolFeatureFlag {
|
||||
const NAME: &'static str = "update-plan-tool";
|
||||
type Value = PresenceFlag;
|
||||
|
||||
fn enabled_for_staff() -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
register_feature_flag!(UpdatePlanToolFeatureFlag);
|
||||
|
||||
pub struct LspToolFeatureFlag;
|
||||
|
||||
impl FeatureFlag for LspToolFeatureFlag {
|
||||
const NAME: &'static str = "lsp-tool";
|
||||
type Value = PresenceFlag;
|
||||
|
||||
fn enabled_for_staff() -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
register_feature_flag!(LspToolFeatureFlag);
|
||||
|
||||
pub struct RenameToolFeatureFlag;
|
||||
|
||||
impl FeatureFlag for RenameToolFeatureFlag {
|
||||
const NAME: &'static str = "rename-tool";
|
||||
type Value = PresenceFlag;
|
||||
|
||||
fn enabled_for_staff() -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
register_feature_flag!(RenameToolFeatureFlag);
|
||||
|
||||
pub struct ProjectPanelUndoRedoFeatureFlag;
|
||||
|
||||
impl FeatureFlag for ProjectPanelUndoRedoFeatureFlag {
|
||||
const NAME: &'static str = "project-panel-undo-redo";
|
||||
type Value = PresenceFlag;
|
||||
|
||||
fn enabled_for_staff() -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
register_feature_flag!(ProjectPanelUndoRedoFeatureFlag);
|
||||
|
||||
/// Controls how agent thread worktree chips are labeled in the sidebar.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, EnumFeatureFlag)]
|
||||
pub enum AgentThreadWorktreeLabel {
|
||||
#[default]
|
||||
Both,
|
||||
Worktree,
|
||||
Branch,
|
||||
}
|
||||
|
||||
pub struct AgentThreadWorktreeLabelFlag;
|
||||
|
||||
impl FeatureFlag for AgentThreadWorktreeLabelFlag {
|
||||
const NAME: &'static str = "agent-thread-worktree-label";
|
||||
type Value = AgentThreadWorktreeLabel;
|
||||
|
||||
fn enabled_for_staff() -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
register_feature_flag!(AgentThreadWorktreeLabelFlag);
|
||||
|
||||
pub struct AutoWatchFeatureFlag;
|
||||
|
||||
impl FeatureFlag for AutoWatchFeatureFlag {
|
||||
const NAME: &'static str = "auto-watch-screens";
|
||||
type Value = PresenceFlag;
|
||||
}
|
||||
register_feature_flag!(AutoWatchFeatureFlag);
|
||||
|
||||
pub struct SkillsFeatureFlag;
|
||||
|
||||
impl FeatureFlag for SkillsFeatureFlag {
|
||||
const NAME: &'static str = "skills";
|
||||
type Value = PresenceFlag;
|
||||
|
||||
fn enabled_for_staff() -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
register_feature_flag!(SkillsFeatureFlag);
|
||||
76
crates/feature_flags/src/settings.rs
Normal file
76
crates/feature_flags/src/settings.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
use collections::HashMap;
|
||||
use schemars::{Schema, json_schema};
|
||||
use serde_json::{Map, Value};
|
||||
use settings::{RegisterSetting, Settings, SettingsContent};
|
||||
|
||||
use crate::FeatureFlagStore;
|
||||
|
||||
#[derive(Clone, Debug, Default, RegisterSetting)]
|
||||
pub struct FeatureFlagsSettings {
|
||||
pub overrides: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl Settings for FeatureFlagsSettings {
|
||||
fn from_settings(content: &SettingsContent) -> Self {
|
||||
Self {
|
||||
overrides: content
|
||||
.feature_flags
|
||||
.as_ref()
|
||||
.map(|map| map.0.clone())
|
||||
.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Produces a JSON schema for the `feature_flags` object that lists each known
|
||||
/// flag as a property with its variant keys as an `enum`.
|
||||
///
|
||||
/// Unknown flags are permitted via `additionalProperties: { "type": "string" }`,
|
||||
/// so removing a flag from the binary never turns existing entries in
|
||||
/// `settings.json` into validation errors.
|
||||
pub fn generate_feature_flags_schema() -> Schema {
|
||||
let mut properties = Map::new();
|
||||
|
||||
for descriptor in FeatureFlagStore::known_flags() {
|
||||
let variants = (descriptor.variants)();
|
||||
let enum_values: Vec<Value> = variants
|
||||
.iter()
|
||||
.map(|v| Value::String(v.override_key.to_string()))
|
||||
.collect();
|
||||
let enum_descriptions: Vec<Value> = variants
|
||||
.iter()
|
||||
.map(|v| Value::String(v.label.to_string()))
|
||||
.collect();
|
||||
|
||||
let mut property = Map::new();
|
||||
property.insert("type".to_string(), Value::String("string".to_string()));
|
||||
property.insert("enum".to_string(), Value::Array(enum_values));
|
||||
// VS Code / json-language-server use `enumDescriptions` for hover docs
|
||||
// on each enum value; schemars passes them through untouched.
|
||||
property.insert(
|
||||
"enumDescriptions".to_string(),
|
||||
Value::Array(enum_descriptions),
|
||||
);
|
||||
property.insert(
|
||||
"description".to_string(),
|
||||
Value::String(format!(
|
||||
"Override for the `{}` feature flag. Default: `{}` (the {} variant).",
|
||||
descriptor.name,
|
||||
(descriptor.default_variant_key)(),
|
||||
(descriptor.default_variant_key)(),
|
||||
)),
|
||||
);
|
||||
|
||||
properties.insert(descriptor.name.to_string(), Value::Object(property));
|
||||
}
|
||||
|
||||
json_schema!({
|
||||
"type": "object",
|
||||
"description": "Local overrides for feature flags, keyed by flag name.",
|
||||
"properties": properties,
|
||||
"additionalProperties": {
|
||||
"type": "string",
|
||||
"description": "Unknown feature flag; retained so removed flags don't trip settings validation."
|
||||
}
|
||||
})
|
||||
}
|
||||
408
crates/feature_flags/src/store.rs
Normal file
408
crates/feature_flags/src/store.rs
Normal file
@@ -0,0 +1,408 @@
|
||||
use std::any::TypeId;
|
||||
use std::sync::Arc;
|
||||
|
||||
use collections::HashMap;
|
||||
use fs::Fs;
|
||||
use gpui::{App, BorrowAppContext, Subscription};
|
||||
use settings::{Settings, SettingsStore, update_settings_file};
|
||||
|
||||
use crate::{FeatureFlag, FeatureFlagValue, FeatureFlagsSettings, ZED_DISABLE_STAFF};
|
||||
|
||||
pub struct FeatureFlagDescriptor {
|
||||
pub name: &'static str,
|
||||
pub variants: fn() -> Vec<FeatureFlagVariant>,
|
||||
pub on_variant_key: fn() -> &'static str,
|
||||
pub default_variant_key: fn() -> &'static str,
|
||||
pub enabled_for_all: fn() -> bool,
|
||||
pub enabled_for_staff: fn() -> bool,
|
||||
pub type_id: fn() -> TypeId,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct FeatureFlagVariant {
|
||||
pub override_key: &'static str,
|
||||
pub label: &'static str,
|
||||
}
|
||||
|
||||
inventory::collect!(FeatureFlagDescriptor);
|
||||
|
||||
#[doc(hidden)]
|
||||
pub mod __private {
|
||||
pub use inventory;
|
||||
}
|
||||
|
||||
/// Submits a [`FeatureFlagDescriptor`] for this flag so it shows up in the
|
||||
/// configuration UI and in `FeatureFlagStore::known_flags()`.
|
||||
#[macro_export]
|
||||
macro_rules! register_feature_flag {
|
||||
($flag:ty) => {
|
||||
$crate::__private::inventory::submit! {
|
||||
$crate::FeatureFlagDescriptor {
|
||||
name: <$flag as $crate::FeatureFlag>::NAME,
|
||||
variants: || {
|
||||
<<$flag as $crate::FeatureFlag>::Value as $crate::FeatureFlagValue>::all_variants()
|
||||
.iter()
|
||||
.map(|v| $crate::FeatureFlagVariant {
|
||||
override_key: <<$flag as $crate::FeatureFlag>::Value as $crate::FeatureFlagValue>::override_key(v),
|
||||
label: <<$flag as $crate::FeatureFlag>::Value as $crate::FeatureFlagValue>::label(v),
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
on_variant_key: || {
|
||||
<<$flag as $crate::FeatureFlag>::Value as $crate::FeatureFlagValue>::override_key(
|
||||
&<<$flag as $crate::FeatureFlag>::Value as $crate::FeatureFlagValue>::on_variant(),
|
||||
)
|
||||
},
|
||||
default_variant_key: || {
|
||||
<<$flag as $crate::FeatureFlag>::Value as $crate::FeatureFlagValue>::override_key(
|
||||
&<<$flag as $crate::FeatureFlag>::Value as ::std::default::Default>::default(),
|
||||
)
|
||||
},
|
||||
enabled_for_all: <$flag as $crate::FeatureFlag>::enabled_for_all,
|
||||
enabled_for_staff: <$flag as $crate::FeatureFlag>::enabled_for_staff,
|
||||
type_id: || std::any::TypeId::of::<$flag>(),
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FeatureFlagStore {
|
||||
staff: bool,
|
||||
server_flags: HashMap<String, String>,
|
||||
server_flags_received: bool,
|
||||
|
||||
_settings_subscription: Option<Subscription>,
|
||||
}
|
||||
|
||||
impl FeatureFlagStore {
|
||||
pub fn init(cx: &mut App) {
|
||||
let subscription = cx.observe_global::<SettingsStore>(|cx| {
|
||||
// Touch the global so anything observing `FeatureFlagStore` re-runs
|
||||
cx.update_default_global::<FeatureFlagStore, _>(|_, _| {});
|
||||
});
|
||||
|
||||
cx.update_default_global::<FeatureFlagStore, _>(|store, _| {
|
||||
store._settings_subscription = Some(subscription);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn known_flags() -> impl Iterator<Item = &'static FeatureFlagDescriptor> {
|
||||
let mut seen = collections::HashSet::default();
|
||||
inventory::iter::<FeatureFlagDescriptor>().filter(move |d| seen.insert((d.type_id)()))
|
||||
}
|
||||
|
||||
pub fn is_staff(&self) -> bool {
|
||||
self.staff
|
||||
}
|
||||
|
||||
pub fn server_flags_received(&self) -> bool {
|
||||
self.server_flags_received
|
||||
}
|
||||
|
||||
pub fn set_staff(&mut self, staff: bool) {
|
||||
self.staff = staff;
|
||||
}
|
||||
|
||||
pub fn update_server_flags(&mut self, staff: bool, flags: Vec<String>) {
|
||||
self.staff = staff;
|
||||
self.server_flags_received = true;
|
||||
self.server_flags.clear();
|
||||
for flag in flags {
|
||||
self.server_flags.insert(flag.clone(), flag);
|
||||
}
|
||||
}
|
||||
|
||||
/// The user's override key for this flag, read directly from
|
||||
/// [`FeatureFlagsSettings`].
|
||||
pub fn override_for<'a>(flag_name: &str, cx: &'a App) -> Option<&'a str> {
|
||||
FeatureFlagsSettings::get_global(cx)
|
||||
.overrides
|
||||
.get(flag_name)
|
||||
.map(String::as_str)
|
||||
}
|
||||
|
||||
/// Applies an override by writing to `settings.json`. The store's own
|
||||
/// `overrides` field will be updated when the settings-store observer
|
||||
/// fires. Pass the [`FeatureFlagValue::override_key`] of the variant
|
||||
/// you want forced.
|
||||
pub fn set_override(flag_name: &str, override_key: String, fs: Arc<dyn Fs>, cx: &App) {
|
||||
let flag_name = flag_name.to_owned();
|
||||
update_settings_file(fs, cx, move |content, _| {
|
||||
content
|
||||
.feature_flags
|
||||
.get_or_insert_default()
|
||||
.insert(flag_name, override_key);
|
||||
});
|
||||
}
|
||||
|
||||
/// Removes any override for the given flag from `settings.json`. Leaves
|
||||
/// an empty `"feature_flags"` object rather than removing the key
|
||||
/// entirely so the user can see it's still a meaningful settings surface.
|
||||
pub fn clear_override(flag_name: &str, fs: Arc<dyn Fs>, cx: &App) {
|
||||
let flag_name = flag_name.to_owned();
|
||||
update_settings_file(fs, cx, move |content, _| {
|
||||
if let Some(map) = content.feature_flags.as_mut() {
|
||||
map.remove(&flag_name);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// The resolved value of the flag for the current user, taking overrides,
|
||||
/// `enabled_for_all`, staff rules, and server flags into account in that
|
||||
/// order of precedence. Overrides are read directly from
|
||||
/// [`FeatureFlagsSettings`].
|
||||
pub fn try_flag_value<T: FeatureFlag>(&self, cx: &App) -> Option<T::Value> {
|
||||
// `enabled_for_all` always wins, including over user overrides.
|
||||
if T::enabled_for_all() {
|
||||
return Some(T::Value::on_variant());
|
||||
}
|
||||
|
||||
if let Some(override_key) = FeatureFlagsSettings::get_global(cx).overrides.get(T::NAME) {
|
||||
return variant_from_key::<T::Value>(override_key);
|
||||
}
|
||||
|
||||
// Staff default: resolve to the enabled variant.
|
||||
if (cfg!(debug_assertions) || self.staff) && !*ZED_DISABLE_STAFF && T::enabled_for_staff() {
|
||||
return Some(T::Value::on_variant());
|
||||
}
|
||||
|
||||
// Server-delivered flag.
|
||||
if let Some(wire) = self.server_flags.get(T::NAME) {
|
||||
return T::Value::from_wire(wire);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Whether the flag resolves to its "on" value. Best for presence-style
|
||||
/// flags. For enum flags with meaningful non-default variants, prefer
|
||||
/// [`crate::FeatureFlagAppExt::flag_value`].
|
||||
pub fn has_flag<T: FeatureFlag>(&self, cx: &App) -> bool {
|
||||
self.try_flag_value::<T>(cx)
|
||||
.is_some_and(|v| v == T::Value::on_variant())
|
||||
}
|
||||
|
||||
/// Mirrors the resolution order of [`Self::try_flag_value`], but falls
|
||||
/// back to the [`Default`] variant when no rule applies so the UI always
|
||||
/// shows *something* selected — matching what
|
||||
/// [`crate::FeatureFlagAppExt::flag_value`] would return.
|
||||
pub fn resolved_key(&self, descriptor: &FeatureFlagDescriptor, cx: &App) -> &'static str {
|
||||
let on_variant_key = (descriptor.on_variant_key)();
|
||||
|
||||
if (descriptor.enabled_for_all)() {
|
||||
return on_variant_key;
|
||||
}
|
||||
|
||||
if let Some(requested) = FeatureFlagsSettings::get_global(cx)
|
||||
.overrides
|
||||
.get(descriptor.name)
|
||||
{
|
||||
if let Some(variant) = (descriptor.variants)()
|
||||
.into_iter()
|
||||
.find(|v| v.override_key == requested.as_str())
|
||||
{
|
||||
return variant.override_key;
|
||||
}
|
||||
}
|
||||
|
||||
if (cfg!(debug_assertions) || self.staff)
|
||||
&& !*ZED_DISABLE_STAFF
|
||||
&& (descriptor.enabled_for_staff)()
|
||||
{
|
||||
return on_variant_key;
|
||||
}
|
||||
|
||||
if self.server_flags.contains_key(descriptor.name) {
|
||||
return on_variant_key;
|
||||
}
|
||||
|
||||
(descriptor.default_variant_key)()
|
||||
}
|
||||
|
||||
/// Whether this flag is forced on by `enabled_for_all` and therefore not
|
||||
/// user-overridable. The UI uses this to render the row as disabled.
|
||||
pub fn is_forced_on(descriptor: &FeatureFlagDescriptor) -> bool {
|
||||
(descriptor.enabled_for_all)()
|
||||
}
|
||||
|
||||
/// Fallback used when the store isn't installed as a global yet (e.g. very
|
||||
/// early in startup). Matches the pre-existing default behavior.
|
||||
pub fn has_flag_default<T: FeatureFlag>() -> bool {
|
||||
if T::enabled_for_all() {
|
||||
return true;
|
||||
}
|
||||
cfg!(debug_assertions) && T::enabled_for_staff() && !*ZED_DISABLE_STAFF
|
||||
}
|
||||
}
|
||||
|
||||
fn variant_from_key<V: FeatureFlagValue>(key: &str) -> Option<V> {
|
||||
V::all_variants()
|
||||
.iter()
|
||||
.find(|v| v.override_key() == key)
|
||||
.cloned()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{EnumFeatureFlag, FeatureFlag, PresenceFlag};
|
||||
use gpui::UpdateGlobal;
|
||||
use settings::SettingsStore;
|
||||
|
||||
struct DemoFlag;
|
||||
impl FeatureFlag for DemoFlag {
|
||||
const NAME: &'static str = "demo";
|
||||
type Value = PresenceFlag;
|
||||
fn enabled_for_staff() -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, EnumFeatureFlag)]
|
||||
enum Intensity {
|
||||
#[default]
|
||||
Low,
|
||||
High,
|
||||
}
|
||||
|
||||
struct IntensityFlag;
|
||||
impl FeatureFlag for IntensityFlag {
|
||||
const NAME: &'static str = "intensity";
|
||||
type Value = Intensity;
|
||||
fn enabled_for_all() -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fn init_settings_store(cx: &mut App) {
|
||||
let store = SettingsStore::test(cx);
|
||||
cx.set_global(store);
|
||||
SettingsStore::update_global(cx, |store, _| {
|
||||
store.register_setting::<FeatureFlagsSettings>();
|
||||
});
|
||||
}
|
||||
|
||||
fn set_override(name: &str, value: &str, cx: &mut App) {
|
||||
SettingsStore::update_global(cx, |store: &mut SettingsStore, cx| {
|
||||
store.update_user_settings(cx, |content| {
|
||||
content
|
||||
.feature_flags
|
||||
.get_or_insert_default()
|
||||
.insert(name.to_string(), value.to_string());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn server_flag_enables_presence(cx: &mut App) {
|
||||
init_settings_store(cx);
|
||||
let mut store = FeatureFlagStore::default();
|
||||
assert!(!store.has_flag::<DemoFlag>(cx));
|
||||
store.update_server_flags(false, vec!["demo".to_string()]);
|
||||
assert!(store.has_flag::<DemoFlag>(cx));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn off_override_beats_server_flag(cx: &mut App) {
|
||||
init_settings_store(cx);
|
||||
let mut store = FeatureFlagStore::default();
|
||||
store.update_server_flags(false, vec!["demo".to_string()]);
|
||||
set_override(DemoFlag::NAME, "off", cx);
|
||||
assert!(!store.has_flag::<DemoFlag>(cx));
|
||||
assert_eq!(
|
||||
store.try_flag_value::<DemoFlag>(cx),
|
||||
Some(PresenceFlag::Off)
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn enabled_for_all_wins_over_override(cx: &mut App) {
|
||||
init_settings_store(cx);
|
||||
let store = FeatureFlagStore::default();
|
||||
set_override(IntensityFlag::NAME, "high", cx);
|
||||
assert_eq!(
|
||||
store.try_flag_value::<IntensityFlag>(cx),
|
||||
Some(Intensity::Low)
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn enum_override_selects_specific_variant(cx: &mut App) {
|
||||
init_settings_store(cx);
|
||||
let store = FeatureFlagStore::default();
|
||||
// Staff path would normally resolve to `Low`; the override pushes
|
||||
// us to `High` instead.
|
||||
set_override("enum-demo", "high", cx);
|
||||
|
||||
struct EnumDemo;
|
||||
impl FeatureFlag for EnumDemo {
|
||||
const NAME: &'static str = "enum-demo";
|
||||
type Value = Intensity;
|
||||
}
|
||||
|
||||
assert_eq!(store.try_flag_value::<EnumDemo>(cx), Some(Intensity::High));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn unknown_variant_key_resolves_to_none(cx: &mut App) {
|
||||
init_settings_store(cx);
|
||||
let store = FeatureFlagStore::default();
|
||||
set_override("enum-demo", "nonsense", cx);
|
||||
|
||||
struct EnumDemo;
|
||||
impl FeatureFlag for EnumDemo {
|
||||
const NAME: &'static str = "enum-demo";
|
||||
type Value = Intensity;
|
||||
}
|
||||
|
||||
assert_eq!(store.try_flag_value::<EnumDemo>(cx), None);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn on_override_enables_without_server_or_staff(cx: &mut App) {
|
||||
init_settings_store(cx);
|
||||
let store = FeatureFlagStore::default();
|
||||
set_override(DemoFlag::NAME, "on", cx);
|
||||
assert!(store.has_flag::<DemoFlag>(cx));
|
||||
}
|
||||
|
||||
/// No rule applies, so the store's `try_flag_value` returns `None`. The
|
||||
/// `FeatureFlagAppExt::flag_value` path (used by most callers) falls
|
||||
/// back to [`Default`], which for `PresenceFlag` is `Off`.
|
||||
#[gpui::test]
|
||||
fn presence_flag_defaults_to_off(cx: &mut App) {
|
||||
init_settings_store(cx);
|
||||
let store = FeatureFlagStore::default();
|
||||
assert_eq!(store.try_flag_value::<DemoFlag>(cx), None);
|
||||
assert_eq!(PresenceFlag::default(), PresenceFlag::Off);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn on_flags_ready_waits_for_server_flags(cx: &mut gpui::TestAppContext) {
|
||||
use crate::FeatureFlagAppExt;
|
||||
use std::cell::Cell;
|
||||
use std::rc::Rc;
|
||||
|
||||
cx.update(|cx| {
|
||||
init_settings_store(cx);
|
||||
FeatureFlagStore::init(cx);
|
||||
});
|
||||
|
||||
let fired = Rc::new(Cell::new(false));
|
||||
cx.update({
|
||||
let fired = fired.clone();
|
||||
|cx| cx.on_flags_ready(move |_, _| fired.set(true)).detach()
|
||||
});
|
||||
|
||||
// Settings-triggered no-op touch must not fire on_flags_ready.
|
||||
cx.update(|cx| cx.update_default_global::<FeatureFlagStore, _>(|_, _| {}));
|
||||
cx.run_until_parked();
|
||||
assert!(!fired.get());
|
||||
|
||||
// Server flags arrive — now it should fire.
|
||||
cx.update(|cx| cx.update_flags(true, vec![]));
|
||||
cx.run_until_parked();
|
||||
assert!(fired.get());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user