Generalize settings JSON schema logic to work w/ arbitrary setting types

Max Brunsfeld created

Change summary

crates/settings/src/settings.rs       | 139 ++++++++++++++--------------
crates/settings/src/settings_store.rs |  99 ++++++++++++++++++++
crates/zed/src/languages/json.rs      |  16 ++-
3 files changed, 179 insertions(+), 75 deletions(-)

Detailed changes

crates/settings/src/settings.rs 🔗

@@ -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;

crates/settings/src/settings_store.rs 🔗

@@ -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 {

crates/zed/src/languages/json.rs 🔗

@@ -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)],