mirror of
https://github.com/zed-industries/zed.git
synced 2024-10-26 16:37:58 +00:00
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,
|
||||
};
|
||||
use schemars::{
|
||||
gen::{SchemaGenerator, SchemaSettings},
|
||||
gen::SchemaGenerator,
|
||||
schema::{InstanceType, ObjectValidation, Schema, SchemaObject, SingleOrVec},
|
||||
JsonSchema,
|
||||
};
|
||||
|
@ -25,7 +25,7 @@ use util::ResultExt as _;
|
|||
|
||||
pub use keymap_file::{keymap_file_json_schema, KeymapFileContent};
|
||||
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 INITIAL_USER_SETTINGS_ASSET_PATH: &str = "settings/initial_user_settings.json";
|
||||
|
@ -150,6 +150,75 @@ impl Setting for Settings {
|
|||
|
||||
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)]
|
||||
|
@ -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>) {
|
||||
if let Some(value) = value {
|
||||
*target = value;
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
use anyhow::{anyhow, Result};
|
||||
use collections::{hash_map, BTreeMap, HashMap, HashSet};
|
||||
use collections::{btree_map, hash_map, BTreeMap, HashMap, HashSet};
|
||||
use gpui::AppContext;
|
||||
use lazy_static::lazy_static;
|
||||
use schemars::JsonSchema;
|
||||
use schemars::{gen::SchemaGenerator, schema::RootSchema, JsonSchema};
|
||||
use serde::{de::DeserializeOwned, Deserialize as _, Serialize};
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
|
@ -39,6 +39,10 @@ pub trait Setting: 'static {
|
|||
cx: &AppContext,
|
||||
) -> Self;
|
||||
|
||||
fn json_schema(generator: &mut SchemaGenerator, _: &SettingsJsonSchemaParams) -> RootSchema {
|
||||
generator.root_schema_for::<Self::FileContent>()
|
||||
}
|
||||
|
||||
fn load_via_json_merge(
|
||||
default_value: &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.
|
||||
#[derive(Default)]
|
||||
pub struct SettingsStore {
|
||||
|
@ -84,6 +93,11 @@ trait AnySettingValue {
|
|||
fn value_for_path(&self, path: Option<&Path>) -> &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 json_schema(
|
||||
&self,
|
||||
generator: &mut SchemaGenerator,
|
||||
_: &SettingsJsonSchemaParams,
|
||||
) -> RootSchema;
|
||||
}
|
||||
|
||||
struct DeserializedSetting(Box<dyn Any>);
|
||||
|
@ -270,6 +284,79 @@ impl SettingsStore {
|
|||
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(
|
||||
&mut self,
|
||||
user_settings_changed: bool,
|
||||
|
@ -457,6 +544,14 @@ impl<T: Setting> AnySettingValue for SettingValue<T> {
|
|||
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 {
|
||||
|
|
|
@ -6,7 +6,7 @@ use gpui::AppContext;
|
|||
use language::{LanguageRegistry, LanguageServerBinary, LanguageServerName, LspAdapter};
|
||||
use node_runtime::NodeRuntime;
|
||||
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 staff_mode::StaffMode;
|
||||
use std::{
|
||||
|
@ -128,12 +128,18 @@ impl LspAdapter for JsonLspAdapter {
|
|||
cx: &mut AppContext,
|
||||
) -> Option<BoxFuture<'static, serde_json::Value>> {
|
||||
let action_names = cx.all_action_names().collect::<Vec<_>>();
|
||||
let theme_names = self
|
||||
let theme_names = &self
|
||||
.themes
|
||||
.list(**cx.default_global::<StaffMode>())
|
||||
.map(|meta| meta.name)
|
||||
.collect();
|
||||
let language_names = self.languages.language_names();
|
||||
.collect::<Vec<_>>();
|
||||
let language_names = &self.languages.language_names();
|
||||
let settings_schema = cx
|
||||
.global::<SettingsStore>()
|
||||
.json_schema(&SettingsJsonSchemaParams {
|
||||
theme_names,
|
||||
language_names,
|
||||
});
|
||||
Some(
|
||||
future::ready(serde_json::json!({
|
||||
"json": {
|
||||
|
@ -143,7 +149,7 @@ impl LspAdapter for JsonLspAdapter {
|
|||
"schemas": [
|
||||
{
|
||||
"fileMatch": [schema_file_match(&paths::SETTINGS)],
|
||||
"schema": settings_file_json_schema(theme_names, &language_names),
|
||||
"schema": settings_schema,
|
||||
},
|
||||
{
|
||||
"fileMatch": [schema_file_match(&paths::KEYMAP)],
|
||||
|
|
Loading…
Reference in a new issue