Generalize settings JSON schema logic to work w/ arbitrary setting types
This commit is contained in:
parent
9a6a2d9d27
commit
b6b2c5d1d1
3 changed files with 179 additions and 75 deletions
|
@ -8,7 +8,7 @@ use gpui::{
|
||||||
fonts, AppContext, AssetSource,
|
fonts, AppContext, AssetSource,
|
||||||
};
|
};
|
||||||
use schemars::{
|
use schemars::{
|
||||||
gen::{SchemaGenerator, SchemaSettings},
|
gen::SchemaGenerator,
|
||||||
schema::{InstanceType, ObjectValidation, Schema, SchemaObject, SingleOrVec},
|
schema::{InstanceType, ObjectValidation, Schema, SchemaObject, SingleOrVec},
|
||||||
JsonSchema,
|
JsonSchema,
|
||||||
};
|
};
|
||||||
|
@ -25,7 +25,7 @@ use util::ResultExt as _;
|
||||||
|
|
||||||
pub use keymap_file::{keymap_file_json_schema, KeymapFileContent};
|
pub use keymap_file::{keymap_file_json_schema, KeymapFileContent};
|
||||||
pub use settings_file::*;
|
pub use settings_file::*;
|
||||||
pub use settings_store::SettingsStore;
|
pub use settings_store::{SettingsJsonSchemaParams, SettingsStore};
|
||||||
|
|
||||||
pub const DEFAULT_SETTINGS_ASSET_PATH: &str = "settings/default.json";
|
pub const DEFAULT_SETTINGS_ASSET_PATH: &str = "settings/default.json";
|
||||||
pub const INITIAL_USER_SETTINGS_ASSET_PATH: &str = "settings/initial_user_settings.json";
|
pub const INITIAL_USER_SETTINGS_ASSET_PATH: &str = "settings/initial_user_settings.json";
|
||||||
|
@ -150,6 +150,75 @@ impl Setting for Settings {
|
||||||
|
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn json_schema(
|
||||||
|
generator: &mut SchemaGenerator,
|
||||||
|
params: &SettingsJsonSchemaParams,
|
||||||
|
) -> schemars::schema::RootSchema {
|
||||||
|
let mut root_schema = generator.root_schema_for::<SettingsFileContent>();
|
||||||
|
|
||||||
|
// Create a schema for a theme name.
|
||||||
|
let theme_name_schema = SchemaObject {
|
||||||
|
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
|
||||||
|
enum_values: Some(
|
||||||
|
params
|
||||||
|
.theme_names
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.map(Value::String)
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create a schema for a 'languages overrides' object, associating editor
|
||||||
|
// settings with specific langauges.
|
||||||
|
assert!(root_schema.definitions.contains_key("EditorSettings"));
|
||||||
|
|
||||||
|
let languages_object_schema = SchemaObject {
|
||||||
|
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Object))),
|
||||||
|
object: Some(Box::new(ObjectValidation {
|
||||||
|
properties: params
|
||||||
|
.language_names
|
||||||
|
.iter()
|
||||||
|
.map(|name| {
|
||||||
|
(
|
||||||
|
name.clone(),
|
||||||
|
Schema::new_ref("#/definitions/EditorSettings".into()),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
..Default::default()
|
||||||
|
})),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add these new schemas as definitions, and modify properties of the root
|
||||||
|
// schema to reference them.
|
||||||
|
root_schema.definitions.extend([
|
||||||
|
("ThemeName".into(), theme_name_schema.into()),
|
||||||
|
("Languages".into(), languages_object_schema.into()),
|
||||||
|
]);
|
||||||
|
let root_schema_object = &mut root_schema.schema.object.as_mut().unwrap();
|
||||||
|
|
||||||
|
root_schema_object.properties.extend([
|
||||||
|
(
|
||||||
|
"theme".to_owned(),
|
||||||
|
Schema::new_ref("#/definitions/ThemeName".into()),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"languages".to_owned(),
|
||||||
|
Schema::new_ref("#/definitions/Languages".into()),
|
||||||
|
),
|
||||||
|
// For backward compatibility
|
||||||
|
(
|
||||||
|
"language_overrides".to_owned(),
|
||||||
|
Schema::new_ref("#/definitions/Languages".into()),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
root_schema
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
|
||||||
|
@ -926,72 +995,6 @@ impl Settings {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn settings_file_json_schema(
|
|
||||||
theme_names: Vec<String>,
|
|
||||||
language_names: &[String],
|
|
||||||
) -> serde_json::Value {
|
|
||||||
let settings = SchemaSettings::draft07().with(|settings| {
|
|
||||||
settings.option_add_null_type = false;
|
|
||||||
});
|
|
||||||
let generator = SchemaGenerator::new(settings);
|
|
||||||
|
|
||||||
let mut root_schema = generator.into_root_schema_for::<SettingsFileContent>();
|
|
||||||
|
|
||||||
// Create a schema for a theme name.
|
|
||||||
let theme_name_schema = SchemaObject {
|
|
||||||
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
|
|
||||||
enum_values: Some(theme_names.into_iter().map(Value::String).collect()),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create a schema for a 'languages overrides' object, associating editor
|
|
||||||
// settings with specific langauges.
|
|
||||||
assert!(root_schema.definitions.contains_key("EditorSettings"));
|
|
||||||
|
|
||||||
let languages_object_schema = SchemaObject {
|
|
||||||
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Object))),
|
|
||||||
object: Some(Box::new(ObjectValidation {
|
|
||||||
properties: language_names
|
|
||||||
.iter()
|
|
||||||
.map(|name| {
|
|
||||||
(
|
|
||||||
name.clone(),
|
|
||||||
Schema::new_ref("#/definitions/EditorSettings".into()),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
..Default::default()
|
|
||||||
})),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add these new schemas as definitions, and modify properties of the root
|
|
||||||
// schema to reference them.
|
|
||||||
root_schema.definitions.extend([
|
|
||||||
("ThemeName".into(), theme_name_schema.into()),
|
|
||||||
("Languages".into(), languages_object_schema.into()),
|
|
||||||
]);
|
|
||||||
let root_schema_object = &mut root_schema.schema.object.as_mut().unwrap();
|
|
||||||
|
|
||||||
root_schema_object.properties.extend([
|
|
||||||
(
|
|
||||||
"theme".to_owned(),
|
|
||||||
Schema::new_ref("#/definitions/ThemeName".into()),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"languages".to_owned(),
|
|
||||||
Schema::new_ref("#/definitions/Languages".into()),
|
|
||||||
),
|
|
||||||
// For backward compatibility
|
|
||||||
(
|
|
||||||
"language_overrides".to_owned(),
|
|
||||||
Schema::new_ref("#/definitions/Languages".into()),
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
|
|
||||||
serde_json::to_value(root_schema).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn merge<T: Copy>(target: &mut T, value: Option<T>) {
|
fn merge<T: Copy>(target: &mut T, value: Option<T>) {
|
||||||
if let Some(value) = value {
|
if let Some(value) = value {
|
||||||
*target = value;
|
*target = value;
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use collections::{hash_map, BTreeMap, HashMap, HashSet};
|
use collections::{btree_map, hash_map, BTreeMap, HashMap, HashSet};
|
||||||
use gpui::AppContext;
|
use gpui::AppContext;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use schemars::JsonSchema;
|
use schemars::{gen::SchemaGenerator, schema::RootSchema, JsonSchema};
|
||||||
use serde::{de::DeserializeOwned, Deserialize as _, Serialize};
|
use serde::{de::DeserializeOwned, Deserialize as _, Serialize};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use std::{
|
use std::{
|
||||||
|
@ -39,6 +39,10 @@ pub trait Setting: 'static {
|
||||||
cx: &AppContext,
|
cx: &AppContext,
|
||||||
) -> Self;
|
) -> Self;
|
||||||
|
|
||||||
|
fn json_schema(generator: &mut SchemaGenerator, _: &SettingsJsonSchemaParams) -> RootSchema {
|
||||||
|
generator.root_schema_for::<Self::FileContent>()
|
||||||
|
}
|
||||||
|
|
||||||
fn load_via_json_merge(
|
fn load_via_json_merge(
|
||||||
default_value: &Self::FileContent,
|
default_value: &Self::FileContent,
|
||||||
user_values: &[&Self::FileContent],
|
user_values: &[&Self::FileContent],
|
||||||
|
@ -54,6 +58,11 @@ pub trait Setting: 'static {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct SettingsJsonSchemaParams<'a> {
|
||||||
|
pub theme_names: &'a [String],
|
||||||
|
pub language_names: &'a [String],
|
||||||
|
}
|
||||||
|
|
||||||
/// A set of strongly-typed setting values defined via multiple JSON files.
|
/// A set of strongly-typed setting values defined via multiple JSON files.
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct SettingsStore {
|
pub struct SettingsStore {
|
||||||
|
@ -84,6 +93,11 @@ trait AnySettingValue {
|
||||||
fn value_for_path(&self, path: Option<&Path>) -> &dyn Any;
|
fn value_for_path(&self, path: Option<&Path>) -> &dyn Any;
|
||||||
fn set_global_value(&mut self, value: Box<dyn Any>);
|
fn set_global_value(&mut self, value: Box<dyn Any>);
|
||||||
fn set_local_value(&mut self, path: Arc<Path>, value: Box<dyn Any>);
|
fn set_local_value(&mut self, path: Arc<Path>, value: Box<dyn Any>);
|
||||||
|
fn json_schema(
|
||||||
|
&self,
|
||||||
|
generator: &mut SchemaGenerator,
|
||||||
|
_: &SettingsJsonSchemaParams,
|
||||||
|
) -> RootSchema;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct DeserializedSetting(Box<dyn Any>);
|
struct DeserializedSetting(Box<dyn Any>);
|
||||||
|
@ -270,6 +284,79 @@ impl SettingsStore {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn json_schema(&self, schema_params: &SettingsJsonSchemaParams) -> serde_json::Value {
|
||||||
|
use schemars::{
|
||||||
|
gen::SchemaSettings,
|
||||||
|
schema::{Schema, SchemaObject},
|
||||||
|
};
|
||||||
|
|
||||||
|
let settings = SchemaSettings::draft07().with(|settings| {
|
||||||
|
settings.option_add_null_type = false;
|
||||||
|
});
|
||||||
|
let mut generator = SchemaGenerator::new(settings);
|
||||||
|
let mut combined_schema = RootSchema::default();
|
||||||
|
|
||||||
|
for setting_value in self.setting_values.values() {
|
||||||
|
let setting_schema = setting_value.json_schema(&mut generator, schema_params);
|
||||||
|
combined_schema
|
||||||
|
.definitions
|
||||||
|
.extend(setting_schema.definitions);
|
||||||
|
|
||||||
|
let target_schema = if let Some(key) = setting_value.key() {
|
||||||
|
let key_schema = combined_schema
|
||||||
|
.schema
|
||||||
|
.object()
|
||||||
|
.properties
|
||||||
|
.entry(key.to_string())
|
||||||
|
.or_insert_with(|| Schema::Object(SchemaObject::default()));
|
||||||
|
if let Schema::Object(key_schema) = key_schema {
|
||||||
|
key_schema
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
&mut combined_schema.schema
|
||||||
|
};
|
||||||
|
|
||||||
|
merge_schema(target_schema, setting_schema.schema);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn merge_schema(target: &mut SchemaObject, source: SchemaObject) {
|
||||||
|
if let Some(source) = source.object {
|
||||||
|
let target_properties = &mut target.object().properties;
|
||||||
|
for (key, value) in source.properties {
|
||||||
|
match target_properties.entry(key) {
|
||||||
|
btree_map::Entry::Vacant(e) => {
|
||||||
|
e.insert(value);
|
||||||
|
}
|
||||||
|
btree_map::Entry::Occupied(e) => {
|
||||||
|
if let (Schema::Object(target), Schema::Object(src)) =
|
||||||
|
(e.into_mut(), value)
|
||||||
|
{
|
||||||
|
merge_schema(target, src);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
overwrite(&mut target.instance_type, source.instance_type);
|
||||||
|
overwrite(&mut target.string, source.string);
|
||||||
|
overwrite(&mut target.number, source.number);
|
||||||
|
overwrite(&mut target.reference, source.reference);
|
||||||
|
overwrite(&mut target.array, source.array);
|
||||||
|
overwrite(&mut target.enum_values, source.enum_values);
|
||||||
|
|
||||||
|
fn overwrite<T>(target: &mut Option<T>, source: Option<T>) {
|
||||||
|
if let Some(source) = source {
|
||||||
|
*target = Some(source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
serde_json::to_value(&combined_schema).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
fn recompute_values(
|
fn recompute_values(
|
||||||
&mut self,
|
&mut self,
|
||||||
user_settings_changed: bool,
|
user_settings_changed: bool,
|
||||||
|
@ -457,6 +544,14 @@ impl<T: Setting> AnySettingValue for SettingValue<T> {
|
||||||
Err(ix) => self.local_values.insert(ix, (path, value)),
|
Err(ix) => self.local_values.insert(ix, (path, value)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn json_schema(
|
||||||
|
&self,
|
||||||
|
generator: &mut SchemaGenerator,
|
||||||
|
params: &SettingsJsonSchemaParams,
|
||||||
|
) -> RootSchema {
|
||||||
|
T::json_schema(generator, params)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// impl Debug for SettingsStore {
|
// impl Debug for SettingsStore {
|
||||||
|
|
|
@ -6,7 +6,7 @@ use gpui::AppContext;
|
||||||
use language::{LanguageRegistry, LanguageServerBinary, LanguageServerName, LspAdapter};
|
use language::{LanguageRegistry, LanguageServerBinary, LanguageServerName, LspAdapter};
|
||||||
use node_runtime::NodeRuntime;
|
use node_runtime::NodeRuntime;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use settings::{keymap_file_json_schema, settings_file_json_schema};
|
use settings::{keymap_file_json_schema, SettingsJsonSchemaParams, SettingsStore};
|
||||||
use smol::fs;
|
use smol::fs;
|
||||||
use staff_mode::StaffMode;
|
use staff_mode::StaffMode;
|
||||||
use std::{
|
use std::{
|
||||||
|
@ -128,12 +128,18 @@ impl LspAdapter for JsonLspAdapter {
|
||||||
cx: &mut AppContext,
|
cx: &mut AppContext,
|
||||||
) -> Option<BoxFuture<'static, serde_json::Value>> {
|
) -> Option<BoxFuture<'static, serde_json::Value>> {
|
||||||
let action_names = cx.all_action_names().collect::<Vec<_>>();
|
let action_names = cx.all_action_names().collect::<Vec<_>>();
|
||||||
let theme_names = self
|
let theme_names = &self
|
||||||
.themes
|
.themes
|
||||||
.list(**cx.default_global::<StaffMode>())
|
.list(**cx.default_global::<StaffMode>())
|
||||||
.map(|meta| meta.name)
|
.map(|meta| meta.name)
|
||||||
.collect();
|
.collect::<Vec<_>>();
|
||||||
let language_names = self.languages.language_names();
|
let language_names = &self.languages.language_names();
|
||||||
|
let settings_schema = cx
|
||||||
|
.global::<SettingsStore>()
|
||||||
|
.json_schema(&SettingsJsonSchemaParams {
|
||||||
|
theme_names,
|
||||||
|
language_names,
|
||||||
|
});
|
||||||
Some(
|
Some(
|
||||||
future::ready(serde_json::json!({
|
future::ready(serde_json::json!({
|
||||||
"json": {
|
"json": {
|
||||||
|
@ -143,7 +149,7 @@ impl LspAdapter for JsonLspAdapter {
|
||||||
"schemas": [
|
"schemas": [
|
||||||
{
|
{
|
||||||
"fileMatch": [schema_file_match(&paths::SETTINGS)],
|
"fileMatch": [schema_file_match(&paths::SETTINGS)],
|
||||||
"schema": settings_file_json_schema(theme_names, &language_names),
|
"schema": settings_schema,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fileMatch": [schema_file_match(&paths::KEYMAP)],
|
"fileMatch": [schema_file_match(&paths::KEYMAP)],
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue