Fix settings migrations for nested platform/channel/profile keys (#48550)

Richard Feldman created

Previously, some settings migrations only operated on root-level keys
and missed settings nested under platform keys (`macos`, `linux`, etc.),
channel keys (`nightly`, `stable`, etc.), or profile blocks. This fixes
migrations to recurse into those nested locations.

Also fixes `m_2026_02_02` to gracefully skip when `edit_predictions` is
not an object (e.g. `true`) instead of bailing and aborting the entire
migration chain.

Release Notes:

- Fixed settings migrations to correctly handle settings nested under
platform, channel, or profile keys.

Change summary

Cargo.lock                                              |   1 
crates/migrator/Cargo.toml                              |   1 
crates/migrator/src/migrations.rs                       | 109 ++
crates/migrator/src/migrations/m_2025_10_01/settings.rs |   2 
crates/migrator/src/migrations/m_2025_10_02/settings.rs |   2 
crates/migrator/src/migrations/m_2025_10_16/settings.rs |   2 
crates/migrator/src/migrations/m_2025_10_17/settings.rs |   8 
crates/migrator/src/migrations/m_2025_10_21/settings.rs |   8 
crates/migrator/src/migrations/m_2025_11_25/settings.rs |  20 
crates/migrator/src/migrations/m_2026_02_02/settings.rs |   8 
crates/migrator/src/migrations/m_2026_02_03/settings.rs |  13 
crates/migrator/src/migrator.rs                         | 469 +++++++++++
crates/migrator/src/patterns.rs                         |   1 
crates/migrator/src/patterns/settings.rs                |  21 
crates/settings/src/settings.rs                         |  17 
crates/settings_content/src/settings_content.rs         |  54 +
16 files changed, 673 insertions(+), 63 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -10137,6 +10137,7 @@ dependencies = [
  "pretty_assertions",
  "serde_json",
  "serde_json_lenient",
+ "settings_content",
  "settings_json",
  "streaming-iterator",
  "tree-sitter",

crates/migrator/Cargo.toml 🔗

@@ -22,6 +22,7 @@ tree-sitter-json.workspace = true
 tree-sitter.workspace = true
 serde_json_lenient.workspace = true
 serde_json.workspace = true
+settings_content.workspace = true
 settings_json.workspace = true
 
 [dev-dependencies]

crates/migrator/src/migrations.rs 🔗

@@ -1,3 +1,112 @@
+use anyhow::Result;
+use serde_json::Value;
+use settings_content::{PlatformOverrides, ReleaseChannelOverrides};
+
+/// Applies a migration callback to the root settings object as well as all
+/// nested platform, release-channel, and profile override objects.
+pub(crate) fn migrate_settings(
+    value: &mut Value,
+    mut migrate_one: impl FnMut(&mut serde_json::Map<String, Value>) -> Result<()>,
+) -> Result<()> {
+    let Some(root_object) = value.as_object_mut() else {
+        return Ok(());
+    };
+
+    migrate_one(root_object)?;
+
+    let override_keys = ReleaseChannelOverrides::OVERRIDE_KEYS
+        .iter()
+        .copied()
+        .chain(PlatformOverrides::OVERRIDE_KEYS.iter().copied());
+
+    for key in override_keys {
+        if let Some(sub_object) = root_object.get_mut(key) {
+            if let Some(sub_map) = sub_object.as_object_mut() {
+                migrate_one(sub_map)?;
+            }
+        }
+    }
+
+    if let Some(profiles) = root_object.get_mut("profiles") {
+        if let Some(profiles_object) = profiles.as_object_mut() {
+            for (_profile_name, profile_settings) in profiles_object.iter_mut() {
+                if let Some(profile_map) = profile_settings.as_object_mut() {
+                    migrate_one(profile_map)?;
+                }
+            }
+        }
+    }
+
+    Ok(())
+}
+
+/// Applies a migration callback to a value and its `languages` children,
+/// at the root level as well as all nested platform, release-channel, and
+/// profile override objects.
+pub(crate) fn migrate_language_setting(
+    value: &mut Value,
+    migrate_fn: fn(&mut Value, path: &[&str]) -> Result<()>,
+) -> Result<()> {
+    fn apply_to_value_and_languages(
+        value: &mut Value,
+        prefix: &[&str],
+        migrate_fn: fn(&mut Value, path: &[&str]) -> Result<()>,
+    ) -> Result<()> {
+        migrate_fn(value, prefix)?;
+        let languages = value
+            .as_object_mut()
+            .and_then(|obj| obj.get_mut("languages"))
+            .and_then(|languages| languages.as_object_mut());
+        if let Some(languages) = languages {
+            for (language_name, language) in languages.iter_mut() {
+                let mut path: Vec<&str> = prefix.to_vec();
+                path.push("languages");
+                path.push(language_name);
+                migrate_fn(language, &path)?;
+            }
+        }
+        Ok(())
+    }
+
+    if !value.is_object() {
+        return Ok(());
+    }
+
+    apply_to_value_and_languages(value, &[], migrate_fn)?;
+
+    let Some(root_object) = value.as_object_mut() else {
+        return Ok(());
+    };
+
+    let override_keys = ReleaseChannelOverrides::OVERRIDE_KEYS
+        .iter()
+        .copied()
+        .chain(PlatformOverrides::OVERRIDE_KEYS.iter().copied());
+
+    for key in override_keys {
+        if let Some(sub_value) = root_object.get_mut(key) {
+            apply_to_value_and_languages(sub_value, &[key], migrate_fn)?;
+        }
+    }
+
+    if let Some(profiles) = root_object.get_mut("profiles") {
+        if let Some(profiles_object) = profiles.as_object_mut() {
+            let profile_names: Vec<String> = profiles_object.keys().cloned().collect();
+            for profile_name in &profile_names {
+                if let Some(profile_settings) = profiles_object.get_mut(profile_name.as_str()) {
+                    apply_to_value_and_languages(
+                        profile_settings,
+                        &["profiles", profile_name],
+                        migrate_fn,
+                    )?;
+                }
+            }
+        }
+    }
+
+    Ok(())
+}
+
 pub(crate) mod m_2025_01_02 {
     mod settings;
 

crates/migrator/src/migrations/m_2025_10_02/settings.rs 🔗

@@ -1,7 +1,7 @@
 use anyhow::Result;
 use serde_json::Value;
 
-use crate::patterns::migrate_language_setting;
+use crate::migrations::migrate_language_setting;
 
 pub fn remove_formatters_on_save(value: &mut Value) -> Result<()> {
     migrate_language_setting(value, remove_formatters_on_save_inner)

crates/migrator/src/migrations/m_2025_10_16/settings.rs 🔗

@@ -1,7 +1,7 @@
 use anyhow::Result;
 use serde_json::Value;
 
-use crate::patterns::migrate_language_setting;
+use crate::migrations::migrate_language_setting;
 
 pub fn restore_code_actions_on_format(value: &mut Value) -> Result<()> {
     migrate_language_setting(value, restore_code_actions_on_format_inner)

crates/migrator/src/migrations/m_2025_10_17/settings.rs 🔗

@@ -1,8 +1,14 @@
 use anyhow::Result;
 use serde_json::Value;
 
+use crate::migrations::migrate_settings;
+
 pub fn make_file_finder_include_ignored_an_enum(value: &mut Value) -> Result<()> {
-    let Some(file_finder) = value.get_mut("file_finder") else {
+    migrate_settings(value, migrate_one)
+}
+
+fn migrate_one(obj: &mut serde_json::Map<String, Value>) -> Result<()> {
+    let Some(file_finder) = obj.get_mut("file_finder") else {
         return Ok(());
     };
 

crates/migrator/src/migrations/m_2025_10_21/settings.rs 🔗

@@ -1,8 +1,14 @@
 use anyhow::Result;
 use serde_json::Value;
 
+use crate::migrations::migrate_settings;
+
 pub fn make_relative_line_numbers_an_enum(value: &mut Value) -> Result<()> {
-    let Some(relative_line_numbers) = value.get_mut("relative_line_numbers") else {
+    migrate_settings(value, migrate_one)
+}
+
+fn migrate_one(obj: &mut serde_json::Map<String, Value>) -> Result<()> {
+    let Some(relative_line_numbers) = obj.get_mut("relative_line_numbers") else {
         return Ok(());
     };
 

crates/migrator/src/migrations/m_2025_11_25/settings.rs 🔗

@@ -1,14 +1,18 @@
 use anyhow::Result;
 use serde_json::Value;
 
-pub fn remove_context_server_source(settings: &mut Value) -> Result<()> {
-    if let Some(obj) = settings.as_object_mut() {
-        if let Some(context_servers) = obj.get_mut("context_servers") {
-            if let Some(servers) = context_servers.as_object_mut() {
-                for (_, server) in servers.iter_mut() {
-                    if let Some(server_obj) = server.as_object_mut() {
-                        server_obj.remove("source");
-                    }
+use crate::migrations::migrate_settings;
+
+pub fn remove_context_server_source(value: &mut Value) -> Result<()> {
+    migrate_settings(value, migrate_one)
+}
+
+fn migrate_one(obj: &mut serde_json::Map<String, Value>) -> Result<()> {
+    if let Some(context_servers) = obj.get_mut("context_servers") {
+        if let Some(servers) = context_servers.as_object_mut() {
+            for (_, server) in servers.iter_mut() {
+                if let Some(server_obj) = server.as_object_mut() {
+                    server_obj.remove("source");
                 }
             }
         }

crates/migrator/src/migrations/m_2026_02_02/settings.rs 🔗

@@ -1,11 +1,13 @@
 use anyhow::Result;
 use serde_json::Value;
 
+use crate::migrations::migrate_settings;
+
 pub fn move_edit_prediction_provider_to_edit_predictions(value: &mut Value) -> Result<()> {
-    let Some(obj) = value.as_object_mut() else {
-        return Ok(());
-    };
+    migrate_settings(value, migrate_one)
+}
 
+fn migrate_one(obj: &mut serde_json::Map<String, Value>) -> Result<()> {
     let Some(features) = obj.get_mut("features") else {
         return Ok(());
     };

crates/migrator/src/migrations/m_2026_02_03/settings.rs 🔗

@@ -1,11 +1,16 @@
 use anyhow::Result;
 use serde_json::Value;
 
+use crate::migrations::migrate_settings;
+
 pub fn migrate_experimental_sweep_mercury(value: &mut Value) -> Result<()> {
-    let Some(obj) = value.as_object_mut() else {
-        return Ok(());
-    };
+    migrate_settings(value, |obj| {
+        migrate_one(obj);
+        Ok(())
+    })
+}
 
+fn migrate_one(obj: &mut serde_json::Map<String, Value>) {
     if let Some(edit_predictions) = obj.get_mut("edit_predictions") {
         if let Some(edit_predictions_obj) = edit_predictions.as_object_mut() {
             migrate_provider_field(edit_predictions_obj, "provider");
@@ -17,8 +22,6 @@ pub fn migrate_experimental_sweep_mercury(value: &mut Value) -> Result<()> {
             migrate_provider_field(features_obj, "edit_prediction_provider");
         }
     }
-
-    Ok(())
 }
 
 fn migrate_provider_field(obj: &mut serde_json::Map<String, Value>, field_name: &str) {

crates/migrator/src/migrator.rs 🔗

@@ -2219,6 +2219,165 @@ mod tests {
                 .unindent(),
             ),
         );
+
+        // Platform key: settings nested inside "linux" should be migrated
+        assert_migrate_settings_with_migrations(
+            &[MigrationType::Json(
+                migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum,
+            )],
+            &r#"
+            {
+                "linux": {
+                    "file_finder": {
+                        "include_ignored": true
+                    }
+                }
+            }
+            "#
+            .unindent(),
+            Some(
+                &r#"
+                {
+                    "linux": {
+                        "file_finder": {
+                            "include_ignored": "all"
+                        }
+                    }
+                }
+                "#
+                .unindent(),
+            ),
+        );
+
+        // Profile: settings nested inside profiles should be migrated
+        assert_migrate_settings_with_migrations(
+            &[MigrationType::Json(
+                migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum,
+            )],
+            &r#"
+            {
+                "profiles": {
+                    "work": {
+                        "file_finder": {
+                            "include_ignored": false
+                        }
+                    }
+                }
+            }
+            "#
+            .unindent(),
+            Some(
+                &r#"
+                {
+                    "profiles": {
+                        "work": {
+                            "file_finder": {
+                                "include_ignored": "indexed"
+                            }
+                        }
+                    }
+                }
+                "#
+                .unindent(),
+            ),
+        );
+    }
+
+    #[test]
+    fn test_make_relative_line_numbers_an_enum() {
+        assert_migrate_settings_with_migrations(
+            &[MigrationType::Json(
+                migrations::m_2025_10_21::make_relative_line_numbers_an_enum,
+            )],
+            &r#"{ }"#.unindent(),
+            None,
+        );
+
+        assert_migrate_settings_with_migrations(
+            &[MigrationType::Json(
+                migrations::m_2025_10_21::make_relative_line_numbers_an_enum,
+            )],
+            &r#"{
+                "relative_line_numbers": true
+            }"#
+            .unindent(),
+            Some(
+                &r#"{
+                    "relative_line_numbers": "enabled"
+                }"#
+                .unindent(),
+            ),
+        );
+
+        assert_migrate_settings_with_migrations(
+            &[MigrationType::Json(
+                migrations::m_2025_10_21::make_relative_line_numbers_an_enum,
+            )],
+            &r#"{
+                "relative_line_numbers": false
+            }"#
+            .unindent(),
+            Some(
+                &r#"{
+                    "relative_line_numbers": "disabled"
+                }"#
+                .unindent(),
+            ),
+        );
+
+        // Platform key: settings nested inside "macos" should be migrated
+        assert_migrate_settings_with_migrations(
+            &[MigrationType::Json(
+                migrations::m_2025_10_21::make_relative_line_numbers_an_enum,
+            )],
+            &r#"
+            {
+                "macos": {
+                    "relative_line_numbers": true
+                }
+            }
+            "#
+            .unindent(),
+            Some(
+                &r#"
+                {
+                    "macos": {
+                        "relative_line_numbers": "enabled"
+                    }
+                }
+                "#
+                .unindent(),
+            ),
+        );
+
+        // Profile: settings nested inside profiles should be migrated
+        assert_migrate_settings_with_migrations(
+            &[MigrationType::Json(
+                migrations::m_2025_10_21::make_relative_line_numbers_an_enum,
+            )],
+            &r#"
+            {
+                "profiles": {
+                    "dev": {
+                        "relative_line_numbers": false
+                    }
+                }
+            }
+            "#
+            .unindent(),
+            Some(
+                &r#"
+                {
+                    "profiles": {
+                        "dev": {
+                            "relative_line_numbers": "disabled"
+                        }
+                    }
+                }
+                "#
+                .unindent(),
+            ),
+        );
     }
 
     #[test]
@@ -2267,6 +2426,84 @@ mod tests {
                 .unindent(),
             ),
         );
+
+        // Platform key: settings nested inside "linux" should be migrated
+        assert_migrate_settings_with_migrations(
+            &[MigrationType::Json(
+                migrations::m_2025_11_25::remove_context_server_source,
+            )],
+            &r#"
+            {
+                "linux": {
+                    "context_servers": {
+                        "my_server": {
+                            "source": "extension",
+                            "settings": {
+                                "key": "value"
+                            }
+                        }
+                    }
+                }
+            }
+            "#
+            .unindent(),
+            Some(
+                &r#"
+                {
+                    "linux": {
+                        "context_servers": {
+                            "my_server": {
+                                "settings": {
+                                    "key": "value"
+                                }
+                            }
+                        }
+                    }
+                }
+                "#
+                .unindent(),
+            ),
+        );
+
+        // Profile: settings nested inside profiles should be migrated
+        assert_migrate_settings_with_migrations(
+            &[MigrationType::Json(
+                migrations::m_2025_11_25::remove_context_server_source,
+            )],
+            &r#"
+            {
+                "profiles": {
+                    "work": {
+                        "context_servers": {
+                            "my_server": {
+                                "source": "custom",
+                                "command": "foo",
+                                "args": ["bar"]
+                            }
+                        }
+                    }
+                }
+            }
+            "#
+            .unindent(),
+            Some(
+                &r#"
+                {
+                    "profiles": {
+                        "work": {
+                            "context_servers": {
+                                "my_server": {
+                                    "command": "foo",
+                                    "args": ["bar"]
+                                }
+                            }
+                        }
+                    }
+                }
+                "#
+                .unindent(),
+            ),
+        );
     }
 
     #[test]
@@ -2470,6 +2707,117 @@ mod tests {
             .unindent(),
             None,
         );
+
+        // Platform key: settings nested inside "macos" should be migrated
+        assert_migrate_settings_with_migrations(
+            &[MigrationType::Json(
+                migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
+            )],
+            &r#"
+            {
+                "macos": {
+                    "features": {
+                        "edit_prediction_provider": "copilot"
+                    }
+                }
+            }
+            "#
+            .unindent(),
+            Some(
+                &r#"
+                {
+                    "macos": {
+                        "edit_predictions": {
+                            "provider": "copilot"
+                        }
+                    }
+                }
+                "#
+                .unindent(),
+            ),
+        );
+
+        // Profile: settings nested inside profiles should be migrated
+        assert_migrate_settings_with_migrations(
+            &[MigrationType::Json(
+                migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
+            )],
+            &r#"
+            {
+                "profiles": {
+                    "work": {
+                        "features": {
+                            "edit_prediction_provider": "copilot"
+                        }
+                    }
+                }
+            }
+            "#
+            .unindent(),
+            Some(
+                &r#"
+                {
+                    "profiles": {
+                        "work": {
+                            "edit_predictions": {
+                                "provider": "copilot"
+                            }
+                        }
+                    }
+                }
+                "#
+                .unindent(),
+            ),
+        );
+
+        // Combined: root + platform + profile should all be migrated simultaneously
+        assert_migrate_settings_with_migrations(
+            &[MigrationType::Json(
+                migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
+            )],
+            &r#"
+            {
+                "features": {
+                    "edit_prediction_provider": "copilot"
+                },
+                "macos": {
+                    "features": {
+                        "edit_prediction_provider": "zed"
+                    }
+                },
+                "profiles": {
+                    "work": {
+                        "features": {
+                            "edit_prediction_provider": "supermaven"
+                        }
+                    }
+                }
+            }
+            "#
+            .unindent(),
+            Some(
+                &r#"
+                {
+                    "edit_predictions": {
+                        "provider": "copilot"
+                    },
+                    "macos": {
+                        "edit_predictions": {
+                            "provider": "zed"
+                        }
+                    },
+                    "profiles": {
+                        "work": {
+                            "edit_predictions": {
+                                "provider": "supermaven"
+                            }
+                        }
+                    }
+                }
+                "#
+                .unindent(),
+            ),
+        );
     }
 
     #[test]
@@ -2591,5 +2939,126 @@ mod tests {
             .unindent(),
             None,
         );
+
+        // Platform key: settings nested inside "linux" should be migrated
+        assert_migrate_settings_with_migrations(
+            &[MigrationType::Json(
+                migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
+            )],
+            &r#"
+            {
+                "linux": {
+                    "edit_predictions": {
+                        "provider": {
+                            "experimental": "sweep"
+                        }
+                    }
+                }
+            }
+            "#
+            .unindent(),
+            Some(
+                &r#"
+                {
+                    "linux": {
+                        "edit_predictions": {
+                            "provider": "sweep"
+                        }
+                    }
+                }
+                "#
+                .unindent(),
+            ),
+        );
+
+        // Profile: settings nested inside profiles should be migrated
+        assert_migrate_settings_with_migrations(
+            &[MigrationType::Json(
+                migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
+            )],
+            &r#"
+            {
+                "profiles": {
+                    "dev": {
+                        "edit_predictions": {
+                            "provider": {
+                                "experimental": "mercury"
+                            }
+                        }
+                    }
+                }
+            }
+            "#
+            .unindent(),
+            Some(
+                &r#"
+                {
+                    "profiles": {
+                        "dev": {
+                            "edit_predictions": {
+                                "provider": "mercury"
+                            }
+                        }
+                    }
+                }
+                "#
+                .unindent(),
+            ),
+        );
+
+        // Combined: root + platform + profile should all be migrated simultaneously
+        assert_migrate_settings_with_migrations(
+            &[MigrationType::Json(
+                migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
+            )],
+            &r#"
+            {
+                "edit_predictions": {
+                    "provider": {
+                        "experimental": "sweep"
+                    }
+                },
+                "linux": {
+                    "edit_predictions": {
+                        "provider": {
+                            "experimental": "mercury"
+                        }
+                    }
+                },
+                "profiles": {
+                    "dev": {
+                        "edit_predictions": {
+                            "provider": {
+                                "experimental": "sweep"
+                            }
+                        }
+                    }
+                }
+            }
+            "#
+            .unindent(),
+            Some(
+                &r#"
+                {
+                    "edit_predictions": {
+                        "provider": "sweep"
+                    },
+                    "linux": {
+                        "edit_predictions": {
+                            "provider": "mercury"
+                        }
+                    },
+                    "profiles": {
+                        "dev": {
+                            "edit_predictions": {
+                                "provider": "sweep"
+                            }
+                        }
+                    }
+                }
+                "#
+                .unindent(),
+            ),
+        );
     }
 }

crates/migrator/src/patterns.rs 🔗

@@ -10,5 +10,4 @@ pub(crate) use settings::{
     SETTINGS_ASSISTANT_PATTERN, SETTINGS_ASSISTANT_TOOLS_PATTERN,
     SETTINGS_DUPLICATED_AGENT_PATTERN, SETTINGS_EDIT_PREDICTIONS_ASSISTANT_PATTERN,
     SETTINGS_LANGUAGES_PATTERN, SETTINGS_NESTED_KEY_VALUE_PATTERN, SETTINGS_ROOT_KEY_VALUE_PATTERN,
-    migrate_language_setting,
 };

crates/migrator/src/patterns/settings.rs 🔗

@@ -108,24 +108,3 @@ pub const SETTINGS_DUPLICATED_AGENT_PATTERN: &str = r#"(document
     (#eq? @agent1 "agent")
     (#eq? @agent2 "agent")
 )"#;
-
-/// Migrate language settings,
-/// calls `migrate_fn` with the top level object as well as all language settings under the "languages" key
-/// Fails early if `migrate_fn` returns an error at any point
-pub fn migrate_language_setting(
-    value: &mut serde_json::Value,
-    migrate_fn: fn(&mut serde_json::Value, path: &[&str]) -> anyhow::Result<()>,
-) -> anyhow::Result<()> {
-    migrate_fn(value, &[])?;
-    let languages = value
-        .as_object_mut()
-        .and_then(|obj| obj.get_mut("languages"))
-        .and_then(|languages| languages.as_object_mut());
-    if let Some(languages) = languages {
-        for (language_name, language) in languages.iter_mut() {
-            let path = vec!["languages", language_name];
-            migrate_fn(language, &path)?;
-        }
-    }
-    Ok(())
-}

crates/settings/src/settings.rs 🔗

@@ -24,7 +24,7 @@ pub mod private {
 }
 
 use gpui::{App, Global};
-use release_channel::ReleaseChannel;
+
 use rust_embed::RustEmbed;
 use std::env;
 use std::{borrow::Cow, fmt, str};
@@ -73,21 +73,12 @@ impl UserSettingsContentExt for UserSettingsContent {
     }
 
     fn for_release_channel(&self) -> Option<&SettingsContent> {
-        match *release_channel::RELEASE_CHANNEL {
-            ReleaseChannel::Dev => self.dev.as_deref(),
-            ReleaseChannel::Nightly => self.nightly.as_deref(),
-            ReleaseChannel::Preview => self.preview.as_deref(),
-            ReleaseChannel::Stable => self.stable.as_deref(),
-        }
+        self.release_channel_overrides
+            .get_by_key(release_channel::RELEASE_CHANNEL.dev_name())
     }
 
     fn for_os(&self) -> Option<&SettingsContent> {
-        match env::consts::OS {
-            "macos" => self.macos.as_deref(),
-            "linux" => self.linux.as_deref(),
-            "windows" => self.windows.as_deref(),
-            _ => None,
-        }
+        self.platform_overrides.get_by_key(env::consts::OS)
     }
 }
 

crates/settings_content/src/settings_content.rs 🔗

@@ -32,6 +32,37 @@ use collections::{HashMap, IndexMap};
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use settings_macros::{MergeFrom, with_fallible_options};
+
+/// Defines a settings override struct where each field is
+/// `Option<Box<SettingsContent>>`, along with:
+/// - `OVERRIDE_KEYS`: a `&[&str]` of the field names (the JSON keys)
+/// - `get_by_key(&self, key) -> Option<&SettingsContent>`: accessor by key
+///
+/// The field list is the single source of truth for the override key strings.
+macro_rules! settings_overrides {
+    (
+        $(#[$attr:meta])*
+        pub struct $name:ident { $($field:ident),* $(,)? }
+    ) => {
+        $(#[$attr])*
+        pub struct $name {
+            $(pub $field: Option<Box<SettingsContent>>,)*
+        }
+
+        impl $name {
+            /// The JSON override keys, derived from the field names on this struct.
+            pub const OVERRIDE_KEYS: &[&str] = &[$(stringify!($field)),*];
+
+            /// Look up an override by its JSON key name.
+            pub fn get_by_key(&self, key: &str) -> Option<&SettingsContent> {
+                match key {
+                    $(stringify!($field) => self.$field.as_deref(),)*
+                    _ => None,
+                }
+            }
+        }
+    }
+}
 use std::collections::BTreeSet;
 use std::sync::Arc;
 pub use util::serde::default_true;
@@ -217,20 +248,29 @@ impl RootUserSettings for UserSettingsContent {
     }
 }
 
+settings_overrides! {
+    #[with_fallible_options]
+    #[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize, JsonSchema, MergeFrom)]
+    pub struct ReleaseChannelOverrides { dev, nightly, preview, stable }
+}
+
+settings_overrides! {
+    #[with_fallible_options]
+    #[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize, JsonSchema, MergeFrom)]
+    pub struct PlatformOverrides { macos, linux, windows }
+}
+
 #[with_fallible_options]
 #[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct UserSettingsContent {
     #[serde(flatten)]
     pub content: Box<SettingsContent>,
 
-    pub dev: Option<Box<SettingsContent>>,
-    pub nightly: Option<Box<SettingsContent>>,
-    pub preview: Option<Box<SettingsContent>>,
-    pub stable: Option<Box<SettingsContent>>,
+    #[serde(flatten)]
+    pub release_channel_overrides: ReleaseChannelOverrides,
 
-    pub macos: Option<Box<SettingsContent>>,
-    pub windows: Option<Box<SettingsContent>>,
-    pub linux: Option<Box<SettingsContent>>,
+    #[serde(flatten)]
+    pub platform_overrides: PlatformOverrides,
 
     #[serde(default)]
     pub profiles: IndexMap<String, SettingsContent>,