Detailed changes
@@ -2532,21 +2532,31 @@
"format_dap_log_messages": true,
"button": true,
},
- // Configures any number of settings profiles that are temporarily applied on
- // top of your existing user settings when selected from
- // `settings profile selector: toggle`.
+ // Configures any number of settings profiles that are temporarily applied
+ // when selected from `settings profile selector: toggle`.
+ //
+ // Each profile has an optional `base` ("user" or "default") and a `settings`
+ // object. When `base` is "user" (the default), the profile applies on top of
+ // your user settings. When `base` is "default", user settings are ignored and
+ // the profile applies on top of Zed's defaults.
+ //
// Examples:
// "profiles": {
// "Presenting": {
- // "agent_ui_font_size": 20.0,
- // "buffer_font_size": 20.0,
- // "theme": "One Light",
- // "ui_font_size": 20.0
+ // "base": "default",
+ // "settings": {
+ // "agent_ui_font_size": 20.0,
+ // "buffer_font_size": 20.0,
+ // "theme": "One Light",
+ // "ui_font_size": 20.0
+ // }
// },
// "Python (ty)": {
- // "languages": {
- // "Python": {
- // "language_servers": ["ty"]
+ // "settings": {
+ // "languages": {
+ // "Python": {
+ // "language_servers": ["ty"]
+ // }
// }
// }
// }
@@ -322,3 +322,9 @@ pub(crate) mod m_2026_03_30 {
pub(crate) use settings::make_play_sound_when_agent_done_an_enum;
}
+
+pub(crate) mod m_2026_04_01 {
+ mod settings;
+
+ pub(crate) use settings::restructure_profiles_with_settings_key;
+}
@@ -0,0 +1,29 @@
+use anyhow::Result;
+use serde_json::Value;
+
+pub fn restructure_profiles_with_settings_key(value: &mut Value) -> Result<()> {
+ let Some(root_object) = value.as_object_mut() else {
+ return Ok(());
+ };
+
+ let Some(profiles) = root_object.get_mut("profiles") else {
+ return Ok(());
+ };
+
+ let Some(profiles_map) = profiles.as_object_mut() else {
+ return Ok(());
+ };
+
+ for profile_value in profiles_map.values_mut() {
+ if profile_value
+ .as_object()
+ .is_some_and(|m| m.contains_key("settings") || m.contains_key("base"))
+ {
+ continue;
+ }
+
+ *profile_value = serde_json::json!({ "settings": profile_value });
+ }
+
+ Ok(())
+}
@@ -248,6 +248,7 @@ pub fn migrate_settings(text: &str) -> Result<Option<String>> {
&SETTINGS_QUERY_2026_03_16,
),
MigrationType::Json(migrations::m_2026_03_30::make_play_sound_when_agent_done_an_enum),
+ MigrationType::Json(migrations::m_2026_04_01::restructure_profiles_with_settings_key),
];
run_migrations(text, migrations)
}
@@ -4607,4 +4608,78 @@ mod tests {
),
);
}
+
+ #[test]
+ fn test_restructure_profiles_with_settings_key() {
+ assert_migrate_settings(
+ &r#"
+ {
+ "buffer_font_size": 14,
+ "profiles": {
+ "Presenting": {
+ "buffer_font_size": 20,
+ "theme": "One Light"
+ },
+ "Minimal": {
+ "vim_mode": true
+ }
+ }
+ }
+ "#
+ .unindent(),
+ Some(
+ &r#"
+ {
+ "buffer_font_size": 14,
+ "profiles": {
+ "Presenting": {
+ "settings": {
+ "buffer_font_size": 20,
+ "theme": "One Light"
+ }
+ },
+ "Minimal": {
+ "settings": {
+ "vim_mode": true
+ }
+ }
+ }
+ }
+ "#
+ .unindent(),
+ ),
+ );
+ }
+
+ #[test]
+ fn test_restructure_profiles_with_settings_key_already_migrated() {
+ assert_migrate_settings(
+ &r#"
+ {
+ "profiles": {
+ "Presenting": {
+ "settings": {
+ "buffer_font_size": 20
+ }
+ }
+ }
+ }
+ "#
+ .unindent(),
+ None,
+ );
+ }
+
+ #[test]
+ fn test_restructure_profiles_with_settings_key_no_profiles() {
+ assert_migrate_settings(
+ &r#"
+ {
+ "buffer_font_size": 14
+ }
+ "#
+ .unindent(),
+ None,
+ );
+ }
}
@@ -59,13 +59,13 @@ pub struct ActiveSettingsProfileName(pub String);
impl Global for ActiveSettingsProfileName {}
pub trait UserSettingsContentExt {
- fn for_profile(&self, cx: &App) -> Option<&SettingsContent>;
+ fn for_profile(&self, cx: &App) -> Option<&SettingsProfile>;
fn for_release_channel(&self) -> Option<&SettingsContent>;
fn for_os(&self) -> Option<&SettingsContent>;
}
impl UserSettingsContentExt for UserSettingsContent {
- fn for_profile(&self, cx: &App) -> Option<&SettingsContent> {
+ fn for_profile(&self, cx: &App) -> Option<&SettingsProfile> {
let Some(active_profile) = cx.try_global::<ActiveSettingsProfileName>() else {
return None;
};
@@ -36,8 +36,8 @@ use crate::{
LanguageToSettingsMap, LspSettings, LspSettingsMap, SemanticTokenRules, ThemeName,
UserSettingsContentExt, VsCodeSettings, WorktreeId,
settings_content::{
- ExtensionsSettingsContent, ProjectSettingsContent, RootUserSettings, SettingsContent,
- UserSettingsContent, merge_from::MergeFrom,
+ ExtensionsSettingsContent, ProfileBase, ProjectSettingsContent, RootUserSettings,
+ SettingsContent, UserSettingsContent, merge_from::MergeFrom,
},
};
@@ -1210,10 +1210,19 @@ impl SettingsStore {
merged.merge_from_option(self.extension_settings.as_deref());
merged.merge_from_option(self.global_settings.as_deref());
if let Some(user_settings) = self.user_settings.as_ref() {
- merged.merge_from(&user_settings.content);
- merged.merge_from_option(user_settings.for_release_channel());
- merged.merge_from_option(user_settings.for_os());
- merged.merge_from_option(user_settings.for_profile(cx));
+ let active_profile = user_settings.for_profile(cx);
+ let should_merge_user_settings =
+ active_profile.is_none_or(|profile| profile.base == ProfileBase::User);
+
+ if should_merge_user_settings {
+ merged.merge_from(&user_settings.content);
+ merged.merge_from_option(user_settings.for_release_channel());
+ merged.merge_from_option(user_settings.for_os());
+ }
+
+ if let Some(profile) = active_profile {
+ merged.merge_from(&profile.settings);
+ }
}
merged.merge_from_option(self.server_settings.as_deref());
@@ -265,6 +265,35 @@ settings_overrides! {
pub struct PlatformOverrides { macos, linux, windows }
}
+/// Determines what settings a profile starts from before applying its overrides.
+#[derive(
+ Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom,
+)]
+#[serde(rename_all = "snake_case")]
+pub enum ProfileBase {
+ /// Apply profile settings on top of the user's current settings.
+ #[default]
+ User,
+ /// Apply profile settings on top of Zed's default settings, ignoring user customizations.
+ Default,
+}
+
+/// A named settings profile that can temporarily override settings.
+#[with_fallible_options]
+#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize, JsonSchema, MergeFrom)]
+pub struct SettingsProfile {
+ /// What base settings to start from before applying this profile's overrides.
+ ///
+ /// - `user`: Apply on top of user's settings (default)
+ /// - `default`: Apply on top of Zed's default settings, ignoring user customizations
+ #[serde(default)]
+ pub base: ProfileBase,
+
+ /// The settings overrides for this profile.
+ #[serde(default)]
+ pub settings: Box<SettingsContent>,
+}
+
#[with_fallible_options]
#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize, JsonSchema, MergeFrom)]
pub struct UserSettingsContent {
@@ -278,7 +307,7 @@ pub struct UserSettingsContent {
pub platform_overrides: PlatformOverrides,
#[serde(default)]
- pub profiles: IndexMap<String, SettingsContent>,
+ pub profiles: IndexMap<String, SettingsProfile>,
}
pub struct ExtensionsSettingsContent {
@@ -291,7 +291,7 @@ mod tests {
use zed_actions::settings_profile_selector;
async fn init_test(
- profiles_json: serde_json::Value,
+ user_settings_json: serde_json::Value,
cx: &mut TestAppContext,
) -> (Entity<Workspace>, &mut VisualTestContext) {
cx.update(|cx| {
@@ -307,13 +307,8 @@ mod tests {
cx.update(|cx| {
SettingsStore::update_global(cx, |store, cx| {
- let settings_json = json!({
- "buffer_font_size": 10.0,
- "profiles": profiles_json,
- });
-
store
- .set_user_settings(&settings_json.to_string(), cx)
+ .set_user_settings(&user_settings_json.to_string(), cx)
.unwrap();
});
});
@@ -328,7 +323,6 @@ mod tests {
cx.update(|_, cx| {
assert!(!cx.has_global::<ActiveSettingsProfileName>());
- assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx), px(10.0));
});
(workspace, cx)
@@ -354,15 +348,22 @@ mod tests {
let classroom_and_streaming_profile_name = "Classroom / Streaming".to_string();
let demo_videos_profile_name = "Demo Videos".to_string();
- let profiles_json = json!({
- classroom_and_streaming_profile_name.clone(): {
- "buffer_font_size": 20.0,
- },
- demo_videos_profile_name.clone(): {
- "buffer_font_size": 15.0
+ let user_settings_json = json!({
+ "buffer_font_size": 10.0,
+ "profiles": {
+ classroom_and_streaming_profile_name.clone(): {
+ "settings": {
+ "buffer_font_size": 20.0,
+ }
+ },
+ demo_videos_profile_name.clone(): {
+ "settings": {
+ "buffer_font_size": 15.0
+ }
+ }
}
});
- let (workspace, cx) = init_test(profiles_json.clone(), cx).await;
+ let (workspace, cx) = init_test(user_settings_json, cx).await;
cx.dispatch_action(settings_profile_selector::Toggle);
let picker = active_settings_profile_picker(&workspace, cx);
@@ -575,24 +576,134 @@ mod tests {
});
}
+ #[gpui::test]
+ async fn test_settings_profile_with_user_base(cx: &mut TestAppContext) {
+ let user_settings_json = json!({
+ "buffer_font_size": 10.0,
+ "profiles": {
+ "Explicit User": {
+ "base": "user",
+ "settings": {
+ "buffer_font_size": 20.0
+ }
+ },
+ "Implicit User": {
+ "settings": {
+ "buffer_font_size": 20.0
+ }
+ }
+ }
+ });
+ let (workspace, cx) = init_test(user_settings_json, cx).await;
+
+ // Select "Explicit User" (index 1) β profile applies on top of user settings.
+ cx.dispatch_action(settings_profile_selector::Toggle);
+ let picker = active_settings_profile_picker(&workspace, cx);
+ cx.dispatch_action(SelectNext);
+
+ picker.read_with(cx, |picker, cx| {
+ assert_eq!(
+ picker.delegate.selected_profile_name.as_deref(),
+ Some("Explicit User")
+ );
+ assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx), px(20.0));
+ });
+
+ cx.dispatch_action(Confirm);
+
+ // Select "Implicit User" (index 2) β no base specified, same behavior.
+ cx.dispatch_action(settings_profile_selector::Toggle);
+ let picker = active_settings_profile_picker(&workspace, cx);
+ cx.dispatch_action(SelectNext);
+
+ picker.read_with(cx, |picker, cx| {
+ assert_eq!(
+ picker.delegate.selected_profile_name.as_deref(),
+ Some("Implicit User")
+ );
+ assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx), px(20.0));
+ });
+
+ cx.dispatch_action(Confirm);
+ }
+
+ #[gpui::test]
+ async fn test_settings_profile_with_default_base(cx: &mut TestAppContext) {
+ let user_settings_json = json!({
+ "buffer_font_size": 10.0,
+ "profiles": {
+ "Clean Slate": {
+ "base": "default"
+ },
+ "Custom on Defaults": {
+ "base": "default",
+ "settings": {
+ "buffer_font_size": 30.0
+ }
+ }
+ }
+ });
+ let (workspace, cx) = init_test(user_settings_json, cx).await;
+
+ // User has buffer_font_size: 10, factory default is 15.
+ cx.update(|_, cx| {
+ assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx), px(10.0));
+ });
+
+ // "Clean Slate" has base: "default" with no settings overrides,
+ // so we get the factory default (15), not the user's value (10).
+ cx.dispatch_action(settings_profile_selector::Toggle);
+ let picker = active_settings_profile_picker(&workspace, cx);
+ cx.dispatch_action(SelectNext);
+
+ picker.read_with(cx, |picker, cx| {
+ assert_eq!(
+ picker.delegate.selected_profile_name.as_deref(),
+ Some("Clean Slate")
+ );
+ assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx), px(15.0));
+ });
+
+ // "Custom on Defaults" has base: "default" with buffer_font_size: 30,
+ // so the profile's override (30) applies on top of the factory default,
+ // not on top of the user's value (10).
+ cx.dispatch_action(SelectNext);
+
+ picker.read_with(cx, |picker, cx| {
+ assert_eq!(
+ picker.delegate.selected_profile_name.as_deref(),
+ Some("Custom on Defaults")
+ );
+ assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx), px(30.0));
+ });
+
+ cx.dispatch_action(Confirm);
+
+ cx.update(|_, cx| {
+ assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx), px(30.0));
+ });
+ }
+
#[gpui::test]
async fn test_settings_profile_selector_is_in_user_configuration_order(
cx: &mut TestAppContext,
) {
// Must be unique names (HashMap)
- let profiles_json = json!({
- "z": {},
- "e": {},
- "d": {},
- " ": {},
- "r": {},
- "u": {},
- "l": {},
- "3": {},
- "s": {},
- "!": {},
+ let user_settings_json = json!({
+ "profiles": {
+ "z": { "settings": {} },
+ "e": { "settings": {} },
+ "d": { "settings": {} },
+ " ": { "settings": {} },
+ "r": { "settings": {} },
+ "u": { "settings": {} },
+ "l": { "settings": {} },
+ "3": { "settings": {} },
+ "s": { "settings": {} },
+ "!": { "settings": {} },
+ }
});
- let (workspace, cx) = init_test(profiles_json.clone(), cx).await;
+ let (workspace, cx) = init_test(user_settings_json, cx).await;
cx.dispatch_action(settings_profile_selector::Toggle);
let picker = active_settings_profile_picker(&workspace, cx);
@@ -3002,21 +3002,36 @@ If you wish to exclude certain hosts from using the proxy, set the `NO_PROXY` en
## Profiles
-- Description: Configuration profiles that can be applied on top of existing settings
+- Description: Configuration profiles that can be temporarily applied on top of existing settings or Zed's defaults.
- Setting: `profiles`
- Default: `{}`
**Options**
-Configuration object for defining settings profiles. Example:
+Each profile is an object with the following optional fields:
+
+- `base`: What settings to start from before applying the profile's overrides.
+ - `"user"` (default): Apply on top of your current user settings.
+ - `"default"`: Apply on top of Zed's default settings, ignoring user customizations.
+- `settings`: The settings overrides for this profile.
+
+Examples:
```json [settings]
{
"profiles": {
- "presentation": {
- "buffer_font_size": 20,
- "ui_font_size": 18,
- "theme": "One Light"
+ "Presentation": {
+ "settings": {
+ "buffer_font_size": 20,
+ "ui_font_size": 18,
+ "theme": "One Light"
+ }
+ },
+ "Clean Slate": {
+ "base": "default",
+ "settings": {
+ "theme": "Ayu Dark"
+ }
}
}
}
@@ -5332,12 +5347,12 @@ For example, to use `Nerd Font` as a fallback, add the following to your setting
## Settings Profiles
-- Description: Configure any number of settings profiles that are temporarily applied on top of your existing user settings when selected from `settings profile selector: toggle`.
+- Description: Configure any number of settings profiles that are temporarily applied when selected from `settings profile selector: toggle`.
- Setting: `profiles`
- Default: `{}`
In your `settings.json` file, add the `profiles` object.
-Each key within this object is the name of a settings profile, and each value is an object that can include any of Zed's settings.
+Each key within this object is the name of a settings profile. Each profile has an optional `base` field (`"user"` or `"default"`) and a `settings` object containing any of Zed's settings.
Example:
@@ -5345,24 +5360,30 @@ Example:
{
"profiles": {
"Presenting (Dark)": {
- "agent_buffer_font_size": 18.0,
- "buffer_font_size": 18.0,
- "theme": "One Dark",
- "ui_font_size": 18.0
+ "settings": {
+ "agent_buffer_font_size": 18.0,
+ "buffer_font_size": 18.0,
+ "theme": "One Dark",
+ "ui_font_size": 18.0
+ }
},
"Presenting (Light)": {
- "agent_buffer_font_size": 18.0,
- "buffer_font_size": 18.0,
- "theme": "One Light",
- "ui_font_size": 18.0
+ "settings": {
+ "agent_buffer_font_size": 18.0,
+ "buffer_font_size": 18.0,
+ "theme": "One Light",
+ "ui_font_size": 18.0
+ }
},
"Writing": {
- "agent_buffer_font_size": 15.0,
- "buffer_font_size": 15.0,
- "theme": "Catppuccin FrappΓ© - No Italics",
- "ui_font_size": 15.0,
- "tab_bar": { "show": false },
- "toolbar": { "breadcrumbs": false }
+ "settings": {
+ "agent_buffer_font_size": 15.0,
+ "buffer_font_size": 15.0,
+ "theme": "Catppuccin FrappΓ© - No Italics",
+ "ui_font_size": 15.0,
+ "tab_bar": { "show": false },
+ "toolbar": { "breadcrumbs": false }
+ }
}
}
}