settings ui: Create settings key trait (#37489)

This PR separates out the associated constant `KEY` from the `Settings`
trait into a new trait `SettingsKey`. This allows for the key trait to
be derived using attributes to specify the path so that the new
`SettingsUi` derive macro can use the same attributes to determine top
level settings paths thereby removing the need to duplicate the path in
both `Settings::KEY` and `#[settings_ui(path = "...")]`

Co-authored-by: Ben Kunkle <ben@zed.dev>

Release Notes:

- N/A

---------

Co-authored-by: Ben Kunkle <ben@zed.dev>
This commit is contained in:
Anthony Eid
2025-09-04 15:19:02 -04:00
committed by GitHub
co-authored by Ben Kunkle
parent 223fda2fe2
commit 5f03202b5c
44 changed files with 473 additions and 255 deletions
+14 -5
View File
@@ -1,10 +1,10 @@
use std::fmt::{Display, Formatter};
use crate as settings;
use crate::{self as settings};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources, VsCodeSettings};
use settings_ui_macros::SettingsUi;
use settings_ui_macros::{SettingsKey, SettingsUi};
/// Base key bindings scheme. Base keymaps can be overridden with user keymaps.
///
@@ -101,16 +101,25 @@ impl BaseKeymap {
}
#[derive(
Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default, SettingsUi,
Copy,
Clone,
Debug,
Serialize,
Deserialize,
JsonSchema,
PartialEq,
Eq,
Default,
SettingsUi,
SettingsKey,
)]
// extracted so that it can be an option, and still work with derive(SettingsUi)
#[settings_key(None)]
pub struct BaseKeymapSetting {
pub base_keymap: Option<BaseKeymap>,
}
impl Settings for BaseKeymap {
const KEY: Option<&'static str> = None;
type FileContent = BaseKeymapSetting;
fn load(
+3 -3
View File
@@ -21,12 +21,12 @@ pub use keymap_file::{
pub use settings_file::*;
pub use settings_json::*;
pub use settings_store::{
InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation, SettingsSources,
SettingsStore,
InvalidSettingsError, LocalSettingsKind, Settings, SettingsKey, SettingsLocation,
SettingsSources, SettingsStore,
};
pub use settings_ui_core::*;
// Re-export the derive macro
pub use settings_ui_macros::SettingsUi;
pub use settings_ui_macros::{SettingsKey, SettingsUi};
pub use vscode_import::{VsCodeSettings, VsCodeSettingsSource};
#[derive(Clone, Debug, PartialEq)]
+70 -25
View File
@@ -36,17 +36,19 @@ use crate::{
settings_ui_core::SettingsUi, update_value_in_json_text,
};
/// A value that can be defined as a user setting.
///
/// Settings can be loaded from a combination of multiple JSON files.
pub trait Settings: 'static + Send + Sync {
pub trait SettingsKey: 'static + Send + Sync {
/// The name of a key within the JSON file from which this setting should
/// be deserialized. If this is `None`, then the setting will be deserialized
/// from the root object.
const KEY: Option<&'static str>;
const FALLBACK_KEY: Option<&'static str> = None;
}
/// A value that can be defined as a user setting.
///
/// Settings can be loaded from a combination of multiple JSON files.
pub trait Settings: 'static + Send + Sync {
/// The name of the keys in the [`FileContent`](Self::FileContent) that should
/// always be written to a settings file, even if their value matches the default
/// value.
@@ -57,8 +59,19 @@ pub trait Settings: 'static + Send + Sync {
const PRESERVED_KEYS: Option<&'static [&'static str]> = None;
/// The type that is stored in an individual JSON file.
type FileContent: Clone + Default + Serialize + DeserializeOwned + JsonSchema + SettingsUi;
type FileContent: Clone
+ Default
+ Serialize
+ DeserializeOwned
+ JsonSchema
+ SettingsUi
+ SettingsKey;
/*
* let path = Settings
*
*
*/
/// The logic for combining together values from one or more JSON files into the
/// final value for this setting.
///
@@ -71,7 +84,7 @@ pub trait Settings: 'static + Send + Sync {
Self: Sized;
fn missing_default() -> anyhow::Error {
anyhow::anyhow!("missing default")
anyhow::anyhow!("missing default for: {}", std::any::type_name::<Self>())
}
/// Use [the helpers in the vscode_import module](crate::vscode_import) to apply known
@@ -1393,7 +1406,7 @@ impl Debug for SettingsStore {
impl<T: Settings> AnySettingValue for SettingValue<T> {
fn key(&self) -> Option<&'static str> {
T::KEY
T::FileContent::KEY
}
fn setting_type_name(&self) -> &'static str {
@@ -1445,16 +1458,21 @@ impl<T: Settings> AnySettingValue for SettingValue<T> {
mut json: &Value,
) -> (Option<&'static str>, Result<DeserializedSetting>) {
let mut key = None;
if let Some(k) = T::KEY {
if let Some(k) = T::FileContent::KEY {
if let Some(value) = json.get(k) {
json = value;
key = Some(k);
} else if let Some((k, value)) = T::FALLBACK_KEY.and_then(|k| Some((k, json.get(k)?))) {
} else if let Some((k, value)) =
T::FileContent::FALLBACK_KEY.and_then(|k| Some((k, json.get(k)?)))
{
json = value;
key = Some(k);
} else {
let value = T::FileContent::default();
return (T::KEY, Ok(DeserializedSetting(Box::new(value))));
return (
T::FileContent::KEY,
Ok(DeserializedSetting(Box::new(value))),
);
}
}
let value = serde_path_to_error::deserialize::<_, T::FileContent>(json)
@@ -1498,6 +1516,7 @@ impl<T: Settings> AnySettingValue for SettingValue<T> {
}
}
}
self.global_value
.as_ref()
.unwrap_or_else(|| panic!("no default value for setting {}", self.setting_type_name()))
@@ -1570,7 +1589,7 @@ mod tests {
// This is so the SettingsUi macro can still work properly
use crate as settings;
use serde_derive::Deserialize;
use settings_ui_macros::SettingsUi;
use settings_ui_macros::{SettingsKey, SettingsUi};
use unindent::Unindent;
#[gpui::test]
@@ -2120,7 +2139,8 @@ mod tests {
staff: bool,
}
#[derive(Default, Clone, Serialize, Deserialize, JsonSchema, SettingsUi)]
#[derive(Default, Clone, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)]
#[settings_key(key = "user")]
struct UserSettingsContent {
name: Option<String>,
age: Option<u32>,
@@ -2128,7 +2148,6 @@ mod tests {
}
impl Settings for UserSettings {
const KEY: Option<&'static str> = Some("user");
type FileContent = UserSettingsContent;
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
@@ -2143,12 +2162,37 @@ mod tests {
#[derive(Debug, Deserialize, PartialEq)]
struct TurboSetting(bool);
#[derive(
Copy,
Clone,
PartialEq,
Eq,
Debug,
Default,
serde::Serialize,
serde::Deserialize,
SettingsUi,
SettingsKey,
JsonSchema,
)]
#[serde(default)]
#[settings_key(None)]
pub struct TurboSettingContent {
turbo: Option<bool>,
}
impl Settings for TurboSetting {
const KEY: Option<&'static str> = Some("turbo");
type FileContent = bool;
type FileContent = TurboSettingContent;
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
sources.json_merge()
Ok(Self(
sources
.user
.or(sources.server)
.unwrap_or(sources.default)
.turbo
.unwrap_or_default(),
))
}
fn import_from_vscode(_vscode: &VsCodeSettings, _current: &mut Self::FileContent) {}
@@ -2162,15 +2206,14 @@ mod tests {
key2: String,
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)]
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)]
#[settings_key(None)]
struct MultiKeySettingsJson {
key1: Option<String>,
key2: Option<String>,
}
impl Settings for MultiKeySettings {
const KEY: Option<&'static str> = None;
type FileContent = MultiKeySettingsJson;
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
@@ -2200,15 +2243,16 @@ mod tests {
Hour24,
}
#[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema, SettingsUi)]
#[derive(
Clone, Default, Debug, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey,
)]
#[settings_key(key = "journal")]
struct JournalSettingsJson {
pub path: Option<String>,
pub hour_format: Option<HourFormat>,
}
impl Settings for JournalSettings {
const KEY: Option<&'static str> = Some("journal");
type FileContent = JournalSettingsJson;
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
@@ -2288,7 +2332,10 @@ mod tests {
);
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, SettingsUi)]
#[derive(
Clone, Debug, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey,
)]
#[settings_key(None)]
struct LanguageSettings {
#[serde(default)]
languages: HashMap<String, LanguageSettingEntry>,
@@ -2301,8 +2348,6 @@ mod tests {
}
impl Settings for LanguageSettings {
const KEY: Option<&'static str> = None;
type FileContent = Self;
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {