dynamically inject theme names and language properties into schema

Keith Simmons created

Change summary

crates/language/src/language.rs |   8 ++
crates/settings/src/settings.rs | 138 +++++++++++++++++++++-------------
crates/zed/src/zed.rs           |   5 +
3 files changed, 97 insertions(+), 54 deletions(-)

Detailed changes

crates/language/src/language.rs 🔗

@@ -234,6 +234,14 @@ impl LanguageRegistry {
             .cloned()
     }
 
+    pub fn language_names(&self) -> Vec<String> {
+        self.languages
+            .read()
+            .iter()
+            .map(|language| language.name().to_string())
+            .collect()
+    }
+
     pub fn select_language(&self, path: impl AsRef<Path>) -> Option<Arc<Language>> {
         let path = path.as_ref();
         let filename = path.file_name().and_then(|name| name.to_str());

crates/settings/src/settings.rs 🔗

@@ -2,10 +2,14 @@ use anyhow::Result;
 use gpui::font_cache::{FamilyId, FontCache};
 use schemars::{
     gen::{SchemaGenerator, SchemaSettings},
+    schema::{
+        InstanceType, ObjectValidation, Schema, SchemaObject, SingleOrVec, SubschemaValidation,
+    },
     JsonSchema,
 };
 use serde::Deserialize;
-use std::{collections::HashMap, fmt::Display, sync::Arc};
+use serde_json::Value;
+use std::{collections::HashMap, fmt::Display, os::unix::fs::chroot, sync::Arc};
 use theme::{Theme, ThemeRegistry};
 use util::ResultExt as _;
 
@@ -36,53 +40,6 @@ pub enum SoftWrap {
     PreferredLineLength,
 }
 
-#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
-pub enum ThemeNames {
-    Dark,
-    Light,
-}
-
-impl Display for ThemeNames {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        match self {
-            ThemeNames::Dark => write!(f, "Dark"),
-            ThemeNames::Light => write!(f, "Light"),
-        }
-    }
-}
-
-#[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
-pub struct LanguageOverrides {
-    C: Option<LanguageOverride>,
-    JSON: Option<LanguageOverride>,
-    Markdown: Option<LanguageOverride>,
-    PlainText: Option<LanguageOverride>,
-    Rust: Option<LanguageOverride>,
-    TSX: Option<LanguageOverride>,
-    TypeScript: Option<LanguageOverride>,
-}
-
-impl IntoIterator for LanguageOverrides {
-    type Item = (String, LanguageOverride);
-    type IntoIter = std::vec::IntoIter<Self::Item>;
-
-    fn into_iter(self) -> Self::IntoIter {
-        vec![
-            ("C", self.C),
-            ("JSON", self.JSON),
-            ("Markdown", self.Markdown),
-            ("PlainText", self.PlainText),
-            ("Rust", self.Rust),
-            ("TSX", self.TSX),
-            ("TypeScript", self.TypeScript),
-        ]
-        .into_iter()
-        .filter_map(|(name, language_override)| language_override.map(|lo| (name.to_owned(), lo)))
-        .collect::<Vec<_>>()
-        .into_iter()
-    }
-}
-
 #[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
 pub struct SettingsFileContent {
     #[serde(default)]
@@ -94,9 +51,9 @@ pub struct SettingsFileContent {
     #[serde(flatten)]
     pub editor: LanguageOverride,
     #[serde(default)]
-    pub language_overrides: LanguageOverrides,
+    pub language_overrides: HashMap<Arc<str>, LanguageOverride>,
     #[serde(default)]
-    pub theme: Option<ThemeNames>,
+    pub theme: Option<String>,
 }
 
 impl Settings {
@@ -117,13 +74,88 @@ impl Settings {
         })
     }
 
-    pub fn file_json_schema() -> serde_json::Value {
+    pub fn file_json_schema(
+        theme_names: Vec<String>,
+        language_names: Vec<String>,
+    ) -> serde_json::Value {
         let settings = SchemaSettings::draft07().with(|settings| {
-            settings.option_nullable = true;
             settings.option_add_null_type = false;
         });
         let generator = SchemaGenerator::new(settings);
-        serde_json::to_value(generator.into_root_schema_for::<SettingsFileContent>()).unwrap()
+        let mut root_schema = generator.into_root_schema_for::<SettingsFileContent>();
+
+        // Construct theme names reference type
+        let theme_names = theme_names
+            .into_iter()
+            .map(|name| Value::String(name))
+            .collect();
+        let theme_names_schema = Schema::Object(SchemaObject {
+            instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
+            enum_values: Some(theme_names),
+            ..Default::default()
+        });
+        root_schema
+            .definitions
+            .insert("ThemeName".to_owned(), theme_names_schema);
+
+        // Construct language overrides reference type
+        let language_override_schema_reference = Schema::Object(SchemaObject {
+            reference: Some("#/definitions/LanguageOverride".to_owned()),
+            ..Default::default()
+        });
+        let language_overrides_properties = language_names
+            .into_iter()
+            .map(|name| {
+                (
+                    name,
+                    Schema::Object(SchemaObject {
+                        subschemas: Some(Box::new(SubschemaValidation {
+                            all_of: Some(vec![language_override_schema_reference.clone()]),
+                            ..Default::default()
+                        })),
+                        ..Default::default()
+                    }),
+                )
+            })
+            .collect();
+        let language_overrides_schema = Schema::Object(SchemaObject {
+            instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Object))),
+            object: Some(Box::new(ObjectValidation {
+                properties: language_overrides_properties,
+                ..Default::default()
+            })),
+            ..Default::default()
+        });
+        root_schema
+            .definitions
+            .insert("LanguageOverrides".to_owned(), language_overrides_schema);
+
+        // Modify theme property to use new theme reference type
+        let settings_file_schema = root_schema.schema.object.as_mut().unwrap();
+        let language_overrides_schema_reference = Schema::Object(SchemaObject {
+            reference: Some("#/definitions/ThemeName".to_owned()),
+            ..Default::default()
+        });
+        settings_file_schema.properties.insert(
+            "theme".to_owned(),
+            Schema::Object(SchemaObject {
+                subschemas: Some(Box::new(SubschemaValidation {
+                    all_of: Some(vec![language_overrides_schema_reference]),
+                    ..Default::default()
+                })),
+                ..Default::default()
+            }),
+        );
+
+        // Modify language_overrides property to use LanguageOverrides reference
+        settings_file_schema.properties.insert(
+            "language_overrides".to_owned(),
+            Schema::Object(SchemaObject {
+                reference: Some("#/definitions/LanguageOverrides".to_owned()),
+                ..Default::default()
+            }),
+        );
+        serde_json::to_value(root_schema).unwrap()
     }
 
     pub fn with_overrides(

crates/zed/src/zed.rs 🔗

@@ -137,13 +137,16 @@ pub fn build_workspace(
     let mut workspace = Workspace::new(&workspace_params, cx);
     let project = workspace.project().clone();
 
+    let theme_names = app_state.themes.list().collect();
+    let language_names = app_state.languages.language_names();
+
     project.update(cx, |project, _| {
         project.set_language_server_settings(serde_json::json!({
             "json": {
                 "schemas": [
                     {
                         "fileMatch": "**/.zed/settings.json",
-                        "schema": Settings::file_json_schema(),
+                        "schema": Settings::file_json_schema(theme_names, language_names),
                     }
                 ]
             }