diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index c3911f9254..3bc7eb81df 100644 --- a/crates/settings/src/settings.rs +++ b/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::(); + + // 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, - 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::(); - - // 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(target: &mut T, value: Option) { if let Some(value) = value { *target = value; diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 394d457d3d..887b4eef11 100644 --- a/crates/settings/src/settings_store.rs +++ b/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::() + } + 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); fn set_local_value(&mut self, path: Arc, value: Box); + fn json_schema( + &self, + generator: &mut SchemaGenerator, + _: &SettingsJsonSchemaParams, + ) -> RootSchema; } struct DeserializedSetting(Box); @@ -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(target: &mut Option, source: Option) { + 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 AnySettingValue for SettingValue { 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 { diff --git a/crates/zed/src/languages/json.rs b/crates/zed/src/languages/json.rs index d87d36abfe..8ea07c626d 100644 --- a/crates/zed/src/languages/json.rs +++ b/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> { let action_names = cx.all_action_names().collect::>(); - let theme_names = self + let theme_names = &self .themes .list(**cx.default_global::()) .map(|meta| meta.name) - .collect(); - let language_names = self.languages.language_names(); + .collect::>(); + let language_names = &self.languages.language_names(); + let settings_schema = cx + .global::() + .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)],