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

@ -39,6 +39,7 @@ globset.workspace = true
gpui.workspace = true
http_client.workspace = true
imara-diff.workspace = true
inventory.workspace = true
itertools.workspace = true
log.workspace = true
lsp.workspace = true

View file

@ -2006,7 +2006,7 @@ fn test_autoindent_language_without_indents_query(cx: &mut App) {
#[gpui::test]
fn test_autoindent_with_injected_languages(cx: &mut App) {
init_settings(cx, |settings| {
settings.languages.extend([
settings.languages.0.extend([
(
"HTML".into(),
LanguageSettingsContent {

View file

@ -39,11 +39,7 @@ use lsp::{CodeActionKind, InitializeParams, LanguageServerBinary, LanguageServer
pub use manifest::{ManifestDelegate, ManifestName, ManifestProvider, ManifestQuery};
use parking_lot::Mutex;
use regex::Regex;
use schemars::{
JsonSchema,
r#gen::SchemaGenerator,
schema::{InstanceType, Schema, SchemaObject},
};
use schemars::{JsonSchema, json_schema};
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
use serde_json::Value;
use settings::WorktreeId;
@ -694,7 +690,6 @@ pub struct LanguageConfig {
pub matcher: LanguageMatcher,
/// List of bracket types in a language.
#[serde(default)]
#[schemars(schema_with = "bracket_pair_config_json_schema")]
pub brackets: BracketPairConfig,
/// If set to true, auto indentation uses last non empty line to determine
/// the indentation level for a new line.
@ -944,10 +939,9 @@ fn deserialize_regex<'de, D: Deserializer<'de>>(d: D) -> Result<Option<Regex>, D
}
}
fn regex_json_schema(_: &mut SchemaGenerator) -> Schema {
Schema::Object(SchemaObject {
instance_type: Some(InstanceType::String.into()),
..Default::default()
fn regex_json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
json_schema!({
"type": "string"
})
}
@ -988,12 +982,12 @@ pub struct FakeLspAdapter {
/// This struct includes settings for defining which pairs of characters are considered brackets and
/// also specifies any language-specific scopes where these pairs should be ignored for bracket matching purposes.
#[derive(Clone, Debug, Default, JsonSchema)]
#[schemars(with = "Vec::<BracketPairContent>")]
pub struct BracketPairConfig {
/// A list of character pairs that should be treated as brackets in the context of a given language.
pub pairs: Vec<BracketPair>,
/// A list of tree-sitter scopes for which a given bracket should not be active.
/// N-th entry in `[Self::disabled_scopes_by_bracket_ix]` contains a list of disabled scopes for an n-th entry in `[Self::pairs]`
#[serde(skip)]
pub disabled_scopes_by_bracket_ix: Vec<Vec<String>>,
}
@ -1003,10 +997,6 @@ impl BracketPairConfig {
}
}
fn bracket_pair_config_json_schema(r#gen: &mut SchemaGenerator) -> Schema {
Option::<Vec<BracketPairContent>>::json_schema(r#gen)
}
#[derive(Deserialize, JsonSchema)]
pub struct BracketPairContent {
#[serde(flatten)]

View file

@ -1170,7 +1170,7 @@ impl LanguageRegistryState {
if let Some(theme) = self.theme.as_ref() {
language.set_theme(theme.syntax());
}
self.language_settings.languages.insert(
self.language_settings.languages.0.insert(
language.name(),
LanguageSettingsContent {
tab_size: language.config.tab_size,

View file

@ -3,7 +3,6 @@
use crate::{File, Language, LanguageName, LanguageServerName};
use anyhow::Result;
use collections::{FxHashMap, HashMap, HashSet};
use core::slice;
use ec4rs::{
Properties as EditorconfigProperties,
property::{FinalNewline, IndentSize, IndentStyle, TabWidth, TrimTrailingWs},
@ -11,17 +10,15 @@ use ec4rs::{
use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder};
use gpui::{App, Modifiers};
use itertools::{Either, Itertools};
use schemars::{
JsonSchema,
schema::{InstanceType, ObjectValidation, Schema, SchemaObject, SingleOrVec},
};
use schemars::{JsonSchema, json_schema};
use serde::{
Deserialize, Deserializer, Serialize,
de::{self, IntoDeserializer, MapAccess, SeqAccess, Visitor},
};
use serde_json::Value;
use settings::{
Settings, SettingsLocation, SettingsSources, SettingsStore, add_references_to_properties,
ParameterizedJsonSchema, Settings, SettingsLocation, SettingsSources, SettingsStore,
replace_subschema,
};
use shellexpand;
use std::{borrow::Cow, num::NonZeroU32, path::Path, sync::Arc};
@ -306,13 +303,42 @@ pub struct AllLanguageSettingsContent {
pub defaults: LanguageSettingsContent,
/// The settings for individual languages.
#[serde(default)]
pub languages: HashMap<LanguageName, LanguageSettingsContent>,
pub languages: LanguageToSettingsMap,
/// Settings for associating file extensions and filenames
/// with languages.
#[serde(default)]
pub file_types: HashMap<Arc<str>, Vec<String>>,
}
/// Map from language name to settings. Its `ParameterizedJsonSchema` allows only known language
/// names in the keys.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct LanguageToSettingsMap(pub HashMap<LanguageName, LanguageSettingsContent>);
inventory::submit! {
ParameterizedJsonSchema {
add_and_get_ref: |generator, params, _cx| {
let language_settings_content_ref = generator
.subschema_for::<LanguageSettingsContent>()
.to_value();
let schema = json_schema!({
"type": "object",
"properties": params
.language_names
.iter()
.map(|name| {
(
name.clone(),
language_settings_content_ref.clone(),
)
})
.collect::<serde_json::Map<_, _>>()
});
replace_subschema::<LanguageToSettingsMap>(generator, schema)
}
}
}
/// Controls how completions are processed for this language.
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
@ -648,45 +674,30 @@ pub enum FormatOnSave {
On,
/// Files should not be formatted on save.
Off,
List(FormatterList),
List(Vec<Formatter>),
}
impl JsonSchema for FormatOnSave {
fn schema_name() -> String {
fn schema_name() -> Cow<'static, str> {
"OnSaveFormatter".into()
}
fn json_schema(generator: &mut schemars::r#gen::SchemaGenerator) -> Schema {
let mut schema = SchemaObject::default();
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
let formatter_schema = Formatter::json_schema(generator);
schema.instance_type = Some(
vec![
InstanceType::Object,
InstanceType::String,
InstanceType::Array,
json_schema!({
"oneOf": [
{
"type": "array",
"items": formatter_schema
},
{
"type": "string",
"enum": ["on", "off", "prettier", "language_server"]
},
formatter_schema
]
.into(),
);
let valid_raw_values = SchemaObject {
enum_values: Some(vec![
Value::String("on".into()),
Value::String("off".into()),
Value::String("prettier".into()),
Value::String("language_server".into()),
]),
..Default::default()
};
let mut nested_values = SchemaObject::default();
nested_values.array().items = Some(formatter_schema.clone().into());
schema.subschemas().any_of = Some(vec![
nested_values.into(),
valid_raw_values.into(),
formatter_schema,
]);
schema.into()
})
}
}
@ -725,11 +736,11 @@ impl<'de> Deserialize<'de> for FormatOnSave {
} else if v == "off" {
Ok(Self::Value::Off)
} else if v == "language_server" {
Ok(Self::Value::List(FormatterList(
Formatter::LanguageServer { name: None }.into(),
)))
Ok(Self::Value::List(vec![Formatter::LanguageServer {
name: None,
}]))
} else {
let ret: Result<FormatterList, _> =
let ret: Result<Vec<Formatter>, _> =
Deserialize::deserialize(v.into_deserializer());
ret.map(Self::Value::List)
}
@ -738,7 +749,7 @@ impl<'de> Deserialize<'de> for FormatOnSave {
where
A: MapAccess<'d>,
{
let ret: Result<FormatterList, _> =
let ret: Result<Vec<Formatter>, _> =
Deserialize::deserialize(de::value::MapAccessDeserializer::new(map));
ret.map(Self::Value::List)
}
@ -746,7 +757,7 @@ impl<'de> Deserialize<'de> for FormatOnSave {
where
A: SeqAccess<'d>,
{
let ret: Result<FormatterList, _> =
let ret: Result<Vec<Formatter>, _> =
Deserialize::deserialize(de::value::SeqAccessDeserializer::new(map));
ret.map(Self::Value::List)
}
@ -783,45 +794,30 @@ pub enum SelectedFormatter {
/// or falling back to formatting via language server.
#[default]
Auto,
List(FormatterList),
List(Vec<Formatter>),
}
impl JsonSchema for SelectedFormatter {
fn schema_name() -> String {
fn schema_name() -> Cow<'static, str> {
"Formatter".into()
}
fn json_schema(generator: &mut schemars::r#gen::SchemaGenerator) -> Schema {
let mut schema = SchemaObject::default();
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
let formatter_schema = Formatter::json_schema(generator);
schema.instance_type = Some(
vec![
InstanceType::Object,
InstanceType::String,
InstanceType::Array,
json_schema!({
"oneOf": [
{
"type": "array",
"items": formatter_schema
},
{
"type": "string",
"enum": ["auto", "prettier", "language_server"]
},
formatter_schema
]
.into(),
);
let valid_raw_values = SchemaObject {
enum_values: Some(vec![
Value::String("auto".into()),
Value::String("prettier".into()),
Value::String("language_server".into()),
]),
..Default::default()
};
let mut nested_values = SchemaObject::default();
nested_values.array().items = Some(formatter_schema.clone().into());
schema.subschemas().any_of = Some(vec![
nested_values.into(),
valid_raw_values.into(),
formatter_schema,
]);
schema.into()
})
}
}
@ -836,6 +832,7 @@ impl Serialize for SelectedFormatter {
}
}
}
impl<'de> Deserialize<'de> for SelectedFormatter {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
@ -856,11 +853,11 @@ impl<'de> Deserialize<'de> for SelectedFormatter {
if v == "auto" {
Ok(Self::Value::Auto)
} else if v == "language_server" {
Ok(Self::Value::List(FormatterList(
Formatter::LanguageServer { name: None }.into(),
)))
Ok(Self::Value::List(vec![Formatter::LanguageServer {
name: None,
}]))
} else {
let ret: Result<FormatterList, _> =
let ret: Result<Vec<Formatter>, _> =
Deserialize::deserialize(v.into_deserializer());
ret.map(SelectedFormatter::List)
}
@ -869,7 +866,7 @@ impl<'de> Deserialize<'de> for SelectedFormatter {
where
A: MapAccess<'d>,
{
let ret: Result<FormatterList, _> =
let ret: Result<Vec<Formatter>, _> =
Deserialize::deserialize(de::value::MapAccessDeserializer::new(map));
ret.map(SelectedFormatter::List)
}
@ -877,7 +874,7 @@ impl<'de> Deserialize<'de> for SelectedFormatter {
where
A: SeqAccess<'d>,
{
let ret: Result<FormatterList, _> =
let ret: Result<Vec<Formatter>, _> =
Deserialize::deserialize(de::value::SeqAccessDeserializer::new(map));
ret.map(SelectedFormatter::List)
}
@ -885,19 +882,6 @@ impl<'de> Deserialize<'de> for SelectedFormatter {
deserializer.deserialize_any(FormatDeserializer)
}
}
/// Controls which formatter should be used when formatting code.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case", transparent)]
pub struct FormatterList(pub SingleOrVec<Formatter>);
impl AsRef<[Formatter]> for FormatterList {
fn as_ref(&self) -> &[Formatter] {
match &self.0 {
SingleOrVec::Single(single) => slice::from_ref(single),
SingleOrVec::Vec(v) => v,
}
}
}
/// Controls which formatter should be used when formatting code. If there are multiple formatters, they are executed in the order of declaration.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
@ -1209,7 +1193,7 @@ impl settings::Settings for AllLanguageSettings {
serde_json::from_value(serde_json::to_value(&default_value.defaults)?)?;
let mut languages = HashMap::default();
for (language_name, settings) in &default_value.languages {
for (language_name, settings) in &default_value.languages.0 {
let mut language_settings = defaults.clone();
merge_settings(&mut language_settings, settings);
languages.insert(language_name.clone(), language_settings);
@ -1310,7 +1294,7 @@ impl settings::Settings for AllLanguageSettings {
}
// A user's language-specific settings override default language-specific settings.
for (language_name, user_language_settings) in &user_settings.languages {
for (language_name, user_language_settings) in &user_settings.languages.0 {
merge_settings(
languages
.entry(language_name.clone())
@ -1366,51 +1350,6 @@ impl settings::Settings for AllLanguageSettings {
})
}
fn json_schema(
generator: &mut schemars::r#gen::SchemaGenerator,
params: &settings::SettingsJsonSchemaParams,
_: &App,
) -> schemars::schema::RootSchema {
let mut root_schema = generator.root_schema_for::<Self::FileContent>();
// Create a schema for a 'languages overrides' object, associating editor
// settings with specific languages.
assert!(
root_schema
.definitions
.contains_key("LanguageSettingsContent")
);
let languages_object_schema = SchemaObject {
instance_type: Some(InstanceType::Object.into()),
object: Some(Box::new(ObjectValidation {
properties: params
.language_names
.iter()
.map(|name| {
(
name.clone(),
Schema::new_ref("#/definitions/LanguageSettingsContent".into()),
)
})
.collect(),
..Default::default()
})),
..Default::default()
};
root_schema
.definitions
.extend([("Languages".into(), languages_object_schema.into())]);
add_references_to_properties(
&mut root_schema,
&[("languages", "#/definitions/Languages")],
);
root_schema
}
fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
let d = &mut current.defaults;
if let Some(size) = vscode
@ -1674,29 +1613,26 @@ mod tests {
let settings: LanguageSettingsContent = serde_json::from_str(raw).unwrap();
assert_eq!(
settings.formatter,
Some(SelectedFormatter::List(FormatterList(
Formatter::LanguageServer { name: None }.into()
)))
Some(SelectedFormatter::List(vec![Formatter::LanguageServer {
name: None
}]))
);
let raw = "{\"formatter\": [{\"language_server\": {\"name\": null}}]}";
let settings: LanguageSettingsContent = serde_json::from_str(raw).unwrap();
assert_eq!(
settings.formatter,
Some(SelectedFormatter::List(FormatterList(
vec![Formatter::LanguageServer { name: None }].into()
)))
Some(SelectedFormatter::List(vec![Formatter::LanguageServer {
name: None
}]))
);
let raw = "{\"formatter\": [{\"language_server\": {\"name\": null}}, \"prettier\"]}";
let settings: LanguageSettingsContent = serde_json::from_str(raw).unwrap();
assert_eq!(
settings.formatter,
Some(SelectedFormatter::List(FormatterList(
vec![
Formatter::LanguageServer { name: None },
Formatter::Prettier
]
.into()
)))
Some(SelectedFormatter::List(vec![
Formatter::LanguageServer { name: None },
Formatter::Prettier
]))
);
}