diff --git a/crates/json_schema_store/src/json_schema_store.rs b/crates/json_schema_store/src/json_schema_store.rs index 9a5cc658c99ecba4460077c03d6d8d1b445c3284..99db65377926898e15dec2b3cf7df060f4c068bd 100644 --- a/crates/json_schema_store/src/json_schema_store.rs +++ b/crates/json_schema_store/src/json_schema_store.rs @@ -73,6 +73,8 @@ pub fn init(cx: &mut App) { } cx.update_global::(|schema_store, cx| { schema_store.notify_schema_changed(&format!("{SCHEMA_URI_PREFIX}settings"), cx); + schema_store + .notify_schema_changed(&format!("{SCHEMA_URI_PREFIX}project_settings"), cx); }); }) .detach(); @@ -294,6 +296,34 @@ async fn resolve_dynamic_schema( ) }) } + "project_settings" => { + let lsp_adapter_names = languages + .all_lsp_adapters() + .into_iter() + .map(|adapter| adapter.name().to_string()) + .collect::>(); + + cx.update(|cx| { + let language_names = &languages + .language_names() + .into_iter() + .map(|name| name.to_string()) + .collect::>(); + + cx.global::().project_json_schema( + &settings::SettingsJsonSchemaParams { + language_names, + lsp_adapter_names: &lsp_adapter_names, + // These are not allowed in project-specific settings but + // they're still fields required by the + // `SettingsJsonSchemaParams` struct. + font_names: &[], + theme_names: &[], + icon_theme_names: &[], + }, + ) + }) + } "debug_tasks" => { let adapter_schemas = cx.read_global::(|dap_registry, _| { dap_registry.adapters_schema() @@ -344,10 +374,14 @@ pub fn all_schema_file_associations( { "fileMatch": [ schema_file_match(paths::settings_file()), - paths::local_settings_file_relative_path() ], "url": format!("{SCHEMA_URI_PREFIX}settings"), }, + { + "fileMatch": [ + paths::local_settings_file_relative_path()], + "url": format!("{SCHEMA_URI_PREFIX}project_settings"), + }, { "fileMatch": [schema_file_match(paths::keymap_file())], "url": format!("{SCHEMA_URI_PREFIX}keymap"), diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index fbf1feff4de49916239ecb380e7451cd38de494c..28efa544c0a6c438b55d048917f213402ba47da9 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -989,42 +989,76 @@ impl SettingsStore { .map(|((_, path), content)| (path.clone(), &content.project)) } - pub fn json_schema(&self, params: &SettingsJsonSchemaParams) -> Value { - let mut generator = schemars::generate::SchemaSettings::draft2019_09() - .with_transform(DefaultDenyUnknownFields) - .with_transform(AllowTrailingCommas) - .into_generator(); - - UserSettingsContent::json_schema(&mut generator); - + /// Configures common schema replacements shared between user and project + /// settings schemas. + /// + /// This sets up language-specific settings and LSP adapter settings that + /// are valid in both user and project settings. + fn configure_schema_generator( + generator: &mut schemars::SchemaGenerator, + params: &SettingsJsonSchemaParams, + ) { let language_settings_content_ref = generator .subschema_for::() .to_value(); + replace_subschema::(generator, || { + json_schema!({ + "type": "object", + "errorMessage": "No language with this name is installed.", + "properties": params.language_names.iter().map(|name| (name.clone(), language_settings_content_ref.clone())).collect::>() + }) + }); + generator.subschema_for::(); - let lsp_settings_def = generator + let lsp_settings_definition = generator .definitions() .get("LspSettings") .expect("LspSettings should be defined") .clone(); - replace_subschema::(&mut generator, || { + replace_subschema::(generator, || { + let mut lsp_properties = serde_json::Map::new(); + + for adapter_name in params.lsp_adapter_names { + let mut base_lsp_settings = lsp_settings_definition + .as_object() + .expect("LspSettings should be an object") + .clone(); + + if let Some(properties) = base_lsp_settings.get_mut("properties") { + if let Some(properties_object) = properties.as_object_mut() { + properties_object.insert( + "initialization_options".to_string(), + serde_json::json!({ + "$ref": format!("{LSP_SETTINGS_SCHEMA_URL_PREFIX}{adapter_name}") + }), + ); + } + } + + lsp_properties.insert( + adapter_name.clone(), + serde_json::Value::Object(base_lsp_settings), + ); + } + json_schema!({ "type": "object", - "properties": params - .language_names - .iter() - .map(|name| { - ( - name.clone(), - language_settings_content_ref.clone(), - ) - }) - .collect::>(), - "errorMessage": "No language with this name is installed." + "properties": lsp_properties }) }); + } + + pub fn json_schema(&self, params: &SettingsJsonSchemaParams) -> Value { + let mut generator = schemars::generate::SchemaSettings::draft2019_09() + .with_transform(DefaultDenyUnknownFields) + .with_transform(AllowTrailingCommas) + .into_generator(); + + UserSettingsContent::json_schema(&mut generator); + Self::configure_schema_generator(&mut generator, params); replace_subschema::(&mut generator, || { json_schema!({ @@ -1047,40 +1081,24 @@ impl SettingsStore { }) }); - replace_subschema::(&mut generator, || { - let mut lsp_properties = serde_json::Map::new(); - - for adapter_name in params.lsp_adapter_names { - let mut base_lsp_settings = lsp_settings_def - .as_object() - .expect("LspSettings should be an object") - .clone(); - - if let Some(properties) = base_lsp_settings.get_mut("properties") { - if let Some(props_obj) = properties.as_object_mut() { - props_obj.insert( - "initialization_options".to_string(), - serde_json::json!({ - "$ref": format!("{LSP_SETTINGS_SCHEMA_URL_PREFIX}{adapter_name}") - }), - ); - } - } + generator + .root_schema_for::() + .to_value() + } - lsp_properties.insert( - adapter_name.clone(), - serde_json::Value::Object(base_lsp_settings), - ); - } + /// Generate JSON schema for project settings, including only settings valid + /// for project-level configurations. + pub fn project_json_schema(&self, params: &SettingsJsonSchemaParams) -> Value { + let mut generator = schemars::generate::SchemaSettings::draft2019_09() + .with_transform(DefaultDenyUnknownFields) + .with_transform(AllowTrailingCommas) + .into_generator(); - json_schema!({ - "type": "object", - "properties": lsp_properties, - }) - }); + ProjectSettingsContent::json_schema(&mut generator); + Self::configure_schema_generator(&mut generator, params); generator - .root_schema_for::() + .root_schema_for::() .to_value() } @@ -2336,4 +2354,63 @@ mod tests { assert_eq!(init_options_ref, "zed://schemas/settings/lsp/rust-analyzer"); } + + #[gpui::test] + fn test_lsp_project_settings_schema_generation(cx: &mut App) { + let store = SettingsStore::test(cx); + + let schema = store.project_json_schema(&SettingsJsonSchemaParams { + language_names: &["Rust".to_string(), "TypeScript".to_string()], + font_names: &["Zed Mono".to_string()], + theme_names: &["One Dark".into()], + icon_theme_names: &["Zed Icons".into()], + lsp_adapter_names: &[ + "rust-analyzer".to_string(), + "typescript-language-server".to_string(), + ], + }); + + let properties = schema + .pointer("/$defs/LspSettingsMap/properties") + .expect("LspSettingsMap should have properties") + .as_object() + .unwrap(); + + assert!(properties.contains_key("rust-analyzer")); + assert!(properties.contains_key("typescript-language-server")); + + let init_options_ref = properties + .get("rust-analyzer") + .unwrap() + .pointer("/properties/initialization_options/$ref") + .expect("initialization_options should have a $ref") + .as_str() + .unwrap(); + + assert_eq!(init_options_ref, "zed://schemas/settings/lsp/rust-analyzer"); + } + + #[gpui::test] + fn test_project_json_schema_differs_from_user_schema(cx: &mut App) { + let store = SettingsStore::test(cx); + + let params = SettingsJsonSchemaParams { + language_names: &["Rust".to_string()], + font_names: &["Zed Mono".to_string()], + theme_names: &["One Dark".into()], + icon_theme_names: &["Zed Icons".into()], + lsp_adapter_names: &["rust-analyzer".to_string()], + }; + + let user_schema = store.json_schema(¶ms); + let project_schema = store.project_json_schema(¶ms); + + assert_ne!(user_schema, project_schema); + + let user_schema_str = serde_json::to_string(&user_schema).unwrap(); + let project_schema_str = serde_json::to_string(&project_schema).unwrap(); + + assert!(user_schema_str.contains("\"auto_update\"")); + assert!(!project_schema_str.contains("\"auto_update\"")); + } }