Migrate to schemars version 1.0 (#33635)

The major change in schemars 1.0 is that now schemas are represented as
plain json values instead of specialized datatypes. This allows for more
concise construction and manipulation.

This change also improves how settings schemas are generated. Each top
level settings type was being generated as a full root schema including
the definitions it references, and then these were merged. This meant
generating all shared definitions multiple times, and might have bugs in
cases where there are two types with the same names.

Now instead the schemar generator's `definitions` are built up as they
normally are and the `Settings` trait no longer has a special
`json_schema` method. To handle types that have schema that vary at
runtime (`FontFamilyName`, `ThemeName`, etc), values of
`ParameterizedJsonSchema` are collected by `inventory`, and the schema
definitions for these types are replaced.

To help check that this doesn't break anything, I tried to minimize the
overall [schema
diff](https://gist.github.com/mgsloan/1de549def20399d6f37943a3c1583ee7)
with some patches to make the order more consistent + schemas also
sorted with `jq -S .`. A skim of the diff shows that the diffs come
from:

* `enum: ["value"]` turning into `const: "value"`
* Differences in handling of newlines for "description"
* Schemas for generic types no longer including the parameter name, now
all disambiguation is with numeric suffixes
* Enums now using `oneOf` instead of `anyOf`.

Release Notes:

- N/A
This commit is contained in:
Michael Sloan 2025-06-30 15:07:28 -06:00 committed by GitHub
parent a2e786e0f9
commit 5fafab6e52
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
42 changed files with 714 additions and 963 deletions

View file

@ -24,6 +24,7 @@ fs.workspace = true
futures.workspace = true
gpui.workspace = true
indexmap.workspace = true
inventory.workspace = true
log.workspace = true
palette = { workspace = true, default-features = false, features = ["std"] }
parking_lot.workspace = true

View file

@ -4,12 +4,11 @@ use anyhow::Result;
use gpui::{FontStyle, FontWeight, HighlightStyle, Hsla, WindowBackgroundAppearance};
use indexmap::IndexMap;
use palette::FromColor;
use schemars::JsonSchema;
use schemars::r#gen::SchemaGenerator;
use schemars::schema::{Schema, SchemaObject};
use schemars::{JsonSchema, json_schema};
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Value;
use serde_repr::{Deserialize_repr, Serialize_repr};
use std::borrow::Cow;
use crate::{StatusColorsRefinement, ThemeColorsRefinement};
@ -1502,30 +1501,15 @@ pub enum FontWeightContent {
}
impl JsonSchema for FontWeightContent {
fn schema_name() -> String {
"FontWeightContent".to_owned()
fn schema_name() -> Cow<'static, str> {
"FontWeightContent".into()
}
fn is_referenceable() -> bool {
false
}
fn json_schema(_: &mut SchemaGenerator) -> Schema {
SchemaObject {
enum_values: Some(vec![
100.into(),
200.into(),
300.into(),
400.into(),
500.into(),
600.into(),
700.into(),
800.into(),
900.into(),
]),
..Default::default()
}
.into()
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
json_schema!({
"type": "integer",
"enum": [100, 200, 300, 400, 500, 600, 700, 800, 900]
})
}
}

View file

@ -7,17 +7,12 @@ use anyhow::Result;
use derive_more::{Deref, DerefMut};
use gpui::{
App, Context, Font, FontFallbacks, FontFeatures, FontStyle, FontWeight, Global, Pixels,
Subscription, Window, px,
SharedString, Subscription, Window, px,
};
use refineable::Refineable;
use schemars::{
JsonSchema,
r#gen::SchemaGenerator,
schema::{InstanceType, Schema, SchemaObject},
};
use schemars::{JsonSchema, json_schema};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use settings::{Settings, SettingsJsonSchemaParams, SettingsSources, add_references_to_properties};
use settings::{ParameterizedJsonSchema, Settings, SettingsSources, replace_subschema};
use std::sync::Arc;
use util::ResultExt as _;
@ -263,25 +258,19 @@ impl Global for AgentFontSize {}
#[serde(untagged)]
pub enum ThemeSelection {
/// A static theme selection, represented by a single theme name.
Static(#[schemars(schema_with = "theme_name_ref")] String),
Static(ThemeName),
/// A dynamic theme selection, which can change based the [ThemeMode].
Dynamic {
/// The mode used to determine which theme to use.
#[serde(default)]
mode: ThemeMode,
/// The theme to use for light mode.
#[schemars(schema_with = "theme_name_ref")]
light: String,
light: ThemeName,
/// The theme to use for dark mode.
#[schemars(schema_with = "theme_name_ref")]
dark: String,
dark: ThemeName,
},
}
fn theme_name_ref(_: &mut SchemaGenerator) -> Schema {
Schema::new_ref("#/definitions/ThemeName".into())
}
// TODO: Rename ThemeMode -> ThemeAppearanceMode
/// The mode use to select a theme.
///
@ -306,13 +295,13 @@ impl ThemeSelection {
/// Returns the theme name for the selected [ThemeMode].
pub fn theme(&self, system_appearance: Appearance) -> &str {
match self {
Self::Static(theme) => theme,
Self::Static(theme) => &theme.0,
Self::Dynamic { mode, light, dark } => match mode {
ThemeMode::Light => light,
ThemeMode::Dark => dark,
ThemeMode::Light => &light.0,
ThemeMode::Dark => &dark.0,
ThemeMode::System => match system_appearance {
Appearance::Light => light,
Appearance::Dark => dark,
Appearance::Light => &light.0,
Appearance::Dark => &dark.0,
},
},
}
@ -327,27 +316,21 @@ impl ThemeSelection {
}
}
fn icon_theme_name_ref(_: &mut SchemaGenerator) -> Schema {
Schema::new_ref("#/definitions/IconThemeName".into())
}
/// Represents the selection of an icon theme, which can be either static or dynamic.
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(untagged)]
pub enum IconThemeSelection {
/// A static icon theme selection, represented by a single icon theme name.
Static(#[schemars(schema_with = "icon_theme_name_ref")] String),
Static(IconThemeName),
/// A dynamic icon theme selection, which can change based on the [`ThemeMode`].
Dynamic {
/// The mode used to determine which theme to use.
#[serde(default)]
mode: ThemeMode,
/// The icon theme to use for light mode.
#[schemars(schema_with = "icon_theme_name_ref")]
light: String,
light: IconThemeName,
/// The icon theme to use for dark mode.
#[schemars(schema_with = "icon_theme_name_ref")]
dark: String,
dark: IconThemeName,
},
}
@ -355,13 +338,13 @@ impl IconThemeSelection {
/// Returns the icon theme name based on the given [`Appearance`].
pub fn icon_theme(&self, system_appearance: Appearance) -> &str {
match self {
Self::Static(theme) => theme,
Self::Static(theme) => &theme.0,
Self::Dynamic { mode, light, dark } => match mode {
ThemeMode::Light => light,
ThemeMode::Dark => dark,
ThemeMode::Light => &light.0,
ThemeMode::Dark => &dark.0,
ThemeMode::System => match system_appearance {
Appearance::Light => light,
Appearance::Dark => dark,
Appearance::Light => &light.0,
Appearance::Dark => &dark.0,
},
},
}
@ -384,11 +367,12 @@ pub struct ThemeSettingsContent {
pub ui_font_size: Option<f32>,
/// The name of a font to use for rendering in the UI.
#[serde(default)]
pub ui_font_family: Option<String>,
pub ui_font_family: Option<FontFamilyName>,
/// The font fallbacks to use for rendering in the UI.
#[serde(default)]
#[schemars(default = "default_font_fallbacks")]
pub ui_font_fallbacks: Option<Vec<String>>,
#[schemars(extend("uniqueItems" = true))]
pub ui_font_fallbacks: Option<Vec<FontFamilyName>>,
/// The OpenType features to enable for text in the UI.
#[serde(default)]
#[schemars(default = "default_font_features")]
@ -398,11 +382,11 @@ pub struct ThemeSettingsContent {
pub ui_font_weight: Option<f32>,
/// The name of a font to use for rendering in text buffers.
#[serde(default)]
pub buffer_font_family: Option<String>,
pub buffer_font_family: Option<FontFamilyName>,
/// The font fallbacks to use for rendering in text buffers.
#[serde(default)]
#[schemars(default = "default_font_fallbacks")]
pub buffer_font_fallbacks: Option<Vec<String>>,
#[schemars(extend("uniqueItems" = true))]
pub buffer_font_fallbacks: Option<Vec<FontFamilyName>>,
/// The default font size for rendering in text buffers.
#[serde(default)]
pub buffer_font_size: Option<f32>,
@ -467,9 +451,9 @@ impl ThemeSettingsContent {
},
};
*theme_to_update = theme_name.to_string();
*theme_to_update = ThemeName(theme_name.into());
} else {
self.theme = Some(ThemeSelection::Static(theme_name.to_string()));
self.theme = Some(ThemeSelection::Static(ThemeName(theme_name.into())));
}
}
@ -488,9 +472,11 @@ impl ThemeSettingsContent {
},
};
*icon_theme_to_update = icon_theme_name.to_string();
*icon_theme_to_update = IconThemeName(icon_theme_name.into());
} else {
self.icon_theme = Some(IconThemeSelection::Static(icon_theme_name.to_string()));
self.icon_theme = Some(IconThemeSelection::Static(IconThemeName(
icon_theme_name.into(),
)));
}
}
@ -516,8 +502,8 @@ impl ThemeSettingsContent {
} else {
self.theme = Some(ThemeSelection::Dynamic {
mode,
light: ThemeSettings::DEFAULT_LIGHT_THEME.into(),
dark: ThemeSettings::DEFAULT_DARK_THEME.into(),
light: ThemeName(ThemeSettings::DEFAULT_LIGHT_THEME.into()),
dark: ThemeName(ThemeSettings::DEFAULT_DARK_THEME.into()),
});
}
@ -539,7 +525,9 @@ impl ThemeSettingsContent {
} => *mode_to_update = mode,
}
} else {
self.icon_theme = Some(IconThemeSelection::Static(DEFAULT_ICON_THEME_NAME.into()));
self.icon_theme = Some(IconThemeSelection::Static(IconThemeName(
DEFAULT_ICON_THEME_NAME.into(),
)));
}
}
}
@ -815,26 +803,39 @@ impl settings::Settings for ThemeSettings {
let themes = ThemeRegistry::default_global(cx);
let system_appearance = SystemAppearance::default_global(cx);
fn font_fallbacks_from_settings(
fallbacks: Option<Vec<FontFamilyName>>,
) -> Option<FontFallbacks> {
fallbacks.map(|fallbacks| {
FontFallbacks::from_fonts(
fallbacks
.into_iter()
.map(|font_family| font_family.0.to_string())
.collect(),
)
})
}
let defaults = sources.default;
let mut this = Self {
ui_font_size: defaults.ui_font_size.unwrap().into(),
ui_font: Font {
family: defaults.ui_font_family.as_ref().unwrap().clone().into(),
family: defaults.ui_font_family.as_ref().unwrap().0.clone().into(),
features: defaults.ui_font_features.clone().unwrap(),
fallbacks: defaults
.ui_font_fallbacks
.as_ref()
.map(|fallbacks| FontFallbacks::from_fonts(fallbacks.clone())),
fallbacks: font_fallbacks_from_settings(defaults.ui_font_fallbacks.clone()),
weight: defaults.ui_font_weight.map(FontWeight).unwrap(),
style: Default::default(),
},
buffer_font: Font {
family: defaults.buffer_font_family.as_ref().unwrap().clone().into(),
features: defaults.buffer_font_features.clone().unwrap(),
fallbacks: defaults
.buffer_font_fallbacks
family: defaults
.buffer_font_family
.as_ref()
.map(|fallbacks| FontFallbacks::from_fonts(fallbacks.clone())),
.unwrap()
.0
.clone()
.into(),
features: defaults.buffer_font_features.clone().unwrap(),
fallbacks: font_fallbacks_from_settings(defaults.buffer_font_fallbacks.clone()),
weight: defaults.buffer_font_weight.map(FontWeight).unwrap(),
style: FontStyle::default(),
},
@ -872,26 +873,26 @@ impl settings::Settings for ThemeSettings {
}
if let Some(value) = value.buffer_font_family.clone() {
this.buffer_font.family = value.into();
this.buffer_font.family = value.0.into();
}
if let Some(value) = value.buffer_font_features.clone() {
this.buffer_font.features = value;
}
if let Some(value) = value.buffer_font_fallbacks.clone() {
this.buffer_font.fallbacks = Some(FontFallbacks::from_fonts(value));
this.buffer_font.fallbacks = font_fallbacks_from_settings(Some(value));
}
if let Some(value) = value.buffer_font_weight {
this.buffer_font.weight = clamp_font_weight(value);
}
if let Some(value) = value.ui_font_family.clone() {
this.ui_font.family = value.into();
this.ui_font.family = value.0.into();
}
if let Some(value) = value.ui_font_features.clone() {
this.ui_font.features = value;
}
if let Some(value) = value.ui_font_fallbacks.clone() {
this.ui_font.fallbacks = Some(FontFallbacks::from_fonts(value));
this.ui_font.fallbacks = font_fallbacks_from_settings(Some(value));
}
if let Some(value) = value.ui_font_weight {
this.ui_font.weight = clamp_font_weight(value);
@ -959,64 +960,73 @@ impl settings::Settings for ThemeSettings {
Ok(this)
}
fn json_schema(
generator: &mut SchemaGenerator,
params: &SettingsJsonSchemaParams,
cx: &App,
) -> schemars::schema::RootSchema {
let mut root_schema = generator.root_schema_for::<ThemeSettingsContent>();
let theme_names = ThemeRegistry::global(cx)
.list_names()
.into_iter()
.map(|theme_name| Value::String(theme_name.to_string()))
.collect();
let theme_name_schema = SchemaObject {
instance_type: Some(InstanceType::String.into()),
enum_values: Some(theme_names),
..Default::default()
};
let icon_theme_names = ThemeRegistry::global(cx)
.list_icon_themes()
.into_iter()
.map(|icon_theme| Value::String(icon_theme.name.to_string()))
.collect();
let icon_theme_name_schema = SchemaObject {
instance_type: Some(InstanceType::String.into()),
enum_values: Some(icon_theme_names),
..Default::default()
};
root_schema.definitions.extend([
("ThemeName".into(), theme_name_schema.into()),
("IconThemeName".into(), icon_theme_name_schema.into()),
("FontFamilies".into(), params.font_family_schema()),
("FontFallbacks".into(), params.font_fallback_schema()),
]);
add_references_to_properties(
&mut root_schema,
&[
("buffer_font_family", "#/definitions/FontFamilies"),
("buffer_font_fallbacks", "#/definitions/FontFallbacks"),
("ui_font_family", "#/definitions/FontFamilies"),
("ui_font_fallbacks", "#/definitions/FontFallbacks"),
],
);
root_schema
}
fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
vscode.f32_setting("editor.fontWeight", &mut current.buffer_font_weight);
vscode.f32_setting("editor.fontSize", &mut current.buffer_font_size);
vscode.string_setting("editor.font", &mut current.buffer_font_family);
if let Some(font) = vscode.read_string("editor.font") {
current.buffer_font_family = Some(FontFamilyName(font.into()));
}
// TODO: possibly map editor.fontLigatures to buffer_font_features?
}
}
/// Newtype for a theme name. Its `ParameterizedJsonSchema` lists the theme names known at runtime.
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(transparent)]
pub struct ThemeName(pub Arc<str>);
inventory::submit! {
ParameterizedJsonSchema {
add_and_get_ref: |generator, _params, cx| {
let schema = json_schema!({
"type": "string",
"enum": ThemeRegistry::global(cx).list_names(),
});
replace_subschema::<ThemeName>(generator, schema)
}
}
}
/// Newtype for a icon theme name. Its `ParameterizedJsonSchema` lists the icon theme names known at
/// runtime.
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(transparent)]
pub struct IconThemeName(pub Arc<str>);
inventory::submit! {
ParameterizedJsonSchema {
add_and_get_ref: |generator, _params, cx| {
let schema = json_schema!({
"type": "string",
"enum": ThemeRegistry::global(cx)
.list_icon_themes()
.into_iter()
.map(|icon_theme| icon_theme.name)
.collect::<Vec<SharedString>>(),
});
replace_subschema::<IconThemeName>(generator, schema)
}
}
}
/// Newtype for font family name. Its `ParameterizedJsonSchema` lists the font families known at
/// runtime.
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(transparent)]
pub struct FontFamilyName(pub Arc<str>);
inventory::submit! {
ParameterizedJsonSchema {
add_and_get_ref: |generator, params, _cx| {
let schema = json_schema!({
"type": "string",
"enum": params.font_names,
});
replace_subschema::<FontFamilyName>(generator, schema)
}
}
}
fn merge<T: Copy>(target: &mut T, value: Option<T>) {
if let Some(value) = value {
*target = value;