Detailed changes
@@ -6180,7 +6180,23 @@ dependencies = [
name = "feature_flags"
version = "0.1.0"
dependencies = [
+ "collections",
+ "feature_flags_macros",
+ "fs",
"gpui",
+ "inventory",
+ "schemars",
+ "serde_json",
+ "settings",
+]
+
+[[package]]
+name = "feature_flags_macros"
+version = "0.1.0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
]
[[package]]
@@ -9166,6 +9182,7 @@ dependencies = [
"collections",
"dap",
"extension",
+ "feature_flags",
"gpui",
"language",
"parking_lot",
@@ -16120,6 +16137,7 @@ dependencies = [
"db",
"editor",
"extension",
+ "feature_flags",
"fs",
"git",
"gpui",
@@ -73,6 +73,7 @@ members = [
"crates/extension_host",
"crates/extensions_ui",
"crates/feature_flags",
+ "crates/feature_flags_macros",
"crates/feedback",
"crates/file_finder",
"crates/file_icons",
@@ -326,6 +327,7 @@ extension = { path = "crates/extension" }
extension_host = { path = "crates/extension_host" }
extensions_ui = { path = "crates/extensions_ui" }
feature_flags = { path = "crates/feature_flags" }
+feature_flags_macros = { path = "crates/feature_flags_macros" }
feedback = { path = "crates/feedback" }
file_finder = { path = "crates/file_finder" }
file_icons = { path = "crates/file_icons" }
@@ -894,6 +896,7 @@ debug = true
# proc-macros start
gpui_macros = { opt-level = 3 }
derive_refineable = { opt-level = 3 }
+feature_flags_macros = { opt-level = 3 }
settings_macros = { opt-level = 3 }
sqlez_macros = { opt-level = 3, codegen-units = 1 }
ui_macros = { opt-level = 3 }
@@ -346,7 +346,7 @@ pub fn worktree_info_from_thread_paths<S: std::hash::BuildHasher>(
.unwrap_or_default();
linked_short_names.push((short_name.clone(), project_name));
infos.push(ThreadItemWorktreeInfo {
- name: short_name,
+ worktree_name: Some(short_name),
full_path: SharedString::from(folder_path.display().to_string()),
highlight_positions: Vec::new(),
kind: WorktreeKind::Linked,
@@ -357,7 +357,7 @@ pub fn worktree_info_from_thread_paths<S: std::hash::BuildHasher>(
continue;
};
infos.push(ThreadItemWorktreeInfo {
- name: SharedString::from(name.to_string_lossy().to_string()),
+ worktree_name: Some(SharedString::from(name.to_string_lossy().to_string())),
full_path: SharedString::from(folder_path.display().to_string()),
highlight_positions: Vec::new(),
kind: WorktreeKind::Main,
@@ -370,7 +370,10 @@ pub fn worktree_info_from_thread_paths<S: std::hash::BuildHasher>(
// folder paths don't all share the same short name, prefix each
// linked worktree chip with its main project name so the user knows
// which project it belongs to.
- let all_same_name = infos.len() > 1 && infos.iter().all(|i| i.name == infos[0].name);
+ let all_same_name = infos.len() > 1
+ && infos
+ .iter()
+ .all(|i| i.worktree_name == infos[0].worktree_name);
if unique_main_count.len() > 1 && !all_same_name {
for (info, (_short_name, project_name)) in infos
@@ -378,7 +381,9 @@ pub fn worktree_info_from_thread_paths<S: std::hash::BuildHasher>(
.filter(|i| i.kind == WorktreeKind::Linked)
.zip(linked_short_names.iter())
{
- info.name = SharedString::from(format!("{}:{}", project_name, info.name));
+ if let Some(name) = &info.worktree_name {
+ info.worktree_name = Some(SharedString::from(format!("{}:{}", project_name, name)));
+ }
}
}
@@ -1,5 +1,5 @@
use editor::{Editor, EditorEvent};
-use feature_flags::{FeatureFlag, FeatureFlagAppExt as _};
+use feature_flags::{FeatureFlag, FeatureFlagAppExt as _, PresenceFlag, register_feature_flag};
use gpui::{
AppContext, Entity, EventEmitter, FocusHandle, Focusable, ListAlignment, Task, actions,
};
@@ -29,7 +29,9 @@ pub struct TabularDataPreviewFeatureFlag;
impl FeatureFlag for TabularDataPreviewFeatureFlag {
const NAME: &'static str = "tabular-data-preview";
+ type Value = PresenceFlag;
}
+register_feature_flag!(TabularDataPreviewFeatureFlag);
pub struct CsvPreviewView {
pub(crate) engine: TableDataEngine,
@@ -15,7 +15,7 @@ use dap::adapters::DebugAdapterName;
use dap::{DapRegistry, StartDebuggingRequestArguments};
use dap::{client::SessionId, debugger_settings::DebuggerSettings};
use editor::{Editor, MultiBufferOffset, ToPoint};
-use feature_flags::{FeatureFlag, FeatureFlagAppExt as _};
+use feature_flags::{FeatureFlag, FeatureFlagAppExt as _, PresenceFlag, register_feature_flag};
use gpui::{
Action, App, AsyncWindowContext, ClipboardItem, Context, Corner, DismissEvent, Entity,
EntityId, EventEmitter, FocusHandle, Focusable, MouseButton, MouseDownEvent, Point,
@@ -50,7 +50,9 @@ pub struct DebuggerHistoryFeatureFlag;
impl FeatureFlag for DebuggerHistoryFeatureFlag {
const NAME: &'static str = "debugger-history";
+ type Value = PresenceFlag;
}
+register_feature_flag!(DebuggerHistoryFeatureFlag);
const DEBUG_PANEL_KEY: &str = "DebugPanel";
@@ -17,7 +17,7 @@ use copilot::{Copilot, Reinstall, SignIn, SignOut};
use credentials_provider::CredentialsProvider;
use db::kvp::{Dismissable, KeyValueStore};
use edit_prediction_context::{RelatedExcerptStore, RelatedExcerptStoreEvent, RelatedFile};
-use feature_flags::{FeatureFlag, FeatureFlagAppExt as _};
+use feature_flags::{FeatureFlag, FeatureFlagAppExt as _, PresenceFlag, register_feature_flag};
use futures::{
AsyncReadExt as _, FutureExt as _, StreamExt as _,
channel::mpsc::{self, UnboundedReceiver},
@@ -120,7 +120,9 @@ pub struct EditPredictionJumpsFeatureFlag;
impl FeatureFlag for EditPredictionJumpsFeatureFlag {
const NAME: &'static str = "edit_prediction_jumps";
+ type Value = PresenceFlag;
}
+register_feature_flag!(EditPredictionJumpsFeatureFlag);
#[derive(Clone)]
struct EditPredictionStoreGlobal(Entity<EditPredictionStore>);
@@ -115,9 +115,9 @@ fn feature_gate_predict_edits_actions(cx: &mut App) {
})
.detach();
- cx.observe_flag::<PredictEditsRatePredictionsFeatureFlag, _>(move |is_enabled, cx| {
+ cx.observe_flag::<PredictEditsRatePredictionsFeatureFlag, _>(move |value, cx| {
if !DisableAiSettings::get_global(cx).disable_ai {
- if is_enabled {
+ if *value {
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.show_action_types(&rate_completion_action_types);
});
@@ -1,7 +1,7 @@
use buffer_diff::BufferDiff;
use edit_prediction::{EditPrediction, EditPredictionRating, EditPredictionStore};
use editor::{Editor, Inlay, MultiBuffer};
-use feature_flags::FeatureFlag;
+use feature_flags::{FeatureFlag, PresenceFlag, register_feature_flag};
use gpui::{
App, BorderStyle, DismissEvent, EdgesRefinement, Entity, EventEmitter, FocusHandle, Focusable,
Length, StyleRefinement, TextStyleRefinement, Window, actions, prelude::*,
@@ -43,7 +43,9 @@ pub struct PredictEditsRatePredictionsFeatureFlag;
impl FeatureFlag for PredictEditsRatePredictionsFeatureFlag {
const NAME: &'static str = "predict-edits-rate-completions";
+ type Value = PresenceFlag;
}
+register_feature_flag!(PredictEditsRatePredictionsFeatureFlag);
pub struct RatePredictionsModal {
ep_store: Entity<EditPredictionStore>,
@@ -12,4 +12,15 @@ workspace = true
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,4 +1,10 @@
+// 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;
@@ -6,33 +12,98 @@ use std::sync::LazyLock;
use gpui::{App, Context, Global, Subscription, Window};
+pub use feature_flags_macros::EnumFeatureFlag;
pub use flags::*;
-
-#[derive(Default)]
-struct FeatureFlags {
- flags: Vec<String>,
- staff: bool,
-}
+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 FeatureFlags {
- fn has_flag<T: FeatureFlag>(&self) -> bool {
- if T::enabled_for_all() {
- return true;
+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,
}
+ }
+}
- if (cfg!(debug_assertions) || self.staff) && !*ZED_DISABLE_STAFF && T::enabled_for_staff() {
- return true;
+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",
}
+ }
- self.flags.iter().any(|f| f.as_str() == T::NAME)
+ fn label(&self) -> &'static str {
+ match self {
+ PresenceFlag::On => "On",
+ PresenceFlag::Off => "Off",
+ }
}
-}
-impl Global for FeatureFlags {}
+ 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`].
@@ -43,6 +114,10 @@ impl Global for FeatureFlags {}
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
@@ -55,12 +130,23 @@ pub trait FeatureFlag {
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(bool, &mut V, &mut Window, &mut Context<V>) + Send + Sync + 'static;
+ F: Fn(T::Value, &mut V, &mut Window, &mut Context<V>) + Send + Sync + 'static;
fn when_flag_enabled<T: FeatureFlag>(
&mut self,
@@ -75,11 +161,16 @@ where
{
fn observe_flag<T: FeatureFlag, F>(&mut self, window: &Window, callback: F) -> Subscription
where
- F: Fn(bool, &mut V, &mut Window, &mut Context<V>) + 'static,
+ F: Fn(T::Value, &mut V, &mut Window, &mut Context<V>) + 'static,
{
- self.observe_global_in::<FeatureFlags>(window, move |v, window, cx| {
- let feature_flags = cx.global::<FeatureFlags>();
- callback(feature_flags.has_flag::<T>(), v, window, cx);
+ 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);
})
}
@@ -89,8 +180,8 @@ where
callback: impl Fn(&mut V, &mut Window, &mut Context<V>) + Send + Sync + 'static,
) {
if self
- .try_global::<FeatureFlags>()
- .is_some_and(|f| f.has_flag::<T>())
+ .try_global::<FeatureFlagStore>()
+ .is_some_and(|f| f.has_flag::<T>(self))
{
self.defer_in(window, move |view, window, cx| {
callback(view, window, cx);
@@ -98,11 +189,11 @@ where
return;
}
let subscription = Rc::new(RefCell::new(None));
- let inner = self.observe_global_in::<FeatureFlags>(window, {
+ let inner = self.observe_global_in::<FeatureFlagStore>(window, {
let subscription = subscription.clone();
move |v, window, cx| {
- let feature_flags = cx.global::<FeatureFlags>();
- if feature_flags.has_flag::<T>() {
+ let has_flag = cx.global::<FeatureFlagStore>().has_flag::<T>(cx);
+ if has_flag {
callback(v, window, cx);
subscription.take();
}
@@ -121,6 +212,7 @@ 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
@@ -129,33 +221,35 @@ pub trait FeatureFlagAppExt {
fn observe_flag<T: FeatureFlag, F>(&mut self, callback: F) -> Subscription
where
- F: FnMut(bool, &mut App) + 'static;
+ F: FnMut(T::Value, &mut App) + 'static;
}
impl FeatureFlagAppExt for App {
fn update_flags(&mut self, staff: bool, flags: Vec<String>) {
- let feature_flags = self.default_global::<FeatureFlags>();
- feature_flags.staff = staff;
- feature_flags.flags = flags;
+ let store = self.default_global::<FeatureFlagStore>();
+ store.update_server_flags(staff, flags);
}
fn set_staff(&mut self, staff: bool) {
- let feature_flags = self.default_global::<FeatureFlags>();
- feature_flags.staff = staff;
+ let store = self.default_global::<FeatureFlagStore>();
+ store.set_staff(staff);
}
fn has_flag<T: FeatureFlag>(&self) -> bool {
- self.try_global::<FeatureFlags>()
- .map(|flags| flags.has_flag::<T>())
- .unwrap_or_else(|| {
- (cfg!(debug_assertions) && T::enabled_for_staff() && !*ZED_DISABLE_STAFF)
- || T::enabled_for_all()
- })
+ 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::<FeatureFlags>()
- .map(|flags| flags.staff)
+ self.try_global::<FeatureFlagStore>()
+ .map(|store| store.is_staff())
.unwrap_or(false)
}
@@ -163,11 +257,11 @@ impl FeatureFlagAppExt for App {
where
F: FnMut(OnFlagsReady, &mut App) + 'static,
{
- self.observe_global::<FeatureFlags>(move |cx| {
- let feature_flags = cx.global::<FeatureFlags>();
+ self.observe_global::<FeatureFlagStore>(move |cx| {
+ let store = cx.global::<FeatureFlagStore>();
callback(
OnFlagsReady {
- is_staff: feature_flags.staff,
+ is_staff: store.is_staff(),
},
cx,
);
@@ -176,11 +270,16 @@ impl FeatureFlagAppExt for App {
fn observe_flag<T: FeatureFlag, F>(&mut self, mut callback: F) -> Subscription
where
- F: FnMut(bool, &mut App) + 'static,
+ F: FnMut(T::Value, &mut App) + 'static,
{
- self.observe_global::<FeatureFlags>(move |cx| {
- let feature_flags = cx.global::<FeatureFlags>();
- callback(feature_flags.has_flag::<T>(), cx);
+ 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);
})
}
}
@@ -1,26 +1,32 @@
-use crate::FeatureFlag;
+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);
pub struct AgentV2FeatureFlag;
impl FeatureFlag for AgentV2FeatureFlag {
const NAME: &'static str = "agent-v2";
+ type Value = PresenceFlag;
fn enabled_for_staff() -> bool {
true
}
}
+register_feature_flag!(AgentV2FeatureFlag);
/// A feature flag for granting access to beta ACP features.
///
@@ -29,50 +35,83 @@ 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 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 StreamingEditFileToolFeatureFlag;
impl FeatureFlag for StreamingEditFileToolFeatureFlag {
const NAME: &'static str = "streaming-edit-file-tool";
+ type Value = PresenceFlag;
fn enabled_for_staff() -> bool {
true
}
}
+register_feature_flag!(StreamingEditFileToolFeatureFlag);
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 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);
@@ -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."
+ }
+ })
+}
@@ -0,0 +1,374 @@
+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>,
+
+ _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 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.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);
+ }
+}
@@ -0,0 +1,18 @@
+[package]
+name = "feature_flags_macros"
+version = "0.1.0"
+edition.workspace = true
+publish.workspace = true
+license = "GPL-3.0-or-later"
+
+[lib]
+path = "src/feature_flags_macros.rs"
+proc-macro = true
+
+[lints]
+workspace = true
+
+[dependencies]
+proc-macro2.workspace = true
+quote.workspace = true
+syn.workspace = true
@@ -0,0 +1 @@
+../../LICENSE-GPL
@@ -0,0 +1,190 @@
+use proc_macro::TokenStream;
+use proc_macro2::{Span, TokenStream as TokenStream2};
+use quote::quote;
+use syn::{Data, DeriveInput, Fields, Ident, LitStr, parse_macro_input};
+
+/// Derives [`feature_flags::FeatureFlagValue`] for a unit-only enum.
+///
+/// Exactly one variant must be marked with `#[default]`. The default variant
+/// is the one returned when the feature flag is announced by the server,
+/// enabled for all users, or enabled by the staff rule — it's the "on"
+/// value, and also the fallback for `from_wire`.
+///
+/// The generated impl derives:
+///
+/// * `all_variants` — every variant, in source order.
+/// * `override_key` — the variant name, lower-cased with dashes between
+/// PascalCase word boundaries (e.g. `NewWorktree` → `"new-worktree"`).
+/// * `label` — the variant name with PascalCase boundaries expanded to
+/// spaces (e.g. `NewWorktree` → `"New Worktree"`).
+/// * `from_wire` — always returns the default variant, since today the
+/// server wire format is just presence and does not carry a variant.
+///
+/// ## Example
+///
+/// ```ignore
+/// #[derive(Clone, Copy, PartialEq, Eq, Debug, EnumFeatureFlag)]
+/// enum Intensity {
+/// #[default]
+/// Low,
+/// High,
+/// }
+/// ```
+// `attributes(default)` lets users write `#[default]` on a variant even when
+// they're not also deriving `Default`. If `#[derive(Default)]` is present in
+// the same list, it reuses the same attribute — there's no conflict, because
+// helper attributes aren't consumed.
+#[proc_macro_derive(EnumFeatureFlag, attributes(default))]
+pub fn derive_enum_feature_flag(input: TokenStream) -> TokenStream {
+ let input = parse_macro_input!(input as DeriveInput);
+ match expand(&input) {
+ Ok(tokens) => tokens.into(),
+ Err(e) => e.to_compile_error().into(),
+ }
+}
+
+fn expand(input: &DeriveInput) -> syn::Result<TokenStream2> {
+ let Data::Enum(data) = &input.data else {
+ return Err(syn::Error::new_spanned(
+ input,
+ "EnumFeatureFlag can only be derived for enums",
+ ));
+ };
+
+ if data.variants.is_empty() {
+ return Err(syn::Error::new_spanned(
+ input,
+ "EnumFeatureFlag requires at least one variant",
+ ));
+ }
+
+ let mut default_ident: Option<&Ident> = None;
+ let mut variant_idents: Vec<&Ident> = Vec::new();
+
+ for variant in &data.variants {
+ if !matches!(variant.fields, Fields::Unit) {
+ return Err(syn::Error::new_spanned(
+ variant,
+ "EnumFeatureFlag only supports unit variants (no fields)",
+ ));
+ }
+ if has_default_attr(variant) {
+ if default_ident.is_some() {
+ return Err(syn::Error::new_spanned(
+ variant,
+ "only one variant may be marked with #[default]",
+ ));
+ }
+ default_ident = Some(&variant.ident);
+ }
+ variant_idents.push(&variant.ident);
+ }
+
+ let Some(default_ident) = default_ident else {
+ return Err(syn::Error::new_spanned(
+ input,
+ "EnumFeatureFlag requires exactly one variant to be marked with #[default]",
+ ));
+ };
+
+ let name = &input.ident;
+ let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
+
+ let override_key_arms = variant_idents.iter().map(|variant| {
+ let key = LitStr::new(&to_kebab_case(&variant.to_string()), Span::call_site());
+ quote! { #name::#variant => #key }
+ });
+
+ let label_arms = variant_idents.iter().map(|variant| {
+ let label = LitStr::new(&to_space_separated(&variant.to_string()), Span::call_site());
+ quote! { #name::#variant => #label }
+ });
+
+ let all_variants = variant_idents.iter().map(|v| quote! { #name::#v });
+
+ Ok(quote! {
+ impl #impl_generics ::std::default::Default for #name #ty_generics #where_clause {
+ fn default() -> Self {
+ #name::#default_ident
+ }
+ }
+
+ impl #impl_generics ::feature_flags::FeatureFlagValue for #name #ty_generics #where_clause {
+ fn all_variants() -> &'static [Self] {
+ &[ #( #all_variants ),* ]
+ }
+
+ fn override_key(&self) -> &'static str {
+ match self {
+ #( #override_key_arms ),*
+ }
+ }
+
+ fn label(&self) -> &'static str {
+ match self {
+ #( #label_arms ),*
+ }
+ }
+
+ fn from_wire(_: &str) -> ::std::option::Option<Self> {
+ ::std::option::Option::Some(#name::#default_ident)
+ }
+ }
+ })
+}
+
+fn has_default_attr(variant: &syn::Variant) -> bool {
+ variant.attrs.iter().any(|a| a.path().is_ident("default"))
+}
+
+/// Converts a PascalCase identifier to lowercase kebab-case.
+///
+/// `"NewWorktree"` → `"new-worktree"`, `"Low"` → `"low"`,
+/// `"HTTPServer"` → `"httpserver"` (acronyms are not split — keep variant
+/// names descriptive to avoid this).
+fn to_kebab_case(ident: &str) -> String {
+ let mut out = String::with_capacity(ident.len() + 4);
+ for (i, ch) in ident.chars().enumerate() {
+ if ch.is_ascii_uppercase() {
+ if i != 0 {
+ out.push('-');
+ }
+ out.push(ch.to_ascii_lowercase());
+ } else {
+ out.push(ch);
+ }
+ }
+ out
+}
+
+/// Converts a PascalCase identifier to space-separated word form for display.
+///
+/// `"NewWorktree"` → `"New Worktree"`, `"Low"` → `"Low"`.
+fn to_space_separated(ident: &str) -> String {
+ let mut out = String::with_capacity(ident.len() + 4);
+ for (i, ch) in ident.chars().enumerate() {
+ if ch.is_ascii_uppercase() && i != 0 {
+ out.push(' ');
+ }
+ out.push(ch);
+ }
+ out
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn kebab_case() {
+ assert_eq!(to_kebab_case("Low"), "low");
+ assert_eq!(to_kebab_case("NewWorktree"), "new-worktree");
+ assert_eq!(to_kebab_case("A"), "a");
+ }
+
+ #[test]
+ fn space_separated() {
+ assert_eq!(to_space_separated("Low"), "Low");
+ assert_eq!(to_space_separated("NewWorktree"), "New Worktree");
+ }
+}
@@ -14,12 +14,17 @@ path = "src/json_schema_store.rs"
[features]
default = []
+[dev-dependencies]
+gpui = { workspace = true, features = ["test-support"] }
+settings = { workspace = true, features = ["test-support"] }
+
[dependencies]
anyhow.workspace = true
collections.workspace = true
dap.workspace = true
parking_lot.workspace = true
extension.workspace = true
+feature_flags.workspace = true
gpui.workspace = true
language.workspace = true
paths.workspace = true
@@ -352,13 +352,16 @@ async fn resolve_dynamic_schema(
let icon_theme_names = icon_theme_names.as_slice();
let theme_names = theme_names.as_slice();
- settings::SettingsStore::json_schema(&settings::SettingsJsonSchemaParams {
- language_names,
- font_names,
- theme_names,
- icon_theme_names,
- lsp_adapter_names: &lsp_adapter_names,
- })
+ let mut schema =
+ settings::SettingsStore::json_schema(&settings::SettingsJsonSchemaParams {
+ language_names,
+ font_names,
+ theme_names,
+ icon_theme_names,
+ lsp_adapter_names: &lsp_adapter_names,
+ });
+ inject_feature_flags_schema(&mut schema);
+ schema
})
}
"project_settings" => {
@@ -374,16 +377,19 @@ async fn resolve_dynamic_schema(
.map(|name| name.to_string())
.collect::<Vec<_>>();
- settings::SettingsStore::project_json_schema(&settings::SettingsJsonSchemaParams {
- language_names,
- lsp_adapter_names: &lsp_adapter_names,
- // These are not allowed in project-specific settings but
- // they're still fields required by the
- // `SettingsJsonSchemaParams` struct.
- font_names: &[],
- theme_names: &[],
- icon_theme_names: &[],
- })
+ let mut schema =
+ settings::SettingsStore::project_json_schema(&settings::SettingsJsonSchemaParams {
+ language_names,
+ lsp_adapter_names: &lsp_adapter_names,
+ // These are not allowed in project-specific settings but
+ // they're still fields required by the
+ // `SettingsJsonSchemaParams` struct.
+ font_names: &[],
+ theme_names: &[],
+ icon_theme_names: &[],
+ });
+ inject_feature_flags_schema(&mut schema);
+ schema
}
"debug_tasks" => {
let adapter_schemas = cx.read_global::<dap::DapRegistry, _>(|dap_registry, _| {
@@ -513,6 +519,21 @@ pub fn all_schema_file_associations(
file_associations
}
+/// Swaps the placeholder [`settings::FeatureFlagsMap`] subschema produced by
+/// schemars for an enriched one that lists each known flag's variants. The
+/// placeholder is registered in the `settings_content` crate so the
+/// `settings` crate doesn't need a reverse dependency on `feature_flags`.
+fn inject_feature_flags_schema(schema: &mut serde_json::Value) {
+ use schemars::JsonSchema;
+
+ let Some(defs) = schema.get_mut("$defs").and_then(|d| d.as_object_mut()) else {
+ return;
+ };
+ let schema_name = settings::FeatureFlagsMap::schema_name();
+ let enriched = feature_flags::generate_feature_flags_schema().to_value();
+ defs.insert(schema_name.into_owned(), enriched);
+}
+
fn generate_jsonc_schema() -> serde_json::Value {
let generator = schemars::generate::SchemaSettings::draft2019_09()
.with_transform(DefaultDenyUnknownFields)
@@ -68,8 +68,8 @@ pub fn init(cx: &mut App) {
}
cx.observe_flag::<NotebookFeatureFlag, _>({
- move |is_enabled, cx| {
- if is_enabled {
+ move |flag, cx| {
+ if *flag {
workspace::register_project_item::<NotebookEditor>(cx);
} else {
// todo: there is no way to unregister a project item, so if the feature flag
@@ -220,6 +220,7 @@ impl VsCodeSettings {
workspace: self.workspace_settings_content(),
which_key: None,
modeline_lines: None,
+ feature_flags: None,
}
}
@@ -211,6 +211,45 @@ pub struct SettingsContent {
///
/// Default: 5
pub modeline_lines: Option<usize>,
+
+ /// Local overrides for feature flags, keyed by flag name.
+ pub feature_flags: Option<FeatureFlagsMap>,
+}
+
+#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, MergeFrom)]
+#[serde(transparent)]
+pub struct FeatureFlagsMap(pub HashMap<String, String>);
+
+// A manual `JsonSchema` impl keeps this type's schema registered under a
+// unique name. The derived impl on a `#[serde(transparent)]` newtype around
+// `HashMap<String, String>` would inline to the map's own schema name (`Map_of_string`),
+// which is shared with every other `HashMap<String, String>` setting field in
+// `SettingsContent`. A named placeholder lets `json_schema_store` find and
+// replace just this field's schema at runtime without clobbering the others.
+impl JsonSchema for FeatureFlagsMap {
+ fn schema_name() -> std::borrow::Cow<'static, str> {
+ "FeatureFlagsMap".into()
+ }
+
+ fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
+ schemars::json_schema!({
+ "type": "object",
+ "additionalProperties": { "type": "string" }
+ })
+ }
+}
+
+impl std::ops::Deref for FeatureFlagsMap {
+ type Target = HashMap<String, String>;
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl std::ops::DerefMut for FeatureFlagsMap {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.0
+ }
}
impl SettingsContent {
@@ -62,7 +62,7 @@ macro_rules! concat_sections {
}
pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
- vec![
+ let mut pages = vec![
general_page(cx),
appearance_page(),
keymap_page(),
@@ -77,7 +77,32 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
collaboration_page(),
ai_page(cx),
network_page(),
- ]
+ ];
+
+ use feature_flags::FeatureFlagAppExt as _;
+ if cx.is_staff() || cfg!(debug_assertions) {
+ pages.push(developer_page());
+ }
+
+ pages
+}
+
+fn developer_page() -> SettingsPage {
+ SettingsPage {
+ title: "Developer",
+ items: Box::new([
+ SettingsPageItem::SectionHeader("Feature Flags"),
+ SettingsPageItem::SubPageLink(SubPageLink {
+ title: "Feature Flags".into(),
+ r#type: Default::default(),
+ description: None,
+ json_path: Some("feature_flags"),
+ in_json: true,
+ files: USER,
+ render: crate::pages::render_feature_flags_page,
+ }),
+ ]),
+ }
}
fn general_page(cx: &App) -> SettingsPage {
@@ -1,6 +1,7 @@
mod audio_input_output_setup;
mod audio_test_window;
mod edit_prediction_provider_setup;
+mod feature_flags;
mod tool_permissions_setup;
pub(crate) use audio_input_output_setup::{
@@ -8,6 +9,7 @@ pub(crate) use audio_input_output_setup::{
};
pub(crate) use audio_test_window::open_audio_test_window;
pub(crate) use edit_prediction_provider_setup::render_edit_prediction_setup_page;
+pub(crate) use feature_flags::render_feature_flags_page;
pub(crate) use tool_permissions_setup::render_tool_permissions_setup_page;
pub use tool_permissions_setup::{
@@ -0,0 +1,132 @@
+use feature_flags::{FeatureFlagDescriptor, FeatureFlagStore, FeatureFlagVariant};
+use fs::Fs;
+use gpui::{ScrollHandle, prelude::*};
+use ui::{Checkbox, ToggleState, prelude::*};
+
+use crate::SettingsWindow;
+
+pub(crate) fn render_feature_flags_page(
+ _settings_window: &SettingsWindow,
+ scroll_handle: &ScrollHandle,
+ _window: &mut Window,
+ cx: &mut Context<SettingsWindow>,
+) -> AnyElement {
+ // Sort by flag name so the list is stable between renders even though
+ // `inventory::iter` order depends on link order.
+ let mut descriptors: Vec<&'static FeatureFlagDescriptor> =
+ FeatureFlagStore::known_flags().collect();
+ descriptors.sort_by_key(|descriptor| descriptor.name);
+
+ v_flex()
+ .id("feature-flags-page")
+ .min_w_0()
+ .size_full()
+ .pt_2p5()
+ .px_8()
+ .pb_16()
+ .gap_4()
+ .overflow_y_scroll()
+ .track_scroll(scroll_handle)
+ .children(
+ descriptors
+ .into_iter()
+ .map(|descriptor| render_flag_row(descriptor, cx)),
+ )
+ .into_any_element()
+}
+
+fn render_flag_row(
+ descriptor: &'static FeatureFlagDescriptor,
+ cx: &mut Context<SettingsWindow>,
+) -> AnyElement {
+ let forced_on = FeatureFlagStore::is_forced_on(descriptor);
+ let resolved = cx.global::<FeatureFlagStore>().resolved_key(descriptor, cx);
+ let has_override = FeatureFlagStore::override_for(descriptor.name, cx).is_some();
+
+ let header =
+ h_flex()
+ .justify_between()
+ .items_center()
+ .child(
+ h_flex()
+ .gap_2()
+ .child(Label::new(descriptor.name).size(LabelSize::Default).color(
+ if forced_on {
+ Color::Muted
+ } else {
+ Color::Default
+ },
+ ))
+ .when(forced_on, |this| {
+ this.child(
+ Label::new("enabled for all")
+ .size(LabelSize::Small)
+ .color(Color::Muted),
+ )
+ }),
+ )
+ .when(has_override && !forced_on, |this| {
+ let name = descriptor.name;
+ this.child(
+ Button::new(SharedString::from(format!("reset-{}", name)), "Reset")
+ .label_size(LabelSize::Small)
+ .on_click(cx.listener(move |_, _, _, cx| {
+ FeatureFlagStore::clear_override(name, <dyn Fs>::global(cx), cx);
+ })),
+ )
+ });
+
+ v_flex()
+ .id(SharedString::from(format!("flag-row-{}", descriptor.name)))
+ .gap_1()
+ .child(header)
+ .child(render_flag_variants(descriptor, resolved, forced_on, cx))
+ .into_any_element()
+}
+
+fn render_flag_variants(
+ descriptor: &'static FeatureFlagDescriptor,
+ resolved: &'static str,
+ forced_on: bool,
+ cx: &mut Context<SettingsWindow>,
+) -> impl IntoElement {
+ let variants: Vec<FeatureFlagVariant> = (descriptor.variants)();
+
+ let row_items = variants.into_iter().map({
+ let name = descriptor.name;
+ move |variant| {
+ let key = variant.override_key;
+ let label = variant.label;
+ let selected = resolved == key;
+ let state = if selected {
+ ToggleState::Selected
+ } else {
+ ToggleState::Unselected
+ };
+ let checkbox_id = SharedString::from(format!("{}-{}", name, key));
+ let disabled = forced_on;
+ let mut checkbox = Checkbox::new(ElementId::from(checkbox_id), state)
+ .label(label)
+ .disabled(disabled);
+ if !disabled {
+ checkbox =
+ checkbox.on_click(cx.listener(move |_, new_state: &ToggleState, _, cx| {
+ // Clicking an already-selected option is a no-op rather than a
+ // "deselect" — there's no valid "nothing selected" state.
+ if *new_state == ToggleState::Unselected {
+ return;
+ }
+ FeatureFlagStore::set_override(
+ name,
+ key.to_string(),
+ <dyn Fs>::global(cx),
+ cx,
+ );
+ }));
+ }
+ checkbox.into_any_element()
+ }
+ });
+
+ h_flex().gap_4().flex_wrap().children(row_items)
+}
@@ -1521,6 +1521,17 @@ impl SettingsWindow {
})
.detach();
+ use feature_flags::FeatureFlagAppExt as _;
+ let mut last_is_staff = cx.is_staff();
+ cx.observe_global_in::<feature_flags::FeatureFlagStore>(window, move |this, window, cx| {
+ let is_staff = cx.is_staff();
+ if is_staff != last_is_staff {
+ last_is_staff = is_staff;
+ this.rebuild_pages(window, cx);
+ }
+ })
+ .detach();
+
cx.on_window_closed(|cx, _window_id| {
if let Some(existing_window) = cx
.windows()
@@ -2143,6 +2154,15 @@ impl SettingsWindow {
cx.notify();
}
+ fn rebuild_pages(&mut self, window: &mut Window, cx: &mut Context<SettingsWindow>) {
+ self.pages.clear();
+ self.navbar_entries.clear();
+ self.navbar_focus_subscriptions.clear();
+ self.content_handles.clear();
+ self.build_ui(window, cx);
+ self.build_search_index();
+ }
+
#[track_caller]
fn fetch_files(&mut self, window: &mut Window, cx: &mut Context<SettingsWindow>) {
self.worktree_root_dirs.clear();
@@ -24,6 +24,7 @@ agent_ui = { workspace = true, features = ["audio"] }
anyhow.workspace = true
chrono.workspace = true
editor.workspace = true
+feature_flags.workspace = true
fs.workspace = true
git.workspace = true
gpui.workspace = true
@@ -18,6 +18,9 @@ use agent_ui::{
};
use chrono::{DateTime, Utc};
use editor::Editor;
+use feature_flags::{
+ AgentThreadWorktreeLabel, AgentThreadWorktreeLabelFlag, FeatureFlag, FeatureFlagAppExt as _,
+};
use gpui::{
Action as _, AnyElement, App, ClickEvent, Context, DismissEvent, Entity, EntityId, FocusHandle,
Focusable, KeyContext, ListState, Modifiers, Pixels, Render, SharedString, Task, WeakEntity,
@@ -391,6 +394,30 @@ fn workspace_menu_worktree_labels(
.collect()
}
+fn apply_worktree_label_mode(
+ mut worktrees: Vec<ThreadItemWorktreeInfo>,
+ mode: AgentThreadWorktreeLabel,
+) -> Vec<ThreadItemWorktreeInfo> {
+ match mode {
+ AgentThreadWorktreeLabel::Both => {}
+ AgentThreadWorktreeLabel::Worktree => {
+ for wt in &mut worktrees {
+ wt.branch_name = None;
+ }
+ }
+ AgentThreadWorktreeLabel::Branch => {
+ for wt in &mut worktrees {
+ // Fall back to showing the worktree name when no branch is
+ // known; an empty chip would be worse than a mismatched icon.
+ if wt.branch_name.is_some() {
+ wt.worktree_name = None;
+ }
+ }
+ }
+ }
+ worktrees
+}
+
/// Shows a [`RemoteConnectionModal`] on the given workspace and establishes
/// an SSH connection. Suitable for passing to
/// [`MultiWorkspace::find_or_create_workspace`] as the `connect_remote`
@@ -454,6 +481,8 @@ impl Sidebar {
cx.on_focus_in(&focus_handle, window, Self::focus_in)
.detach();
+ AgentThreadWorktreeLabelFlag::watch(cx);
+
let filter_editor = cx.new(|cx| {
let mut editor = Editor::single_line(window, cx);
editor.set_use_modal_editing(true);
@@ -1246,7 +1275,10 @@ impl Sidebar {
}
let mut worktree_matched = false;
for worktree in &mut thread.worktrees {
- if let Some(positions) = fuzzy_match_positions(&query, &worktree.name) {
+ let Some(name) = worktree.worktree_name.as_ref() else {
+ continue;
+ };
+ if let Some(positions) = fuzzy_match_positions(&query, name) {
worktree.highlight_positions = positions;
worktree_matched = true;
}
@@ -3835,6 +3867,11 @@ impl Sidebar {
let is_remote = thread.workspace.is_remote(cx);
+ let worktrees = apply_worktree_label_mode(
+ thread.worktrees.clone(),
+ cx.flag_value::<AgentThreadWorktreeLabelFlag>(),
+ );
+
ThreadItem::new(id, title)
.base_bg(sidebar_bg)
.icon(thread.icon)
@@ -3843,7 +3880,7 @@ impl Sidebar {
.when_some(thread.icon_from_external_svg.clone(), |this, svg| {
this.custom_icon_from_external_svg(svg)
})
- .worktrees(thread.worktrees.clone())
+ .worktrees(worktrees)
.timestamp(timestamp)
.highlight_positions(thread.highlight_positions.to_vec())
.title_generating(thread.is_title_generating)
@@ -449,9 +449,12 @@ fn format_linked_worktree_chips(worktrees: &[ThreadItemWorktreeInfo]) -> String
if wt.kind == ui::WorktreeKind::Main {
continue;
}
- if !seen.contains(&wt.name) {
- seen.push(wt.name.clone());
- chips.push(format!("{{{}}}", wt.name));
+ let Some(name) = wt.worktree_name.as_ref() else {
+ continue;
+ };
+ if !seen.contains(name) {
+ seen.push(name.clone());
+ chips.push(format!("{{{}}}", name));
}
}
if chips.is_empty() {
@@ -3837,7 +3840,10 @@ async fn test_clicking_worktree_thread_does_not_briefly_render_as_separate_proje
}
ListEntry::Thread(thread)
if thread.metadata.title.as_ref().map(|t| t.as_ref()) == Some("WT Thread")
- && thread.worktrees.first().map(|wt| wt.name.as_ref())
+ && thread
+ .worktrees
+ .first()
+ .and_then(|wt| wt.worktree_name.as_ref().map(|n| n.as_ref()))
== Some("wt-feature-a") =>
{
saw_expected_thread = true;
@@ -3847,7 +3853,7 @@ async fn test_clicking_worktree_thread_does_not_briefly_render_as_separate_proje
let worktree_name = thread
.worktrees
.first()
- .map(|wt| wt.name.as_ref())
+ .and_then(|wt| wt.worktree_name.as_ref().map(|n| n.as_ref()))
.unwrap_or("<none>");
panic!(
"unexpected sidebar thread while opening linked worktree thread: title=`{}`, worktree=`{}`",
@@ -10426,7 +10432,7 @@ fn test_worktree_info_branch_names_for_main_worktrees() {
assert_eq!(infos.len(), 1);
assert_eq!(infos[0].kind, ui::WorktreeKind::Main);
assert_eq!(infos[0].branch_name, Some(SharedString::from("feature-x")));
- assert_eq!(infos[0].name, SharedString::from("myapp"));
+ assert_eq!(infos[0].worktree_name, Some(SharedString::from("myapp")));
}
#[test]
@@ -10463,7 +10469,7 @@ fn test_worktree_info_missing_branch_returns_none() {
assert_eq!(infos.len(), 1);
assert_eq!(infos[0].kind, ui::WorktreeKind::Main);
assert_eq!(infos[0].branch_name, None);
- assert_eq!(infos[0].name, SharedString::from("myapp"));
+ assert_eq!(infos[0].worktree_name, Some(SharedString::from("myapp")));
}
#[gpui::test]
@@ -22,13 +22,13 @@ pub enum WorktreeKind {
Linked,
}
-#[derive(Clone)]
+#[derive(Clone, Default)]
pub struct ThreadItemWorktreeInfo {
- pub name: SharedString,
+ pub worktree_name: Option<SharedString>,
+ pub branch_name: Option<SharedString>,
pub full_path: SharedString,
pub highlight_positions: Vec<usize>,
pub kind: WorktreeKind,
- pub branch_name: Option<SharedString>,
}
#[derive(IntoElement, RegisterComponent)]
@@ -371,6 +371,7 @@ impl RenderOnce for ThreadItem {
.worktrees
.into_iter()
.filter(|wt| wt.kind == WorktreeKind::Linked)
+ .filter(|wt| wt.worktree_name.is_some() || wt.branch_name.is_some())
.collect();
let has_worktree = !linked_worktrees.is_empty();
@@ -470,42 +471,68 @@ impl RenderOnce for ThreadItem {
})
.children(
linked_worktrees.into_iter().map(|wt| {
- let worktree_label = if wt.highlight_positions.is_empty() {
- Label::new(wt.name)
+ let worktree_label = wt.worktree_name.clone().map(|name| {
+ if wt.highlight_positions.is_empty() {
+ Label::new(name)
+ .size(LabelSize::Small)
+ .color(Color::Muted)
+ .truncate()
+ .into_any_element()
+ } else {
+ HighlightedLabel::new(
+ name,
+ wt.highlight_positions.clone(),
+ )
.size(LabelSize::Small)
.color(Color::Muted)
.truncate()
.into_any_element()
+ }
+ });
+
+ // When only the branch is shown, lead with a branch icon;
+ // otherwise keep the worktree icon (which "covers" both the
+ // worktree and any accompanying branch).
+ let chip_icon = if wt.worktree_name.is_none()
+ && wt.branch_name.is_some()
+ {
+ IconName::GitBranch
} else {
- HighlightedLabel::new(wt.name, wt.highlight_positions)
+ IconName::GitWorktree
+ };
+
+ let branch_label = wt.branch_name.map(|branch| {
+ Label::new(branch)
.size(LabelSize::Small)
.color(Color::Muted)
.truncate()
.into_any_element()
- };
+ });
+
+ let show_separator =
+ worktree_label.is_some() && branch_label.is_some();
h_flex()
.min_w_0()
.gap_0p5()
.child(
- Icon::new(IconName::GitWorktree)
+ Icon::new(chip_icon)
.size(IconSize::XSmall)
.color(Color::Muted),
)
- .child(worktree_label)
- .when_some(wt.branch_name, |this, branch| {
+ .when_some(worktree_label, |this, label| {
+ this.child(label)
+ })
+ .when(show_separator, |this| {
this.child(
Label::new("/")
.size(LabelSize::Small)
.color(separator_color)
.flex_shrink_0(),
)
- .child(
- Label::new(branch)
- .size(LabelSize::Small)
- .color(Color::Muted)
- .truncate(),
- )
+ })
+ .when_some(branch_label, |this, label| {
+ this.child(label)
})
}),
)
@@ -628,7 +655,7 @@ impl Component for ThreadItem {
.icon(IconName::AiClaude)
.timestamp("2w")
.worktrees(vec![ThreadItemWorktreeInfo {
- name: "link-agent-panel".into(),
+ worktree_name: Some("link-agent-panel".into()),
full_path: "link-agent-panel".into(),
highlight_positions: Vec::new(),
kind: WorktreeKind::Linked,
@@ -656,7 +683,7 @@ impl Component for ThreadItem {
ThreadItem::new("ti-5b", "Full metadata example")
.icon(IconName::AiClaude)
.worktrees(vec![ThreadItemWorktreeInfo {
- name: "my-project".into(),
+ worktree_name: Some("my-project".into()),
full_path: "my-project".into(),
highlight_positions: Vec::new(),
kind: WorktreeKind::Linked,
@@ -675,7 +702,7 @@ impl Component for ThreadItem {
ThreadItem::new("ti-5c", "Full metadata with branch")
.icon(IconName::AiClaude)
.worktrees(vec![ThreadItemWorktreeInfo {
- name: "my-project".into(),
+ worktree_name: Some("my-project".into()),
full_path: "/worktrees/my-project/zed".into(),
highlight_positions: Vec::new(),
kind: WorktreeKind::Linked,
@@ -694,7 +721,7 @@ impl Component for ThreadItem {
ThreadItem::new("ti-5d", "Metadata overflow with long branch name")
.icon(IconName::AiClaude)
.worktrees(vec![ThreadItemWorktreeInfo {
- name: "my-project".into(),
+ worktree_name: Some("my-project".into()),
full_path: "/worktrees/my-project/zed".into(),
highlight_positions: Vec::new(),
kind: WorktreeKind::Linked,
@@ -713,7 +740,7 @@ impl Component for ThreadItem {
ThreadItem::new("ti-5e", "Main worktree branch with diff stats")
.icon(IconName::ZedAgent)
.worktrees(vec![ThreadItemWorktreeInfo {
- name: "zed".into(),
+ worktree_name: Some("zed".into()),
full_path: "/projects/zed".into(),
highlight_positions: Vec::new(),
kind: WorktreeKind::Main,
@@ -732,7 +759,9 @@ impl Component for ThreadItem {
ThreadItem::new("ti-5f", "Thread with a very long worktree name")
.icon(IconName::AiClaude)
.worktrees(vec![ThreadItemWorktreeInfo {
- name: "very-long-worktree-name-that-should-truncate".into(),
+ worktree_name: Some(
+ "very-long-worktree-name-that-should-truncate".into(),
+ ),
full_path: "/worktrees/very-long-worktree-name/zed".into(),
highlight_positions: Vec::new(),
kind: WorktreeKind::Linked,
@@ -749,7 +778,7 @@ impl Component for ThreadItem {
ThreadItem::new("ti-5g", "Filtered thread with highlighted worktree")
.icon(IconName::AiClaude)
.worktrees(vec![ThreadItemWorktreeInfo {
- name: "jade-glen".into(),
+ worktree_name: Some("jade-glen".into()),
full_path: "/worktrees/jade-glen/zed".into(),
highlight_positions: vec![0, 1, 2, 3],
kind: WorktreeKind::Linked,
@@ -767,14 +796,14 @@ impl Component for ThreadItem {
.icon(IconName::AiClaude)
.worktrees(vec![
ThreadItemWorktreeInfo {
- name: "jade-glen".into(),
+ worktree_name: Some("jade-glen".into()),
full_path: "/worktrees/jade-glen/zed".into(),
highlight_positions: Vec::new(),
kind: WorktreeKind::Linked,
branch_name: None,
},
ThreadItemWorktreeInfo {
- name: "fawn-otter".into(),
+ worktree_name: Some("fawn-otter".into()),
full_path: "/worktrees/fawn-otter/zed-slides".into(),
highlight_positions: Vec::new(),
kind: WorktreeKind::Linked,
@@ -793,14 +822,14 @@ impl Component for ThreadItem {
.icon(IconName::ZedAgent)
.worktrees(vec![
ThreadItemWorktreeInfo {
- name: "jade-glen".into(),
+ worktree_name: Some("jade-glen".into()),
full_path: "/worktrees/jade-glen/zed".into(),
highlight_positions: Vec::new(),
kind: WorktreeKind::Linked,
branch_name: Some("fix".into()),
},
ThreadItemWorktreeInfo {
- name: "fawn-otter".into(),
+ worktree_name: Some("fawn-otter".into()),
full_path: "/worktrees/fawn-otter/zed-slides".into(),
highlight_positions: Vec::new(),
kind: WorktreeKind::Linked,
@@ -819,7 +848,7 @@ impl Component for ThreadItem {
.icon(IconName::AiClaude)
.project_name("my-remote-server")
.worktrees(vec![ThreadItemWorktreeInfo {
- name: "jade-glen".into(),
+ worktree_name: Some("jade-glen".into()),
full_path: "/worktrees/jade-glen/zed".into(),
highlight_positions: Vec::new(),
kind: WorktreeKind::Linked,
@@ -840,7 +869,7 @@ impl Component for ThreadItem {
PathBuf::from("/projects/zed-slides"),
]))
.worktrees(vec![ThreadItemWorktreeInfo {
- name: "jade-glen".into(),
+ worktree_name: Some("jade-glen".into()),
full_path: "/worktrees/jade-glen/zed".into(),
highlight_positions: Vec::new(),
kind: WorktreeKind::Linked,
@@ -858,7 +887,7 @@ impl Component for ThreadItem {
.icon(IconName::ZedAgent)
.project_name("remote-dev")
.worktrees(vec![ThreadItemWorktreeInfo {
- name: "my-worktree".into(),
+ worktree_name: Some("my-worktree".into()),
full_path: "/worktrees/my-worktree/zed".into(),
highlight_positions: Vec::new(),
kind: WorktreeKind::Linked,
@@ -555,6 +555,7 @@ fn main() {
debugger_ui::init(cx);
debugger_tools::init(cx);
client::init(&client, cx);
+ feature_flags::FeatureFlagStore::init(cx);
let system_id = cx.foreground_executor().block_on(system_id).ok();
let installation_id = cx.foreground_executor().block_on(installation_id).ok();
@@ -2914,7 +2914,7 @@ impl gpui::Render for ThreadItemBranchNameTestView {
.icon(IconName::AiClaude)
.timestamp("5m")
.worktrees(vec![ThreadItemWorktreeInfo {
- name: "jade-glen".into(),
+ worktree_name: Some("jade-glen".into()),
full_path: "/worktrees/jade-glen/zed".into(),
highlight_positions: Vec::new(),
kind: WorktreeKind::Linked,
@@ -2931,7 +2931,7 @@ impl gpui::Render for ThreadItemBranchNameTestView {
.icon(IconName::AiClaude)
.timestamp("1h")
.worktrees(vec![ThreadItemWorktreeInfo {
- name: "focal-arrow".into(),
+ worktree_name: Some("focal-arrow".into()),
full_path: "/worktrees/focal-arrow/zed".into(),
highlight_positions: Vec::new(),
kind: WorktreeKind::Linked,
@@ -2946,7 +2946,7 @@ impl gpui::Render for ThreadItemBranchNameTestView {
.icon(IconName::ZedAgent)
.timestamp("2d")
.worktrees(vec![ThreadItemWorktreeInfo {
- name: "zed".into(),
+ worktree_name: Some("zed".into()),
full_path: "/projects/zed".into(),
highlight_positions: Vec::new(),
kind: WorktreeKind::Main,
@@ -2963,7 +2963,7 @@ impl gpui::Render for ThreadItemBranchNameTestView {
.icon(IconName::ZedAgent)
.timestamp("3d")
.worktrees(vec![ThreadItemWorktreeInfo {
- name: "zed".into(),
+ worktree_name: Some("zed".into()),
full_path: "/projects/zed".into(),
highlight_positions: Vec::new(),
kind: WorktreeKind::Main,
@@ -2978,7 +2978,7 @@ impl gpui::Render for ThreadItemBranchNameTestView {
.icon(IconName::AiClaude)
.timestamp("6d")
.worktrees(vec![ThreadItemWorktreeInfo {
- name: "stoic-reed".into(),
+ worktree_name: Some("stoic-reed".into()),
full_path: "/worktrees/stoic-reed/zed".into(),
highlight_positions: Vec::new(),
kind: WorktreeKind::Linked,
@@ -2995,7 +2995,7 @@ impl gpui::Render for ThreadItemBranchNameTestView {
.icon(IconName::ZedAgent)
.timestamp("40m")
.worktrees(vec![ThreadItemWorktreeInfo {
- name: "focal-arrow".into(),
+ worktree_name: Some("focal-arrow".into()),
full_path: "/worktrees/focal-arrow/zed".into(),
highlight_positions: Vec::new(),
kind: WorktreeKind::Linked,
@@ -3014,7 +3014,7 @@ impl gpui::Render for ThreadItemBranchNameTestView {
.added(42)
.removed(17)
.worktrees(vec![ThreadItemWorktreeInfo {
- name: "jade-glen".into(),
+ worktree_name: Some("jade-glen".into()),
full_path: "/worktrees/jade-glen/zed".into(),
highlight_positions: Vec::new(),
kind: WorktreeKind::Linked,
@@ -3031,7 +3031,7 @@ impl gpui::Render for ThreadItemBranchNameTestView {
.added(108)
.removed(53)
.worktrees(vec![ThreadItemWorktreeInfo {
- name: "my-project".into(),
+ worktree_name: Some("my-project".into()),
full_path: "/worktrees/my-project/zed".into(),
highlight_positions: Vec::new(),
kind: WorktreeKind::Linked,
@@ -3052,7 +3052,7 @@ impl gpui::Render for ThreadItemBranchNameTestView {
.added(23)
.removed(8)
.worktrees(vec![ThreadItemWorktreeInfo {
- name: "zed".into(),
+ worktree_name: Some("zed".into()),
full_path: "/projects/zed".into(),
highlight_positions: Vec::new(),
kind: WorktreeKind::Main,
@@ -159,8 +159,8 @@ pub fn init(cx: &mut App) {
cx.observe_flag::<PanicFeatureFlag, _>({
let mut added = false;
- move |enabled, cx| {
- if added || !enabled {
+ move |flag, cx| {
+ if added || !*flag {
return;
}
added = true;