From 5fafab6e52f87cd5d6235628b12e6c62a2db0fd4 Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Mon, 30 Jun 2025 15:07:28 -0600 Subject: [PATCH] Migrate to schemars version 1.0 (#33635) The major change in schemars 1.0 is that now schemas are represented as plain json values instead of specialized datatypes. This allows for more concise construction and manipulation. This change also improves how settings schemas are generated. Each top level settings type was being generated as a full root schema including the definitions it references, and then these were merged. This meant generating all shared definitions multiple times, and might have bugs in cases where there are two types with the same names. Now instead the schemar generator's `definitions` are built up as they normally are and the `Settings` trait no longer has a special `json_schema` method. To handle types that have schema that vary at runtime (`FontFamilyName`, `ThemeName`, etc), values of `ParameterizedJsonSchema` are collected by `inventory`, and the schema definitions for these types are replaced. To help check that this doesn't break anything, I tried to minimize the overall [schema diff](https://gist.github.com/mgsloan/1de549def20399d6f37943a3c1583ee7) with some patches to make the order more consistent + schemas also sorted with `jq -S .`. A skim of the diff shows that the diffs come from: * `enum: ["value"]` turning into `const: "value"` * Differences in handling of newlines for "description" * Schemas for generic types no longer including the parameter name, now all disambiguation is with numeric suffixes * Enums now using `oneOf` instead of `anyOf`. Release Notes: - N/A --- Cargo.lock | 13 +- Cargo.toml | 4 +- crates/agent_settings/src/agent_settings.rs | 41 ++- crates/assistant_tools/src/schema.rs | 69 ++--- crates/collab/src/tests/integration_tests.rs | 19 +- .../remote_editing_collaboration_tests.rs | 10 +- crates/dap/src/adapters.rs | 6 +- crates/editor/src/editor_settings_controls.rs | 4 +- crates/editor/src/editor_tests.rs | 54 ++-- crates/gpui/src/action.rs | 12 +- crates/gpui/src/app.rs | 4 +- crates/gpui/src/color.rs | 27 +- crates/gpui/src/geometry.rs | 57 ++-- crates/gpui/src/shared_string.rs | 15 +- crates/gpui/src/text_system/font_features.rs | 41 +-- crates/gpui_macros/src/derive_action.rs | 4 +- .../src/inline_completion_button.rs | 1 + crates/language/Cargo.toml | 1 + crates/language/src/buffer_tests.rs | 2 +- crates/language/src/language.rs | 20 +- crates/language/src/language_registry.rs | 2 +- crates/language/src/language_settings.rs | 246 ++++++----------- crates/languages/src/json.rs | 5 +- crates/lsp/src/lsp.rs | 25 +- crates/project/src/lsp_store.rs | 4 +- crates/project/src/prettier_store.rs | 1 - crates/project/src/project_tests.rs | 6 +- crates/settings/src/keymap_file.rs | 209 ++++++-------- crates/settings/src/settings_json.rs | 101 +++---- crates/settings/src/settings_store.rs | 260 ++++++++++-------- .../src/appearance_settings_controls.rs | 6 +- crates/snippet_provider/src/format.rs | 29 +- crates/task/src/debug_format.rs | 3 +- crates/task/src/serde_helpers.rs | 27 -- crates/task/src/task_template.rs | 12 +- crates/terminal/src/terminal_settings.rs | 43 +-- crates/terminal_view/src/terminal_element.rs | 9 +- crates/theme/Cargo.toml | 1 + crates/theme/src/schema.rs | 34 +-- crates/theme/src/settings.rs | 236 ++++++++-------- crates/vim/src/normal/paste.rs | 2 +- tooling/workspace-hack/Cargo.toml | 4 +- 42 files changed, 710 insertions(+), 959 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1abe5bc059a8c7e38a0008f02f54d033b92e3748..3ba97e9382545494b9ac9cdfecf2e2a6342b6cfb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4133,7 +4133,7 @@ dependencies = [ [[package]] name = "dap-types" version = "0.0.1" -source = "git+https://github.com/zed-industries/dap-types?rev=b40956a7f4d1939da67429d941389ee306a3a308#b40956a7f4d1939da67429d941389ee306a3a308" +source = "git+https://github.com/zed-industries/dap-types?rev=7f39295b441614ca9dbf44293e53c32f666897f9#7f39295b441614ca9dbf44293e53c32f666897f9" dependencies = [ "schemars", "serde", @@ -8847,6 +8847,7 @@ dependencies = [ "http_client", "imara-diff", "indoc", + "inventory", "itertools 0.14.0", "log", "lsp", @@ -14053,12 +14054,13 @@ dependencies = [ [[package]] name = "schemars" -version = "0.8.22" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +checksum = "fe8c9d1c68d67dd9f97ecbc6f932b60eb289c5dbddd8aa1405484a8fd2fcd984" dependencies = [ "dyn-clone", "indexmap", + "ref-cast", "schemars_derive", "serde", "serde_json", @@ -14066,9 +14068,9 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "0.8.22" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +checksum = "6ca9fcb757952f8e8629b9ab066fc62da523c46c2b247b1708a3be06dd82530b" dependencies = [ "proc-macro2", "quote", @@ -16010,6 +16012,7 @@ dependencies = [ "futures 0.3.31", "gpui", "indexmap", + "inventory", "log", "palette", "parking_lot", diff --git a/Cargo.toml b/Cargo.toml index 4239fcf1e9e52d63282ac879067da3bba47acf25..2fee4059c61f1722b7786093aa468c673059aba2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -444,7 +444,7 @@ core-video = { version = "0.4.3", features = ["metal"] } cpal = "0.16" criterion = { version = "0.5", features = ["html_reports"] } ctor = "0.4.0" -dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "b40956a7f4d1939da67429d941389ee306a3a308" } +dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "7f39295b441614ca9dbf44293e53c32f666897f9" } dashmap = "6.0" derive_more = "0.99.17" dirs = "4.0" @@ -540,7 +540,7 @@ rustc-hash = "2.1.0" rustls = { version = "0.23.26" } rustls-platform-verifier = "0.5.0" scap = { git = "https://github.com/zed-industries/scap", rev = "08f0a01417505cc0990b9931a37e5120db92e0d0", default-features = false } -schemars = { version = "0.8", features = ["impl_json_schema", "indexmap2"] } +schemars = { version = "1.0", features = ["indexmap2"] } semver = "1.0" serde = { version = "1.0", features = ["derive", "rc"] } serde_derive = { version = "1.0", features = ["deserialize_in_place"] } diff --git a/crates/agent_settings/src/agent_settings.rs b/crates/agent_settings/src/agent_settings.rs index 294d793e79ec534e2318f03db5fbc9a75821ecc0..019ab18c204c2af9955242a87985581a16e9c02b 100644 --- a/crates/agent_settings/src/agent_settings.rs +++ b/crates/agent_settings/src/agent_settings.rs @@ -6,9 +6,10 @@ use anyhow::{Result, bail}; use collections::IndexMap; use gpui::{App, Pixels, SharedString}; use language_model::LanguageModel; -use schemars::{JsonSchema, schema::Schema}; +use schemars::{JsonSchema, json_schema}; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsSources}; +use std::borrow::Cow; pub use crate::agent_profile::*; @@ -321,29 +322,27 @@ pub struct LanguageModelSelection { pub struct LanguageModelProviderSetting(pub String); impl JsonSchema for LanguageModelProviderSetting { - fn schema_name() -> String { + fn schema_name() -> Cow<'static, str> { "LanguageModelProviderSetting".into() } - fn json_schema(_: &mut schemars::r#gen::SchemaGenerator) -> Schema { - schemars::schema::SchemaObject { - enum_values: Some(vec![ - "anthropic".into(), - "amazon-bedrock".into(), - "google".into(), - "lmstudio".into(), - "ollama".into(), - "openai".into(), - "zed.dev".into(), - "copilot_chat".into(), - "deepseek".into(), - "openrouter".into(), - "mistral".into(), - "vercel".into(), - ]), - ..Default::default() - } - .into() + fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema { + json_schema!({ + "enum": [ + "anthropic", + "amazon-bedrock", + "google", + "lmstudio", + "ollama", + "openai", + "zed.dev", + "copilot_chat", + "deepseek", + "openrouter", + "mistral", + "vercel" + ] + }) } } diff --git a/crates/assistant_tools/src/schema.rs b/crates/assistant_tools/src/schema.rs index 4a71d47d2cdba4d92711c9dd4549d036df58b0ad..888e11de4e83df853d5d1c252d30cecf84c701a2 100644 --- a/crates/assistant_tools/src/schema.rs +++ b/crates/assistant_tools/src/schema.rs @@ -1,8 +1,9 @@ use anyhow::Result; use language_model::LanguageModelToolSchemaFormat; use schemars::{ - JsonSchema, - schema::{RootSchema, Schema, SchemaObject}, + JsonSchema, Schema, + generate::SchemaSettings, + transform::{Transform, transform_subschemas}, }; pub fn json_schema_for( @@ -13,7 +14,7 @@ pub fn json_schema_for( } fn schema_to_json( - schema: &RootSchema, + schema: &Schema, format: LanguageModelToolSchemaFormat, ) -> Result { let mut value = serde_json::to_value(schema)?; @@ -21,58 +22,42 @@ fn schema_to_json( Ok(value) } -fn root_schema_for(format: LanguageModelToolSchemaFormat) -> RootSchema { +fn root_schema_for(format: LanguageModelToolSchemaFormat) -> Schema { let mut generator = match format { - LanguageModelToolSchemaFormat::JsonSchema => schemars::SchemaGenerator::default(), - LanguageModelToolSchemaFormat::JsonSchemaSubset => { - schemars::r#gen::SchemaSettings::default() - .with(|settings| { - settings.meta_schema = None; - settings.inline_subschemas = true; - settings - .visitors - .push(Box::new(TransformToJsonSchemaSubsetVisitor)); - }) - .into_generator() - } + LanguageModelToolSchemaFormat::JsonSchema => SchemaSettings::draft07().into_generator(), + // TODO: Gemini docs mention using a subset of OpenAPI 3, so this may benefit from using + // `SchemaSettings::openapi3()`. + LanguageModelToolSchemaFormat::JsonSchemaSubset => SchemaSettings::draft07() + .with(|settings| { + settings.meta_schema = None; + settings.inline_subschemas = true; + }) + .with_transform(ToJsonSchemaSubsetTransform) + .into_generator(), }; generator.root_schema_for::() } #[derive(Debug, Clone)] -struct TransformToJsonSchemaSubsetVisitor; - -impl schemars::visit::Visitor for TransformToJsonSchemaSubsetVisitor { - fn visit_root_schema(&mut self, root: &mut RootSchema) { - schemars::visit::visit_root_schema(self, root) - } +struct ToJsonSchemaSubsetTransform; - fn visit_schema(&mut self, schema: &mut Schema) { - schemars::visit::visit_schema(self, schema) - } - - fn visit_schema_object(&mut self, schema: &mut SchemaObject) { +impl Transform for ToJsonSchemaSubsetTransform { + fn transform(&mut self, schema: &mut Schema) { // Ensure that the type field is not an array, this happens when we use // Option, the type will be [T, "null"]. - if let Some(instance_type) = schema.instance_type.take() { - schema.instance_type = match instance_type { - schemars::schema::SingleOrVec::Single(t) => { - Some(schemars::schema::SingleOrVec::Single(t)) + if let Some(type_field) = schema.get_mut("type") { + if let Some(types) = type_field.as_array() { + if let Some(first_type) = types.first() { + *type_field = first_type.clone(); } - schemars::schema::SingleOrVec::Vec(items) => items - .into_iter() - .next() - .map(schemars::schema::SingleOrVec::from), - }; + } } - // One of is not supported, use anyOf instead. - if let Some(subschema) = schema.subschemas.as_mut() { - if let Some(one_of) = subschema.one_of.take() { - subschema.any_of = Some(one_of); - } + // oneOf is not supported, use anyOf instead + if let Some(one_of) = schema.remove("oneOf") { + schema.insert("anyOf".to_string(), one_of); } - schemars::visit::visit_schema_object(self, schema) + transform_subschemas(self, schema); } } diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 145a31a179c38eba5ad0312b24ba96afcaa69b49..55427b1aa70fe59dd330e274ddade4839c73affd 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -22,9 +22,7 @@ use gpui::{ use language::{ Diagnostic, DiagnosticEntry, DiagnosticSourceKind, FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LineEnding, OffsetRangeExt, Point, Rope, - language_settings::{ - AllLanguageSettings, Formatter, FormatterList, PrettierSettings, SelectedFormatter, - }, + language_settings::{AllLanguageSettings, Formatter, PrettierSettings, SelectedFormatter}, tree_sitter_rust, tree_sitter_typescript, }; use lsp::{LanguageServerId, OneOf}; @@ -4591,15 +4589,13 @@ async fn test_formatting_buffer( cx_a.update(|cx| { SettingsStore::update_global(cx, |store, cx| { store.update_user_settings::(cx, |file| { - file.defaults.formatter = Some(SelectedFormatter::List(FormatterList( - vec![Formatter::External { + file.defaults.formatter = + Some(SelectedFormatter::List(vec![Formatter::External { command: "awk".into(), arguments: Some( vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()].into(), ), - }] - .into(), - ))); + }])); }); }); }); @@ -4699,9 +4695,10 @@ async fn test_prettier_formatting_buffer( cx_b.update(|cx| { SettingsStore::update_global(cx, |store, cx| { store.update_user_settings::(cx, |file| { - file.defaults.formatter = Some(SelectedFormatter::List(FormatterList( - vec![Formatter::LanguageServer { name: None }].into(), - ))); + file.defaults.formatter = + Some(SelectedFormatter::List(vec![Formatter::LanguageServer { + name: None, + }])); file.defaults.prettier = Some(PrettierSettings { allowed: true, ..PrettierSettings::default() diff --git a/crates/collab/src/tests/remote_editing_collaboration_tests.rs b/crates/collab/src/tests/remote_editing_collaboration_tests.rs index 217273a38787730c1cf6d5535e3e8e456cffb64a..0e9b25dc380fee929274d2a84607e55ee6832cb8 100644 --- a/crates/collab/src/tests/remote_editing_collaboration_tests.rs +++ b/crates/collab/src/tests/remote_editing_collaboration_tests.rs @@ -14,8 +14,7 @@ use http_client::BlockedHttpClient; use language::{ FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageRegistry, language_settings::{ - AllLanguageSettings, Formatter, FormatterList, PrettierSettings, SelectedFormatter, - language_settings, + AllLanguageSettings, Formatter, PrettierSettings, SelectedFormatter, language_settings, }, tree_sitter_typescript, }; @@ -505,9 +504,10 @@ async fn test_ssh_collaboration_formatting_with_prettier( cx_b.update(|cx| { SettingsStore::update_global(cx, |store, cx| { store.update_user_settings::(cx, |file| { - file.defaults.formatter = Some(SelectedFormatter::List(FormatterList( - vec![Formatter::LanguageServer { name: None }].into(), - ))); + file.defaults.formatter = + Some(SelectedFormatter::List(vec![Formatter::LanguageServer { + name: None, + }])); file.defaults.prettier = Some(PrettierSettings { allowed: true, ..PrettierSettings::default() diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index 8e1c84083f18835dee6c4bc3bea4ce7c45147499..d9f26b3b348985f2e52423cb217b1c1446960bbf 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -10,6 +10,7 @@ use gpui::{AsyncApp, SharedString}; pub use http_client::{HttpClient, github::latest_github_release}; use language::{LanguageName, LanguageToolchainStore}; use node_runtime::NodeRuntime; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::WorktreeId; use smol::fs::File; @@ -47,7 +48,10 @@ pub trait DapDelegate: Send + Sync + 'static { async fn shell_env(&self) -> collections::HashMap; } -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)] +#[derive( + Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize, JsonSchema, +)] +#[serde(transparent)] pub struct DebugAdapterName(pub SharedString); impl Deref for DebugAdapterName { diff --git a/crates/editor/src/editor_settings_controls.rs b/crates/editor/src/editor_settings_controls.rs index 54bb865520568ef2f5d0291c19a061a5c87d3568..dc5557b05277da972ea36ba43ffdf08a565edda9 100644 --- a/crates/editor/src/editor_settings_controls.rs +++ b/crates/editor/src/editor_settings_controls.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use gpui::{App, FontFeatures, FontWeight}; use project::project_settings::{InlineBlameSettings, ProjectSettings}; use settings::{EditableSettingControl, Settings}; -use theme::{FontFamilyCache, ThemeSettings}; +use theme::{FontFamilyCache, FontFamilyName, ThemeSettings}; use ui::{ CheckboxWithLabel, ContextMenu, DropdownMenu, NumericStepper, SettingsContainer, SettingsGroup, prelude::*, @@ -75,7 +75,7 @@ impl EditableSettingControl for BufferFontFamilyControl { value: Self::Value, _cx: &App, ) { - settings.buffer_font_family = Some(value.to_string()); + settings.buffer_font_family = Some(FontFamilyName(value.into())); } } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index a6bbe6d621d7901f85b414949cc41a3afa47248a..404284c4b08d7e9ba4776d857a774e86e69802fa 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -30,7 +30,7 @@ use language::{ }, tree_sitter_python, }; -use language_settings::{Formatter, FormatterList, IndentGuideSettings}; +use language_settings::{Formatter, IndentGuideSettings}; use lsp::CompletionParams; use multi_buffer::{IndentGuide, PathKey}; use parking_lot::Mutex; @@ -3567,7 +3567,7 @@ async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) { #[gpui::test] fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) { init_test(cx, |settings| { - settings.languages.extend([ + settings.languages.0.extend([ ( "TOML".into(), LanguageSettingsContent { @@ -5145,7 +5145,7 @@ fn test_transpose(cx: &mut TestAppContext) { #[gpui::test] async fn test_rewrap(cx: &mut TestAppContext) { init_test(cx, |settings| { - settings.languages.extend([ + settings.languages.0.extend([ ( "Markdown".into(), LanguageSettingsContent { @@ -9326,7 +9326,7 @@ async fn test_document_format_during_save(cx: &mut TestAppContext) { // Set rust language override and assert overridden tabsize is sent to language server update_test_language_settings(cx, |settings| { - settings.languages.insert( + settings.languages.0.insert( "Rust".into(), LanguageSettingsContent { tab_size: NonZeroU32::new(8), @@ -9890,7 +9890,7 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) { // Set Rust language override and assert overridden tabsize is sent to language server update_test_language_settings(cx, |settings| { - settings.languages.insert( + settings.languages.0.insert( "Rust".into(), LanguageSettingsContent { tab_size: NonZeroU32::new(8), @@ -9933,9 +9933,9 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) { #[gpui::test] async fn test_document_format_manual_trigger(cx: &mut TestAppContext) { init_test(cx, |settings| { - settings.defaults.formatter = Some(language_settings::SelectedFormatter::List( - FormatterList(vec![Formatter::LanguageServer { name: None }].into()), - )) + settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(vec![ + Formatter::LanguageServer { name: None }, + ])) }); let fs = FakeFs::new(cx.executor()); @@ -10062,21 +10062,17 @@ async fn test_document_format_manual_trigger(cx: &mut TestAppContext) { async fn test_multiple_formatters(cx: &mut TestAppContext) { init_test(cx, |settings| { settings.defaults.remove_trailing_whitespace_on_save = Some(true); - settings.defaults.formatter = - Some(language_settings::SelectedFormatter::List(FormatterList( - vec![ - Formatter::LanguageServer { name: None }, - Formatter::CodeActions( - [ - ("code-action-1".into(), true), - ("code-action-2".into(), true), - ] - .into_iter() - .collect(), - ), + settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(vec![ + Formatter::LanguageServer { name: None }, + Formatter::CodeActions( + [ + ("code-action-1".into(), true), + ("code-action-2".into(), true), ] - .into(), - ))) + .into_iter() + .collect(), + ), + ])) }); let fs = FakeFs::new(cx.executor()); @@ -10328,9 +10324,9 @@ async fn test_multiple_formatters(cx: &mut TestAppContext) { #[gpui::test] async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) { init_test(cx, |settings| { - settings.defaults.formatter = Some(language_settings::SelectedFormatter::List( - FormatterList(vec![Formatter::LanguageServer { name: None }].into()), - )) + settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(vec![ + Formatter::LanguageServer { name: None }, + ])) }); let fs = FakeFs::new(cx.executor()); @@ -14905,7 +14901,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppCon .unwrap(); let _fake_server = fake_servers.next().await.unwrap(); update_test_language_settings(cx, |language_settings| { - language_settings.languages.insert( + language_settings.languages.0.insert( language_name.clone(), LanguageSettingsContent { tab_size: NonZeroU32::new(8), @@ -15803,9 +15799,9 @@ fn completion_menu_entries(menu: &CompletionsMenu) -> Vec { #[gpui::test] async fn test_document_format_with_prettier(cx: &mut TestAppContext) { init_test(cx, |settings| { - settings.defaults.formatter = Some(language_settings::SelectedFormatter::List( - FormatterList(vec![Formatter::Prettier].into()), - )) + settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(vec![ + Formatter::Prettier, + ])) }); let fs = FakeFs::new(cx.executor()); diff --git a/crates/gpui/src/action.rs b/crates/gpui/src/action.rs index 24fbd70b63d87f564301153073c5ebe7b7fdaa32..7885497034c1a9b0e3404503d36e9f4fdc24b276 100644 --- a/crates/gpui/src/action.rs +++ b/crates/gpui/src/action.rs @@ -125,9 +125,7 @@ pub trait Action: Any + Send { Self: Sized; /// Optional JSON schema for the action's input data. - fn action_json_schema( - _: &mut schemars::r#gen::SchemaGenerator, - ) -> Option + fn action_json_schema(_: &mut schemars::SchemaGenerator) -> Option where Self: Sized, { @@ -238,7 +236,7 @@ impl Default for ActionRegistry { struct ActionData { pub build: ActionBuilder, - pub json_schema: fn(&mut schemars::r#gen::SchemaGenerator) -> Option, + pub json_schema: fn(&mut schemars::SchemaGenerator) -> Option, } /// This type must be public so that our macros can build it in other crates. @@ -253,7 +251,7 @@ pub struct MacroActionData { pub name: &'static str, pub type_id: TypeId, pub build: ActionBuilder, - pub json_schema: fn(&mut schemars::r#gen::SchemaGenerator) -> Option, + pub json_schema: fn(&mut schemars::SchemaGenerator) -> Option, pub deprecated_aliases: &'static [&'static str], pub deprecation_message: Option<&'static str>, } @@ -357,8 +355,8 @@ impl ActionRegistry { pub fn action_schemas( &self, - generator: &mut schemars::r#gen::SchemaGenerator, - ) -> Vec<(&'static str, Option)> { + generator: &mut schemars::SchemaGenerator, + ) -> Vec<(&'static str, Option)> { // Use the order from all_names so that the resulting schema has sensible order. self.all_names .iter() diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 1853e6e93488e0cba9db2380594eb3f28b4a0132..5fee4c904778e3942c0448c5da2645cb6c8781dd 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1388,8 +1388,8 @@ impl App { /// Get all non-internal actions that have been registered, along with their schemas. pub fn action_schemas( &self, - generator: &mut schemars::r#gen::SchemaGenerator, - ) -> Vec<(&'static str, Option)> { + generator: &mut schemars::SchemaGenerator, + ) -> Vec<(&'static str, Option)> { self.actions.action_schemas(generator) } diff --git a/crates/gpui/src/color.rs b/crates/gpui/src/color.rs index 1115d1c99c8c8edc1a43f92c4746470c800b7bef..7fc9c24393907d3991edcf9ae82b25eee419e766 100644 --- a/crates/gpui/src/color.rs +++ b/crates/gpui/src/color.rs @@ -1,9 +1,10 @@ use anyhow::{Context as _, bail}; -use schemars::{JsonSchema, SchemaGenerator, schema::Schema}; +use schemars::{JsonSchema, json_schema}; use serde::{ Deserialize, Deserializer, Serialize, Serializer, de::{self, Visitor}, }; +use std::borrow::Cow; use std::{ fmt::{self, Display, Formatter}, hash::{Hash, Hasher}, @@ -99,22 +100,14 @@ impl Visitor<'_> for RgbaVisitor { } impl JsonSchema for Rgba { - fn schema_name() -> String { - "Rgba".to_string() + fn schema_name() -> Cow<'static, str> { + "Rgba".into() } - fn json_schema(_generator: &mut SchemaGenerator) -> Schema { - use schemars::schema::{InstanceType, SchemaObject, StringValidation}; - - Schema::Object(SchemaObject { - instance_type: Some(InstanceType::String.into()), - string: Some(Box::new(StringValidation { - pattern: Some( - r"^#([0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$".to_string(), - ), - ..Default::default() - })), - ..Default::default() + fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema { + json_schema!({ + "type": "string", + "pattern": "^#([0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$" }) } } @@ -629,11 +622,11 @@ impl From for Hsla { } impl JsonSchema for Hsla { - fn schema_name() -> String { + fn schema_name() -> Cow<'static, str> { Rgba::schema_name() } - fn json_schema(generator: &mut SchemaGenerator) -> Schema { + fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema { Rgba::json_schema(generator) } } diff --git a/crates/gpui/src/geometry.rs b/crates/gpui/src/geometry.rs index 30283c8ddedd01744660f5fd08ab8597ed670a56..74be6344f92a2c478318641be5a78eb7bacfe28e 100644 --- a/crates/gpui/src/geometry.rs +++ b/crates/gpui/src/geometry.rs @@ -6,8 +6,9 @@ use anyhow::{Context as _, anyhow}; use core::fmt::Debug; use derive_more::{Add, AddAssign, Div, DivAssign, Mul, Neg, Sub, SubAssign}; use refineable::Refineable; -use schemars::{JsonSchema, SchemaGenerator, schema::Schema}; +use schemars::{JsonSchema, json_schema}; use serde::{Deserialize, Deserializer, Serialize, Serializer, de}; +use std::borrow::Cow; use std::{ cmp::{self, PartialOrd}, fmt::{self, Display}, @@ -3229,20 +3230,14 @@ impl TryFrom<&'_ str> for AbsoluteLength { } impl JsonSchema for AbsoluteLength { - fn schema_name() -> String { - "AbsoluteLength".to_string() + fn schema_name() -> Cow<'static, str> { + "AbsoluteLength".into() } - fn json_schema(_generator: &mut SchemaGenerator) -> Schema { - use schemars::schema::{InstanceType, SchemaObject, StringValidation}; - - Schema::Object(SchemaObject { - instance_type: Some(InstanceType::String.into()), - string: Some(Box::new(StringValidation { - pattern: Some(r"^-?\d+(\.\d+)?(px|rem)$".to_string()), - ..Default::default() - })), - ..Default::default() + fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema { + json_schema!({ + "type": "string", + "pattern": r"^-?\d+(\.\d+)?(px|rem)$" }) } } @@ -3366,20 +3361,14 @@ impl TryFrom<&'_ str> for DefiniteLength { } impl JsonSchema for DefiniteLength { - fn schema_name() -> String { - "DefiniteLength".to_string() + fn schema_name() -> Cow<'static, str> { + "DefiniteLength".into() } - fn json_schema(_generator: &mut SchemaGenerator) -> Schema { - use schemars::schema::{InstanceType, SchemaObject, StringValidation}; - - Schema::Object(SchemaObject { - instance_type: Some(InstanceType::String.into()), - string: Some(Box::new(StringValidation { - pattern: Some(r"^-?\d+(\.\d+)?(px|rem|%)$".to_string()), - ..Default::default() - })), - ..Default::default() + fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema { + json_schema!({ + "type": "string", + "pattern": r"^-?\d+(\.\d+)?(px|rem|%)$" }) } } @@ -3480,20 +3469,14 @@ impl TryFrom<&'_ str> for Length { } impl JsonSchema for Length { - fn schema_name() -> String { - "Length".to_string() + fn schema_name() -> Cow<'static, str> { + "Length".into() } - fn json_schema(_generator: &mut SchemaGenerator) -> Schema { - use schemars::schema::{InstanceType, SchemaObject, StringValidation}; - - Schema::Object(SchemaObject { - instance_type: Some(InstanceType::String.into()), - string: Some(Box::new(StringValidation { - pattern: Some(r"^(auto|-?\d+(\.\d+)?(px|rem|%))$".to_string()), - ..Default::default() - })), - ..Default::default() + fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema { + json_schema!({ + "type": "string", + "pattern": r"^(auto|-?\d+(\.\d+)?(px|rem|%))$" }) } } diff --git a/crates/gpui/src/shared_string.rs b/crates/gpui/src/shared_string.rs index 591bada48d7c0f200a83f5a5319e183e4ce2021f..c325f98cd243121264875d7a9452308772d49e86 100644 --- a/crates/gpui/src/shared_string.rs +++ b/crates/gpui/src/shared_string.rs @@ -2,7 +2,10 @@ use derive_more::{Deref, DerefMut}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use std::{borrow::Borrow, sync::Arc}; +use std::{ + borrow::{Borrow, Cow}, + sync::Arc, +}; use util::arc_cow::ArcCow; /// A shared string is an immutable string that can be cheaply cloned in GPUI @@ -23,12 +26,16 @@ impl SharedString { } impl JsonSchema for SharedString { - fn schema_name() -> String { + fn inline_schema() -> bool { + String::inline_schema() + } + + fn schema_name() -> Cow<'static, str> { String::schema_name() } - fn json_schema(r#gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { - String::json_schema(r#gen) + fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema { + String::json_schema(generator) } } diff --git a/crates/gpui/src/text_system/font_features.rs b/crates/gpui/src/text_system/font_features.rs index 9fca90380791160e31b3771eb061104c91bc2bd9..f95a0581f109877edd6f6b73354f1051c17314d5 100644 --- a/crates/gpui/src/text_system/font_features.rs +++ b/crates/gpui/src/text_system/font_features.rs @@ -1,6 +1,7 @@ +use std::borrow::Cow; use std::sync::Arc; -use schemars::schema::{InstanceType, SchemaObject}; +use schemars::{JsonSchema, json_schema}; /// The OpenType features that can be configured for a given font. #[derive(Default, Clone, Eq, PartialEq, Hash)] @@ -128,36 +129,22 @@ impl serde::Serialize for FontFeatures { } } -impl schemars::JsonSchema for FontFeatures { - fn schema_name() -> String { +impl JsonSchema for FontFeatures { + fn schema_name() -> Cow<'static, str> { "FontFeatures".into() } - fn json_schema(_: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { - let mut schema = SchemaObject::default(); - schema.instance_type = Some(schemars::schema::SingleOrVec::Single(Box::new( - InstanceType::Object, - ))); - { - let mut property = SchemaObject { - instance_type: Some(schemars::schema::SingleOrVec::Vec(vec![ - InstanceType::Boolean, - InstanceType::Integer, - ])), - ..Default::default() - }; - - { - let mut number_constraints = property.number(); - number_constraints.multiple_of = Some(1.0); - number_constraints.minimum = Some(0.0); + fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema { + json_schema!({ + "type": "object", + "patternProperties": { + "[0-9a-zA-Z]{4}$": { + "type": ["boolean", "integer"], + "minimum": 0, + "multipleOf": 1 + } } - schema - .object() - .pattern_properties - .insert("[0-9a-zA-Z]{4}$".into(), property.into()); - } - schema.into() + }) } } diff --git a/crates/gpui_macros/src/derive_action.rs b/crates/gpui_macros/src/derive_action.rs index c382ddd9c652902e1c444f080a601de16bfeca9a..c32baba6cbacdb809623c43aef7ec362b963d178 100644 --- a/crates/gpui_macros/src/derive_action.rs +++ b/crates/gpui_macros/src/derive_action.rs @@ -159,8 +159,8 @@ pub(crate) fn derive_action(input: TokenStream) -> TokenStream { } fn action_json_schema( - _generator: &mut gpui::private::schemars::r#gen::SchemaGenerator, - ) -> Option { + _generator: &mut gpui::private::schemars::SchemaGenerator, + ) -> Option { #json_schema_fn_body } diff --git a/crates/inline_completion_button/src/inline_completion_button.rs b/crates/inline_completion_button/src/inline_completion_button.rs index 4e9c887124d4583c0123db94508c3f2026fddc97..cf1e808f602803c971f2e7c604947ca5b7aee589 100644 --- a/crates/inline_completion_button/src/inline_completion_button.rs +++ b/crates/inline_completion_button/src/inline_completion_button.rs @@ -967,6 +967,7 @@ fn toggle_show_inline_completions_for_language( all_language_settings(None, cx).show_edit_predictions(Some(&language), cx); update_settings_file::(fs, cx, move |file, _| { file.languages + .0 .entry(language.name()) .or_default() .show_edit_predictions = Some(!show_edit_predictions); diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index b0e06c3d65a7bc05df0cb41104a1139353372539..477b978517d56d0f70270a4bf413b285b455ca94 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -39,6 +39,7 @@ globset.workspace = true gpui.workspace = true http_client.workspace = true imara-diff.workspace = true +inventory.workspace = true itertools.workspace = true log.workspace = true lsp.workspace = true diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index ebf7558abb28f8641baa0d52ba7f04e2af8289e9..6955cd054925076f8d2678eff58c44e0b82351d0 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -2006,7 +2006,7 @@ fn test_autoindent_language_without_indents_query(cx: &mut App) { #[gpui::test] fn test_autoindent_with_injected_languages(cx: &mut App) { init_settings(cx, |settings| { - settings.languages.extend([ + settings.languages.0.extend([ ( "HTML".into(), LanguageSettingsContent { diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index f77afc76d2ffae034e2f0d3d3d4d2507c919b518..951a0dbddcc2102ac30fded12f17ea28bab1d204 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -39,11 +39,7 @@ use lsp::{CodeActionKind, InitializeParams, LanguageServerBinary, LanguageServer pub use manifest::{ManifestDelegate, ManifestName, ManifestProvider, ManifestQuery}; use parking_lot::Mutex; use regex::Regex; -use schemars::{ - JsonSchema, - r#gen::SchemaGenerator, - schema::{InstanceType, Schema, SchemaObject}, -}; +use schemars::{JsonSchema, json_schema}; use serde::{Deserialize, Deserializer, Serialize, Serializer, de}; use serde_json::Value; use settings::WorktreeId; @@ -694,7 +690,6 @@ pub struct LanguageConfig { pub matcher: LanguageMatcher, /// List of bracket types in a language. #[serde(default)] - #[schemars(schema_with = "bracket_pair_config_json_schema")] pub brackets: BracketPairConfig, /// If set to true, auto indentation uses last non empty line to determine /// the indentation level for a new line. @@ -944,10 +939,9 @@ fn deserialize_regex<'de, D: Deserializer<'de>>(d: D) -> Result, D } } -fn regex_json_schema(_: &mut SchemaGenerator) -> Schema { - Schema::Object(SchemaObject { - instance_type: Some(InstanceType::String.into()), - ..Default::default() +fn regex_json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema { + json_schema!({ + "type": "string" }) } @@ -988,12 +982,12 @@ pub struct FakeLspAdapter { /// This struct includes settings for defining which pairs of characters are considered brackets and /// also specifies any language-specific scopes where these pairs should be ignored for bracket matching purposes. #[derive(Clone, Debug, Default, JsonSchema)] +#[schemars(with = "Vec::")] pub struct BracketPairConfig { /// A list of character pairs that should be treated as brackets in the context of a given language. pub pairs: Vec, /// A list of tree-sitter scopes for which a given bracket should not be active. /// N-th entry in `[Self::disabled_scopes_by_bracket_ix]` contains a list of disabled scopes for an n-th entry in `[Self::pairs]` - #[serde(skip)] pub disabled_scopes_by_bracket_ix: Vec>, } @@ -1003,10 +997,6 @@ impl BracketPairConfig { } } -fn bracket_pair_config_json_schema(r#gen: &mut SchemaGenerator) -> Schema { - Option::>::json_schema(r#gen) -} - #[derive(Deserialize, JsonSchema)] pub struct BracketPairContent { #[serde(flatten)] diff --git a/crates/language/src/language_registry.rs b/crates/language/src/language_registry.rs index b2bb684e1bb10d6edc72a41d3006d114a4b5f371..ff17d6dd9a9d7bb250f15d358d11eb23ef8f188f 100644 --- a/crates/language/src/language_registry.rs +++ b/crates/language/src/language_registry.rs @@ -1170,7 +1170,7 @@ impl LanguageRegistryState { if let Some(theme) = self.theme.as_ref() { language.set_theme(theme.syntax()); } - self.language_settings.languages.insert( + self.language_settings.languages.0.insert( language.name(), LanguageSettingsContent { tab_size: language.config.tab_size, diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 9dda60b6a685b7705563a7d218990af33b2577f2..d2b9005f979a8c119a6ce43b537327508da4134c 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -3,7 +3,6 @@ use crate::{File, Language, LanguageName, LanguageServerName}; use anyhow::Result; use collections::{FxHashMap, HashMap, HashSet}; -use core::slice; use ec4rs::{ Properties as EditorconfigProperties, property::{FinalNewline, IndentSize, IndentStyle, TabWidth, TrimTrailingWs}, @@ -11,17 +10,15 @@ use ec4rs::{ use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder}; use gpui::{App, Modifiers}; use itertools::{Either, Itertools}; -use schemars::{ - JsonSchema, - schema::{InstanceType, ObjectValidation, Schema, SchemaObject, SingleOrVec}, -}; +use schemars::{JsonSchema, json_schema}; use serde::{ Deserialize, Deserializer, Serialize, de::{self, IntoDeserializer, MapAccess, SeqAccess, Visitor}, }; -use serde_json::Value; + use settings::{ - Settings, SettingsLocation, SettingsSources, SettingsStore, add_references_to_properties, + ParameterizedJsonSchema, Settings, SettingsLocation, SettingsSources, SettingsStore, + replace_subschema, }; use shellexpand; use std::{borrow::Cow, num::NonZeroU32, path::Path, sync::Arc}; @@ -306,13 +303,42 @@ pub struct AllLanguageSettingsContent { pub defaults: LanguageSettingsContent, /// The settings for individual languages. #[serde(default)] - pub languages: HashMap, + pub languages: LanguageToSettingsMap, /// Settings for associating file extensions and filenames /// with languages. #[serde(default)] pub file_types: HashMap, Vec>, } +/// Map from language name to settings. Its `ParameterizedJsonSchema` allows only known language +/// names in the keys. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct LanguageToSettingsMap(pub HashMap); + +inventory::submit! { + ParameterizedJsonSchema { + add_and_get_ref: |generator, params, _cx| { + let language_settings_content_ref = generator + .subschema_for::() + .to_value(); + let schema = json_schema!({ + "type": "object", + "properties": params + .language_names + .iter() + .map(|name| { + ( + name.clone(), + language_settings_content_ref.clone(), + ) + }) + .collect::>() + }); + replace_subschema::(generator, schema) + } + } +} + /// Controls how completions are processed for this language. #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] @@ -648,45 +674,30 @@ pub enum FormatOnSave { On, /// Files should not be formatted on save. Off, - List(FormatterList), + List(Vec), } impl JsonSchema for FormatOnSave { - fn schema_name() -> String { + fn schema_name() -> Cow<'static, str> { "OnSaveFormatter".into() } - fn json_schema(generator: &mut schemars::r#gen::SchemaGenerator) -> Schema { - let mut schema = SchemaObject::default(); + fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema { let formatter_schema = Formatter::json_schema(generator); - schema.instance_type = Some( - vec![ - InstanceType::Object, - InstanceType::String, - InstanceType::Array, - ] - .into(), - ); - - let valid_raw_values = SchemaObject { - enum_values: Some(vec![ - Value::String("on".into()), - Value::String("off".into()), - Value::String("prettier".into()), - Value::String("language_server".into()), - ]), - ..Default::default() - }; - let mut nested_values = SchemaObject::default(); - nested_values.array().items = Some(formatter_schema.clone().into()); - - schema.subschemas().any_of = Some(vec![ - nested_values.into(), - valid_raw_values.into(), - formatter_schema, - ]); - schema.into() + json_schema!({ + "oneOf": [ + { + "type": "array", + "items": formatter_schema + }, + { + "type": "string", + "enum": ["on", "off", "prettier", "language_server"] + }, + formatter_schema + ] + }) } } @@ -725,11 +736,11 @@ impl<'de> Deserialize<'de> for FormatOnSave { } else if v == "off" { Ok(Self::Value::Off) } else if v == "language_server" { - Ok(Self::Value::List(FormatterList( - Formatter::LanguageServer { name: None }.into(), - ))) + Ok(Self::Value::List(vec![Formatter::LanguageServer { + name: None, + }])) } else { - let ret: Result = + let ret: Result, _> = Deserialize::deserialize(v.into_deserializer()); ret.map(Self::Value::List) } @@ -738,7 +749,7 @@ impl<'de> Deserialize<'de> for FormatOnSave { where A: MapAccess<'d>, { - let ret: Result = + let ret: Result, _> = Deserialize::deserialize(de::value::MapAccessDeserializer::new(map)); ret.map(Self::Value::List) } @@ -746,7 +757,7 @@ impl<'de> Deserialize<'de> for FormatOnSave { where A: SeqAccess<'d>, { - let ret: Result = + let ret: Result, _> = Deserialize::deserialize(de::value::SeqAccessDeserializer::new(map)); ret.map(Self::Value::List) } @@ -783,45 +794,30 @@ pub enum SelectedFormatter { /// or falling back to formatting via language server. #[default] Auto, - List(FormatterList), + List(Vec), } impl JsonSchema for SelectedFormatter { - fn schema_name() -> String { + fn schema_name() -> Cow<'static, str> { "Formatter".into() } - fn json_schema(generator: &mut schemars::r#gen::SchemaGenerator) -> Schema { - let mut schema = SchemaObject::default(); + fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema { let formatter_schema = Formatter::json_schema(generator); - schema.instance_type = Some( - vec![ - InstanceType::Object, - InstanceType::String, - InstanceType::Array, - ] - .into(), - ); - - let valid_raw_values = SchemaObject { - enum_values: Some(vec![ - Value::String("auto".into()), - Value::String("prettier".into()), - Value::String("language_server".into()), - ]), - ..Default::default() - }; - - let mut nested_values = SchemaObject::default(); - nested_values.array().items = Some(formatter_schema.clone().into()); - - schema.subschemas().any_of = Some(vec![ - nested_values.into(), - valid_raw_values.into(), - formatter_schema, - ]); - schema.into() + json_schema!({ + "oneOf": [ + { + "type": "array", + "items": formatter_schema + }, + { + "type": "string", + "enum": ["auto", "prettier", "language_server"] + }, + formatter_schema + ] + }) } } @@ -836,6 +832,7 @@ impl Serialize for SelectedFormatter { } } } + impl<'de> Deserialize<'de> for SelectedFormatter { fn deserialize(deserializer: D) -> std::result::Result where @@ -856,11 +853,11 @@ impl<'de> Deserialize<'de> for SelectedFormatter { if v == "auto" { Ok(Self::Value::Auto) } else if v == "language_server" { - Ok(Self::Value::List(FormatterList( - Formatter::LanguageServer { name: None }.into(), - ))) + Ok(Self::Value::List(vec![Formatter::LanguageServer { + name: None, + }])) } else { - let ret: Result = + let ret: Result, _> = Deserialize::deserialize(v.into_deserializer()); ret.map(SelectedFormatter::List) } @@ -869,7 +866,7 @@ impl<'de> Deserialize<'de> for SelectedFormatter { where A: MapAccess<'d>, { - let ret: Result = + let ret: Result, _> = Deserialize::deserialize(de::value::MapAccessDeserializer::new(map)); ret.map(SelectedFormatter::List) } @@ -877,7 +874,7 @@ impl<'de> Deserialize<'de> for SelectedFormatter { where A: SeqAccess<'d>, { - let ret: Result = + let ret: Result, _> = Deserialize::deserialize(de::value::SeqAccessDeserializer::new(map)); ret.map(SelectedFormatter::List) } @@ -885,19 +882,6 @@ impl<'de> Deserialize<'de> for SelectedFormatter { deserializer.deserialize_any(FormatDeserializer) } } -/// Controls which formatter should be used when formatting code. -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case", transparent)] -pub struct FormatterList(pub SingleOrVec); - -impl AsRef<[Formatter]> for FormatterList { - fn as_ref(&self) -> &[Formatter] { - match &self.0 { - SingleOrVec::Single(single) => slice::from_ref(single), - SingleOrVec::Vec(v) => v, - } - } -} /// Controls which formatter should be used when formatting code. If there are multiple formatters, they are executed in the order of declaration. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] @@ -1209,7 +1193,7 @@ impl settings::Settings for AllLanguageSettings { serde_json::from_value(serde_json::to_value(&default_value.defaults)?)?; let mut languages = HashMap::default(); - for (language_name, settings) in &default_value.languages { + for (language_name, settings) in &default_value.languages.0 { let mut language_settings = defaults.clone(); merge_settings(&mut language_settings, settings); languages.insert(language_name.clone(), language_settings); @@ -1310,7 +1294,7 @@ impl settings::Settings for AllLanguageSettings { } // A user's language-specific settings override default language-specific settings. - for (language_name, user_language_settings) in &user_settings.languages { + for (language_name, user_language_settings) in &user_settings.languages.0 { merge_settings( languages .entry(language_name.clone()) @@ -1366,51 +1350,6 @@ impl settings::Settings for AllLanguageSettings { }) } - fn json_schema( - generator: &mut schemars::r#gen::SchemaGenerator, - params: &settings::SettingsJsonSchemaParams, - _: &App, - ) -> schemars::schema::RootSchema { - let mut root_schema = generator.root_schema_for::(); - - // Create a schema for a 'languages overrides' object, associating editor - // settings with specific languages. - assert!( - root_schema - .definitions - .contains_key("LanguageSettingsContent") - ); - - let languages_object_schema = SchemaObject { - instance_type: Some(InstanceType::Object.into()), - object: Some(Box::new(ObjectValidation { - properties: params - .language_names - .iter() - .map(|name| { - ( - name.clone(), - Schema::new_ref("#/definitions/LanguageSettingsContent".into()), - ) - }) - .collect(), - ..Default::default() - })), - ..Default::default() - }; - - root_schema - .definitions - .extend([("Languages".into(), languages_object_schema.into())]); - - add_references_to_properties( - &mut root_schema, - &[("languages", "#/definitions/Languages")], - ); - - root_schema - } - fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) { let d = &mut current.defaults; if let Some(size) = vscode @@ -1674,29 +1613,26 @@ mod tests { let settings: LanguageSettingsContent = serde_json::from_str(raw).unwrap(); assert_eq!( settings.formatter, - Some(SelectedFormatter::List(FormatterList( - Formatter::LanguageServer { name: None }.into() - ))) + Some(SelectedFormatter::List(vec![Formatter::LanguageServer { + name: None + }])) ); let raw = "{\"formatter\": [{\"language_server\": {\"name\": null}}]}"; let settings: LanguageSettingsContent = serde_json::from_str(raw).unwrap(); assert_eq!( settings.formatter, - Some(SelectedFormatter::List(FormatterList( - vec![Formatter::LanguageServer { name: None }].into() - ))) + Some(SelectedFormatter::List(vec![Formatter::LanguageServer { + name: None + }])) ); let raw = "{\"formatter\": [{\"language_server\": {\"name\": null}}, \"prettier\"]}"; let settings: LanguageSettingsContent = serde_json::from_str(raw).unwrap(); assert_eq!( settings.formatter, - Some(SelectedFormatter::List(FormatterList( - vec![ - Formatter::LanguageServer { name: None }, - Formatter::Prettier - ] - .into() - ))) + Some(SelectedFormatter::List(vec![ + Formatter::LanguageServer { name: None }, + Formatter::Prettier + ])) ); } diff --git a/crates/languages/src/json.rs b/crates/languages/src/json.rs index 7a52a82f6b07699f7eeea23dfb346ae99b4b8c11..acc24bb29cd4a1810ef8f57a87407350206c23fe 100644 --- a/crates/languages/src/json.rs +++ b/crates/languages/src/json.rs @@ -269,10 +269,9 @@ impl JsonLspAdapter { #[cfg(debug_assertions)] fn generate_inspector_style_schema() -> serde_json_lenient::Value { - let schema = schemars::r#gen::SchemaSettings::draft07() - .with(|settings| settings.option_add_null_type = false) + let schema = schemars::generate::SchemaSettings::draft07() .into_generator() - .into_root_schema_for::(); + .root_schema_for::(); serde_json_lenient::to_value(schema).unwrap() } diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 28ad606132fcc61fc5e801c8442dcc62fad45357..53dc24a21a93fecee9a320a44a9b9c46655f31be 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -15,11 +15,7 @@ use gpui::{App, AppContext as _, AsyncApp, BackgroundExecutor, SharedString, Tas use notification::DidChangeWorkspaceFolders; use parking_lot::{Mutex, RwLock}; use postage::{barrier, prelude::Stream}; -use schemars::{ - JsonSchema, - r#gen::SchemaGenerator, - schema::{InstanceType, Schema, SchemaObject}, -}; +use schemars::JsonSchema; use serde::{Deserialize, Serialize, de::DeserializeOwned}; use serde_json::{Value, json, value::RawValue}; use smol::{ @@ -130,7 +126,10 @@ impl LanguageServerId { } /// A name of a language server. -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)] +#[derive( + Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize, JsonSchema, +)] +#[serde(transparent)] pub struct LanguageServerName(pub SharedString); impl std::fmt::Display for LanguageServerName { @@ -151,20 +150,6 @@ impl AsRef for LanguageServerName { } } -impl JsonSchema for LanguageServerName { - fn schema_name() -> String { - "LanguageServerName".into() - } - - fn json_schema(_: &mut SchemaGenerator) -> Schema { - SchemaObject { - instance_type: Some(InstanceType::String.into()), - ..Default::default() - } - .into() - } -} - impl LanguageServerName { pub const fn new_static(s: &'static str) -> Self { Self(SharedString::new_static(s)) diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index dc402be2b6db4070e9754b549e847ce653593297..9e1a38a6d165c74a9ec1d340927b9875f464c5ec 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -1405,7 +1405,7 @@ impl LocalLspStore { let formatters = match (trigger, &settings.format_on_save) { (FormatTrigger::Save, FormatOnSave::Off) => &[], - (FormatTrigger::Save, FormatOnSave::List(formatters)) => formatters.as_ref(), + (FormatTrigger::Save, FormatOnSave::List(formatters)) => formatters.as_slice(), (FormatTrigger::Manual, _) | (FormatTrigger::Save, FormatOnSave::On) => { match &settings.formatter { SelectedFormatter::Auto => { @@ -1417,7 +1417,7 @@ impl LocalLspStore { std::slice::from_ref(&Formatter::LanguageServer { name: None }) } } - SelectedFormatter::List(formatter_list) => formatter_list.as_ref(), + SelectedFormatter::List(formatter_list) => formatter_list.as_slice(), } } }; diff --git a/crates/project/src/prettier_store.rs b/crates/project/src/prettier_store.rs index 29997545cd484d0bacbd489b3c5fa058daa2f017..68a3ae8778c351b9290dccdc5355f0b3eb22562b 100644 --- a/crates/project/src/prettier_store.rs +++ b/crates/project/src/prettier_store.rs @@ -705,7 +705,6 @@ pub fn prettier_plugins_for_language( SelectedFormatter::Auto => Some(&language_settings.prettier.plugins), SelectedFormatter::List(list) => list - .as_ref() .contains(&Formatter::Prettier) .then_some(&language_settings.prettier.plugins), } diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 54a013bc4168f09aead0561a962a0255088f76dd..3e9a2c3273c3fa13d45286311fb7ceb80451f9b2 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -2023,7 +2023,7 @@ async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) { cx.update(|cx| { SettingsStore::update_global(cx, |settings, cx| { settings.update_user_settings::(cx, |settings| { - settings.languages.insert( + settings.languages.0.insert( "Rust".into(), LanguageSettingsContent { enable_language_server: Some(false), @@ -2042,14 +2042,14 @@ async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) { cx.update(|cx| { SettingsStore::update_global(cx, |settings, cx| { settings.update_user_settings::(cx, |settings| { - settings.languages.insert( + settings.languages.0.insert( LanguageName::new("Rust"), LanguageSettingsContent { enable_language_server: Some(true), ..Default::default() }, ); - settings.languages.insert( + settings.languages.0.insert( LanguageName::new("JavaScript"), LanguageSettingsContent { enable_language_server: Some(false), diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index 833882dd608211a54b8dab217094739864177f15..7ec34fde22f7791bf1df117a091dc16d24e1e036 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -5,13 +5,10 @@ use gpui::{ Action, ActionBuildError, App, InvalidKeystrokeError, KEYSTROKE_PARSE_EXPECTED_MESSAGE, KeyBinding, KeyBindingContextPredicate, KeyBindingMetaIndex, NoAction, }; -use schemars::{ - JsonSchema, - r#gen::{SchemaGenerator, SchemaSettings}, - schema::{ArrayValidation, InstanceType, Schema, SchemaObject, SubschemaValidation}, -}; +use schemars::{JsonSchema, json_schema}; use serde::Deserialize; -use serde_json::Value; +use serde_json::{Value, json}; +use std::borrow::Cow; use std::{any::TypeId, fmt::Write, rc::Rc, sync::Arc, sync::LazyLock}; use util::{ asset_str, @@ -123,14 +120,14 @@ impl std::fmt::Display for KeymapAction { impl JsonSchema for KeymapAction { /// This is used when generating the JSON schema for the `KeymapAction` type, so that it can /// reference the keymap action schema. - fn schema_name() -> String { + fn schema_name() -> Cow<'static, str> { "KeymapAction".into() } /// This schema will be replaced with the full action schema in /// `KeymapFile::generate_json_schema`. - fn json_schema(_: &mut SchemaGenerator) -> Schema { - Schema::Bool(true) + fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema { + json_schema!(true) } } @@ -424,9 +421,7 @@ impl KeymapFile { } pub fn generate_json_schema_for_registered_actions(cx: &mut App) -> Value { - let mut generator = SchemaSettings::draft07() - .with(|settings| settings.option_add_null_type = false) - .into_generator(); + let mut generator = schemars::generate::SchemaSettings::draft07().into_generator(); let action_schemas = cx.action_schemas(&mut generator); let deprecations = cx.deprecated_actions_to_preferred_actions(); @@ -440,92 +435,70 @@ impl KeymapFile { } fn generate_json_schema( - generator: SchemaGenerator, - action_schemas: Vec<(&'static str, Option)>, + mut generator: schemars::SchemaGenerator, + action_schemas: Vec<(&'static str, Option)>, deprecations: &HashMap<&'static str, &'static str>, deprecation_messages: &HashMap<&'static str, &'static str>, ) -> serde_json::Value { - fn set(input: I) -> Option - where - I: Into, - { - Some(input.into()) - } - - fn add_deprecation(schema_object: &mut SchemaObject, message: String) { - schema_object.extensions.insert( - // deprecationMessage is not part of the JSON Schema spec, - // but json-language-server recognizes it. - "deprecationMessage".to_owned(), + fn add_deprecation(schema: &mut schemars::Schema, message: String) { + schema.insert( + // deprecationMessage is not part of the JSON Schema spec, but + // json-language-server recognizes it. + "deprecationMessage".to_string(), Value::String(message), ); } - fn add_deprecation_preferred_name(schema_object: &mut SchemaObject, new_name: &str) { - add_deprecation(schema_object, format!("Deprecated, use {new_name}")); + fn add_deprecation_preferred_name(schema: &mut schemars::Schema, new_name: &str) { + add_deprecation(schema, format!("Deprecated, use {new_name}")); } - fn add_description(schema_object: &mut SchemaObject, description: String) { - schema_object - .metadata - .get_or_insert(Default::default()) - .description = Some(description); + fn add_description(schema: &mut schemars::Schema, description: String) { + schema.insert("description".to_string(), Value::String(description)); } - let empty_object: SchemaObject = SchemaObject { - instance_type: set(InstanceType::Object), - ..Default::default() - }; + let empty_object = json_schema!({ + "type": "object" + }); // This is a workaround for a json-language-server issue where it matches the first // alternative that matches the value's shape and uses that for documentation. // // In the case of the array validations, it would even provide an error saying that the name // must match the name of the first alternative. - let mut plain_action = SchemaObject { - instance_type: set(InstanceType::String), - const_value: Some(Value::String("".to_owned())), - ..Default::default() - }; + let mut plain_action = json_schema!({ + "type": "string", + "const": "" + }); let no_action_message = "No action named this."; add_description(&mut plain_action, no_action_message.to_owned()); add_deprecation(&mut plain_action, no_action_message.to_owned()); - let mut matches_action_name = SchemaObject { - const_value: Some(Value::String("".to_owned())), - ..Default::default() - }; - let no_action_message = "No action named this that takes input."; - add_description(&mut matches_action_name, no_action_message.to_owned()); - add_deprecation(&mut matches_action_name, no_action_message.to_owned()); - let action_with_input = SchemaObject { - instance_type: set(InstanceType::Array), - array: set(ArrayValidation { - items: set(vec![ - matches_action_name.into(), - // Accept any value, as we want this to be the preferred match when there is a - // typo in the name. - Schema::Bool(true), - ]), - min_items: Some(2), - max_items: Some(2), - ..Default::default() - }), - ..Default::default() - }; - let mut keymap_action_alternatives = vec![plain_action.into(), action_with_input.into()]; - for (name, action_schema) in action_schemas.into_iter() { - let schema = if let Some(Schema::Object(schema)) = action_schema { - Some(schema) - } else { - None - }; + let mut matches_action_name = json_schema!({ + "const": "" + }); + let no_action_message_input = "No action named this that takes input."; + add_description(&mut matches_action_name, no_action_message_input.to_owned()); + add_deprecation(&mut matches_action_name, no_action_message_input.to_owned()); + + let action_with_input = json_schema!({ + "type": "array", + "items": [ + matches_action_name, + true + ], + "minItems": 2, + "maxItems": 2 + }); + let mut keymap_action_alternatives = vec![plain_action, action_with_input]; - let description = schema.as_ref().and_then(|schema| { + for (name, action_schema) in action_schemas.into_iter() { + let description = action_schema.as_ref().and_then(|schema| { schema - .metadata - .as_ref() - .and_then(|metadata| metadata.description.clone()) + .as_object() + .and_then(|obj| obj.get("description")) + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) }); let deprecation = if name == NoAction.name() { @@ -535,84 +508,64 @@ impl KeymapFile { }; // Add an alternative for plain action names. - let mut plain_action = SchemaObject { - instance_type: set(InstanceType::String), - const_value: Some(Value::String(name.to_string())), - ..Default::default() - }; + let mut plain_action = json_schema!({ + "type": "string", + "const": name + }); if let Some(message) = deprecation_messages.get(name) { add_deprecation(&mut plain_action, message.to_string()); } else if let Some(new_name) = deprecation { add_deprecation_preferred_name(&mut plain_action, new_name); } - if let Some(description) = description.clone() { - add_description(&mut plain_action, description); + if let Some(desc) = description.clone() { + add_description(&mut plain_action, desc); } - keymap_action_alternatives.push(plain_action.into()); + keymap_action_alternatives.push(plain_action); // Add an alternative for actions with data specified as a [name, data] array. // - // When a struct with no deserializable fields is added with impl_actions! / - // impl_actions_as! an empty object schema is produced. The action should be invoked - // without data in this case. - if let Some(schema) = schema { + // When a struct with no deserializable fields is added by deriving `Action`, an empty + // object schema is produced. The action should be invoked without data in this case. + if let Some(schema) = action_schema { if schema != empty_object { - let mut matches_action_name = SchemaObject { - const_value: Some(Value::String(name.to_string())), - ..Default::default() - }; - if let Some(description) = description.clone() { - add_description(&mut matches_action_name, description); + let mut matches_action_name = json_schema!({ + "const": name + }); + if let Some(desc) = description.clone() { + add_description(&mut matches_action_name, desc); } if let Some(message) = deprecation_messages.get(name) { add_deprecation(&mut matches_action_name, message.to_string()); } else if let Some(new_name) = deprecation { add_deprecation_preferred_name(&mut matches_action_name, new_name); } - let action_with_input = SchemaObject { - instance_type: set(InstanceType::Array), - array: set(ArrayValidation { - items: set(vec![matches_action_name.into(), schema.into()]), - min_items: Some(2), - max_items: Some(2), - ..Default::default() - }), - ..Default::default() - }; - keymap_action_alternatives.push(action_with_input.into()); + let action_with_input = json_schema!({ + "type": "array", + "items": [matches_action_name, schema], + "minItems": 2, + "maxItems": 2 + }); + keymap_action_alternatives.push(action_with_input); } } } // Placing null first causes json-language-server to default assuming actions should be // null, so place it last. - keymap_action_alternatives.push( - SchemaObject { - instance_type: set(InstanceType::Null), - ..Default::default() - } - .into(), - ); + keymap_action_alternatives.push(json_schema!({ + "type": "null" + })); - let action_schema = SchemaObject { - subschemas: set(SubschemaValidation { - one_of: Some(keymap_action_alternatives), - ..Default::default() + // The `KeymapSection` schema will reference the `KeymapAction` schema by name, so setting + // the definition of `KeymapAction` results in the full action schema being used. + generator.definitions_mut().insert( + KeymapAction::schema_name().to_string(), + json!({ + "oneOf": keymap_action_alternatives }), - ..Default::default() - } - .into(); + ); - // The `KeymapSection` schema will reference the `KeymapAction` schema by name, so replacing - // the definition of `KeymapAction` results in the full action schema being used. - let mut root_schema = generator.into_root_schema_for::(); - root_schema - .definitions - .insert(KeymapAction::schema_name(), action_schema); - - // This and other json schemas can be viewed via `dev: open language server logs` -> - // `json-language-server` -> `Server Info`. - serde_json::to_value(root_schema).unwrap() + generator.root_schema_for::().to_value() } pub fn sections(&self) -> impl DoubleEndedIterator { diff --git a/crates/settings/src/settings_json.rs b/crates/settings/src/settings_json.rs index 1a045607e6645829c2e8a47af4c46cd0fd0b8fa7..e64d8efee0fce83a80732494578d083d2d328ada 100644 --- a/crates/settings/src/settings_json.rs +++ b/crates/settings/src/settings_json.rs @@ -1,11 +1,9 @@ -use std::{ops::Range, sync::LazyLock}; - use anyhow::Result; -use schemars::schema::{ - ArrayValidation, InstanceType, RootSchema, Schema, SchemaObject, SingleOrVec, -}; +use gpui::App; +use schemars::{JsonSchema, Schema}; use serde::{Serialize, de::DeserializeOwned}; use serde_json::Value; +use std::{ops::Range, sync::LazyLock}; use tree_sitter::{Query, StreamingIterator as _}; use util::RangeExt; @@ -14,70 +12,43 @@ pub struct SettingsJsonSchemaParams<'a> { pub font_names: &'a [String], } -impl SettingsJsonSchemaParams<'_> { - pub fn font_family_schema(&self) -> Schema { - let available_fonts: Vec<_> = self.font_names.iter().cloned().map(Value::String).collect(); - - SchemaObject { - instance_type: Some(InstanceType::String.into()), - enum_values: Some(available_fonts), - ..Default::default() - } - .into() - } - - pub fn font_fallback_schema(&self) -> Schema { - SchemaObject { - instance_type: Some(SingleOrVec::Vec(vec![ - InstanceType::Array, - InstanceType::Null, - ])), - array: Some(Box::new(ArrayValidation { - items: Some(schemars::schema::SingleOrVec::Single(Box::new( - self.font_family_schema(), - ))), - unique_items: Some(true), - ..Default::default() - })), - ..Default::default() - } - .into() - } +pub struct ParameterizedJsonSchema { + pub add_and_get_ref: + fn(&mut schemars::SchemaGenerator, &SettingsJsonSchemaParams, &App) -> schemars::Schema, } -type PropertyName<'a> = &'a str; -type ReferencePath<'a> = &'a str; - -/// Modifies the provided [`RootSchema`] by adding references to all of the specified properties. -/// -/// # Examples -/// -/// ``` -/// # let root_schema = RootSchema::default(); -/// add_references_to_properties(&mut root_schema, &[ -/// ("property_a", "#/definitions/DefinitionA"), -/// ("property_b", "#/definitions/DefinitionB"), -/// ]) -/// ``` -pub fn add_references_to_properties( - root_schema: &mut RootSchema, - properties_with_references: &[(PropertyName, ReferencePath)], -) { - for (property, definition) in properties_with_references { - let Some(schema) = root_schema.schema.object().properties.get_mut(*property) else { - log::warn!("property '{property}' not found in JSON schema"); - continue; - }; - - match schema { - Schema::Object(schema) => { - schema.reference = Some(definition.to_string()); - } - Schema::Bool(_) => { - // Boolean schemas can't have references. - } +inventory::collect!(ParameterizedJsonSchema); + +pub fn replace_subschema( + generator: &mut schemars::SchemaGenerator, + schema: schemars::Schema, +) -> schemars::Schema { + const DEFINITIONS_PATH: &str = "#/definitions/"; + // The key in definitions may not match T::schema_name() if multiple types have the same name. + // This is a workaround for there being no straightforward way to get the key used for a type - + // see https://github.com/GREsau/schemars/issues/449 + let ref_schema = generator.subschema_for::(); + if let Some(serde_json::Value::String(definition_pointer)) = ref_schema.get("$ref") { + if let Some(definition_name) = definition_pointer.strip_prefix(DEFINITIONS_PATH) { + generator + .definitions_mut() + .insert(definition_name.to_string(), schema.to_value()); + return ref_schema; + } else { + log::error!( + "bug: expected `$ref` field to start with {DEFINITIONS_PATH}, \ + got {definition_pointer}" + ); } + } else { + log::error!("bug: expected `$ref` field in result of `subschema_for`"); } + // fallback on just using the schema name, which could collide. + let schema_name = T::schema_name(); + generator + .definitions_mut() + .insert(schema_name.to_string(), schema.to_value()); + Schema::new_ref(format!("{DEFINITIONS_PATH}{schema_name}")) } pub fn update_value_in_json_text<'a>( diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index c4cf97bd6cea34f72c1e6d5c79e89c9f34504717..bdc2320508bdabd84f1385c0fbf8578ab4ac98ae 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -6,7 +6,7 @@ use futures::{FutureExt, StreamExt, channel::mpsc, future::LocalBoxFuture}; use gpui::{App, AsyncApp, BorrowAppContext, Global, Task, UpdateGlobal}; use paths::{EDITORCONFIG_NAME, local_settings_file_relative_path, task_file_name}; -use schemars::{JsonSchema, r#gen::SchemaGenerator, schema::RootSchema}; +use schemars::{JsonSchema, json_schema}; use serde::{Deserialize, Serialize, de::DeserializeOwned}; use serde_json::{Value, json}; use smallvec::SmallVec; @@ -24,8 +24,8 @@ use util::{ResultExt as _, merge_non_null_json_value_into}; pub type EditorconfigProperties = ec4rs::Properties; use crate::{ - SettingsJsonSchemaParams, VsCodeSettings, WorktreeId, parse_json_with_comments, - update_value_in_json_text, + ParameterizedJsonSchema, SettingsJsonSchemaParams, VsCodeSettings, WorktreeId, + parse_json_with_comments, update_value_in_json_text, }; /// A value that can be defined as a user setting. @@ -57,14 +57,6 @@ pub trait Settings: 'static + Send + Sync { where Self: Sized; - fn json_schema( - generator: &mut SchemaGenerator, - _: &SettingsJsonSchemaParams, - _: &App, - ) -> RootSchema { - generator.root_schema_for::() - } - fn missing_default() -> anyhow::Error { anyhow::anyhow!("missing default") } @@ -253,12 +245,7 @@ trait AnySettingValue: 'static + Send + Sync { fn all_local_values(&self) -> Vec<(WorktreeId, Arc, &dyn Any)>; fn set_global_value(&mut self, value: Box); fn set_local_value(&mut self, root_id: WorktreeId, path: Arc, value: Box); - fn json_schema( - &self, - generator: &mut SchemaGenerator, - _: &SettingsJsonSchemaParams, - cx: &App, - ) -> RootSchema; + fn json_schema(&self, generator: &mut schemars::SchemaGenerator) -> schemars::Schema; fn edits_for_update( &self, raw_settings: &serde_json::Value, @@ -276,11 +263,11 @@ impl SettingsStore { let (setting_file_updates_tx, mut setting_file_updates_rx) = mpsc::unbounded(); Self { setting_values: Default::default(), - raw_default_settings: serde_json::json!({}), + raw_default_settings: json!({}), raw_global_settings: None, - raw_user_settings: serde_json::json!({}), + raw_user_settings: json!({}), raw_server_settings: None, - raw_extension_settings: serde_json::json!({}), + raw_extension_settings: json!({}), raw_local_settings: Default::default(), raw_editorconfig_settings: BTreeMap::default(), tab_size_callback: Default::default(), @@ -877,108 +864,151 @@ impl SettingsStore { } pub fn json_schema(&self, schema_params: &SettingsJsonSchemaParams, cx: &App) -> Value { - use schemars::{ - r#gen::SchemaSettings, - schema::{Schema, SchemaObject}, - }; - - let settings = SchemaSettings::draft07().with(|settings| { - settings.option_add_null_type = true; + let mut generator = schemars::generate::SchemaSettings::draft07().into_generator(); + let mut combined_schema = json!({ + "type": "object", + "properties": {} }); - let mut generator = SchemaGenerator::new(settings); - let mut combined_schema = RootSchema::default(); + // Merge together settings schemas, similarly to json schema's "allOf". This merging is + // recursive, though at time of writing this recursive nature isn't used very much. An + // example of it is the schema for `jupyter` having contribution from both `EditorSettings` + // and `JupyterSettings`. + // + // This logic could be removed in favor of "allOf", but then there isn't the opportunity to + // validate and fully control the merge. for setting_value in self.setting_values.values() { - let setting_schema = setting_value.json_schema(&mut generator, schema_params, cx); - 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; + let mut setting_schema = setting_value.json_schema(&mut generator); + + if let Some(key) = setting_value.key() { + if let Some(properties) = combined_schema.get_mut("properties") { + if let Some(properties_obj) = properties.as_object_mut() { + if let Some(target) = properties_obj.get_mut(key) { + merge_schema(target, setting_schema.to_value()); + } else { + properties_obj.insert(key.to_string(), setting_schema.to_value()); + } + } } } else { - &mut combined_schema.schema - }; - - merge_schema(target_schema, setting_schema.schema); + setting_schema.remove("description"); + setting_schema.remove("additionalProperties"); + merge_schema(&mut combined_schema, setting_schema.to_value()); + } } - fn merge_schema(target: &mut SchemaObject, mut source: SchemaObject) { - let source_subschemas = source.subschemas(); - let target_subschemas = target.subschemas(); - if let Some(all_of) = source_subschemas.all_of.take() { - target_subschemas - .all_of - .get_or_insert(Vec::new()) - .extend(all_of); - } - if let Some(any_of) = source_subschemas.any_of.take() { - target_subschemas - .any_of - .get_or_insert(Vec::new()) - .extend(any_of); - } - if let Some(one_of) = source_subschemas.one_of.take() { - target_subschemas - .one_of - .get_or_insert(Vec::new()) - .extend(one_of); - } + fn merge_schema(target: &mut serde_json::Value, source: serde_json::Value) { + let (Some(target_obj), serde_json::Value::Object(source_obj)) = + (target.as_object_mut(), source) + else { + return; + }; - 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); + for (source_key, source_value) in source_obj { + match source_key.as_str() { + "properties" => { + let serde_json::Value::Object(source_properties) = source_value else { + log::error!( + "bug: expected object for `{}` json schema field, but got: {}", + source_key, + source_value + ); + continue; + }; + let target_properties = + target_obj.entry(source_key.clone()).or_insert(json!({})); + let Some(target_properties) = target_properties.as_object_mut() else { + log::error!( + "bug: expected object for `{}` json schema field, but got: {}", + source_key, + target_properties + ); + continue; + }; + for (key, value) in source_properties { + if let Some(existing) = target_properties.get_mut(&key) { + merge_schema(existing, value); + } else { + target_properties.insert(key, value); + } } - btree_map::Entry::Occupied(e) => { - if let (Schema::Object(target), Schema::Object(src)) = - (e.into_mut(), value) - { - merge_schema(target, src); + } + "allOf" | "anyOf" | "oneOf" => { + let serde_json::Value::Array(source_array) = source_value else { + log::error!( + "bug: expected array for `{}` json schema field, but got: {}", + source_key, + source_value, + ); + continue; + }; + let target_array = + target_obj.entry(source_key.clone()).or_insert(json!([])); + let Some(target_array) = target_array.as_array_mut() else { + log::error!( + "bug: expected array for `{}` json schema field, but got: {}", + source_key, + target_array, + ); + continue; + }; + target_array.extend(source_array); + } + "type" + | "$ref" + | "enum" + | "minimum" + | "maximum" + | "pattern" + | "description" + | "additionalProperties" => { + if let Some(old_value) = + target_obj.insert(source_key.clone(), source_value.clone()) + { + if old_value != source_value { + log::error!( + "bug: while merging JSON schemas, \ + mismatch `\"{}\": {}` (before was `{}`)", + source_key, + old_value, + source_value + ); } } } + _ => { + log::error!( + "bug: while merging settings JSON schemas, \ + encountered unexpected `\"{}\": {}`", + source_key, + source_value + ); + } } } + } - 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); - } - } + for parameterized_json_schema in inventory::iter::() { + (parameterized_json_schema.add_and_get_ref)(&mut generator, schema_params, cx); } const ZED_SETTINGS: &str = "ZedSettings"; - let RootSchema { - meta_schema, - schema: zed_settings_schema, - mut definitions, - } = combined_schema; - definitions.insert(ZED_SETTINGS.to_string(), zed_settings_schema.into()); - let zed_settings_ref = Schema::new_ref(format!("#/definitions/{ZED_SETTINGS}")); + let old_zed_settings_definition = generator + .definitions_mut() + .insert(ZED_SETTINGS.to_string(), combined_schema); + assert_eq!(old_zed_settings_definition, None); + let zed_settings_ref = schemars::Schema::new_ref(format!("#/definitions/{ZED_SETTINGS}")); + + let mut root_schema = if let Some(meta_schema) = generator.settings().meta_schema.as_ref() { + json_schema!({ "$schema": meta_schema.to_string() }) + } else { + json_schema!({}) + }; // settings file contents matches ZedSettings + overrides for each release stage - let mut root_schema = json!({ - "allOf": [ + root_schema.insert( + "allOf".to_string(), + json!([ zed_settings_ref, { "properties": { @@ -988,17 +1018,14 @@ impl SettingsStore { "preview": zed_settings_ref, } } - ], - "definitions": definitions, - }); - - if let Some(meta_schema) = meta_schema { - if let Some(root_schema_object) = root_schema.as_object_mut() { - root_schema_object.insert("$schema".to_string(), meta_schema.into()); - } - } + ]), + ); + root_schema.insert( + "definitions".to_string(), + generator.take_definitions(true).into(), + ); - root_schema + root_schema.to_value() } fn recompute_values( @@ -1311,13 +1338,8 @@ impl AnySettingValue for SettingValue { } } - fn json_schema( - &self, - generator: &mut SchemaGenerator, - params: &SettingsJsonSchemaParams, - cx: &App, - ) -> RootSchema { - T::json_schema(generator, params, cx) + fn json_schema(&self, generator: &mut schemars::SchemaGenerator) -> schemars::Schema { + T::FileContent::json_schema(generator) } fn edits_for_update( diff --git a/crates/settings_ui/src/appearance_settings_controls.rs b/crates/settings_ui/src/appearance_settings_controls.rs index fa7e31c5cdb56719f49fc3f190f53081f0c7221f..141ae131826f43bf39dd1a7fa435753f84801e4f 100644 --- a/crates/settings_ui/src/appearance_settings_controls.rs +++ b/crates/settings_ui/src/appearance_settings_controls.rs @@ -2,7 +2,9 @@ use std::sync::Arc; use gpui::{App, FontFeatures, FontWeight}; use settings::{EditableSettingControl, Settings}; -use theme::{FontFamilyCache, SystemAppearance, ThemeMode, ThemeRegistry, ThemeSettings}; +use theme::{ + FontFamilyCache, FontFamilyName, SystemAppearance, ThemeMode, ThemeRegistry, ThemeSettings, +}; use ui::{ CheckboxWithLabel, ContextMenu, DropdownMenu, NumericStepper, SettingsContainer, SettingsGroup, ToggleButton, prelude::*, @@ -189,7 +191,7 @@ impl EditableSettingControl for UiFontFamilyControl { value: Self::Value, _cx: &App, ) { - settings.ui_font_family = Some(value.to_string()); + settings.ui_font_family = Some(FontFamilyName(value.into())); } } diff --git a/crates/snippet_provider/src/format.rs b/crates/snippet_provider/src/format.rs index 84bae238b4e62f59a7259f9aefa16c5a0e5aad87..7aa02c7db53048588f3cb7da7aeeaa471da1dfc5 100644 --- a/crates/snippet_provider/src/format.rs +++ b/crates/snippet_provider/src/format.rs @@ -1,11 +1,8 @@ use collections::HashMap; -use schemars::{ - JsonSchema, - r#gen::SchemaSettings, - schema::{ObjectValidation, Schema, SchemaObject}, -}; +use schemars::{JsonSchema, json_schema}; use serde::Deserialize; use serde_json_lenient::Value; +use std::borrow::Cow; #[derive(Deserialize)] pub struct VsSnippetsFile { @@ -15,29 +12,25 @@ pub struct VsSnippetsFile { impl VsSnippetsFile { pub fn generate_json_schema() -> Value { - let schema = SchemaSettings::draft07() - .with(|settings| settings.option_add_null_type = false) + let schema = schemars::generate::SchemaSettings::draft07() .into_generator() - .into_root_schema_for::(); + .root_schema_for::(); serde_json_lenient::to_value(schema).unwrap() } } impl JsonSchema for VsSnippetsFile { - fn schema_name() -> String { + fn schema_name() -> Cow<'static, str> { "VsSnippetsFile".into() } - fn json_schema(r#gen: &mut schemars::r#gen::SchemaGenerator) -> Schema { - SchemaObject { - object: Some(Box::new(ObjectValidation { - additional_properties: Some(Box::new(r#gen.subschema_for::())), - ..Default::default() - })), - ..Default::default() - } - .into() + fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema { + let snippet_schema = generator.subschema_for::(); + json_schema!({ + "type": "object", + "additionalProperties": snippet_schema + }) } } diff --git a/crates/task/src/debug_format.rs b/crates/task/src/debug_format.rs index 0d9733ebfff10c995ddb5181815b1845d33c1636..81941e00742f78619ac1a63ec476e34436020e17 100644 --- a/crates/task/src/debug_format.rs +++ b/crates/task/src/debug_format.rs @@ -287,7 +287,8 @@ pub struct DebugTaskFile(pub Vec); impl DebugTaskFile { pub fn generate_json_schema(schemas: &AdapterSchemas) -> serde_json_lenient::Value { - let build_task_schema = schemars::schema_for!(BuildTaskDefinition); + let mut generator = schemars::generate::SchemaSettings::draft07().into_generator(); + let build_task_schema = generator.root_schema_for::(); let mut build_task_value = serde_json_lenient::to_value(&build_task_schema).unwrap_or_default(); diff --git a/crates/task/src/serde_helpers.rs b/crates/task/src/serde_helpers.rs index d7af919fbf2b49b04f93a870021e12a555bb89a1..a95214d8b0903fb154770e0a3c7f7789819683ee 100644 --- a/crates/task/src/serde_helpers.rs +++ b/crates/task/src/serde_helpers.rs @@ -1,33 +1,6 @@ -use schemars::{ - SchemaGenerator, - schema::{ArrayValidation, InstanceType, Schema, SchemaObject, SingleOrVec, StringValidation}, -}; use serde::de::{self, Deserializer, Visitor}; use std::fmt; -/// Generates a JSON schema for a non-empty string array. -pub fn non_empty_string_vec_json_schema(_: &mut SchemaGenerator) -> Schema { - Schema::Object(SchemaObject { - instance_type: Some(InstanceType::Array.into()), - array: Some(Box::new(ArrayValidation { - unique_items: Some(true), - items: Some(SingleOrVec::Single(Box::new(Schema::Object( - SchemaObject { - instance_type: Some(InstanceType::String.into()), - string: Some(Box::new(StringValidation { - min_length: Some(1), // Ensures string in the array is non-empty - ..Default::default() - })), - ..Default::default() - }, - )))), - ..Default::default() - })), - format: Some("vec-of-non-empty-strings".to_string()), // Use a custom format keyword - ..Default::default() - }) -} - /// Deserializes a non-empty string array. pub fn non_empty_string_vec<'de, D>(deserializer: D) -> Result, D::Error> where diff --git a/crates/task/src/task_template.rs b/crates/task/src/task_template.rs index 02310bb1b0208cc2d6f929b0898a6e5ffadd7586..4ff45cad9e745b8c340bd24ea604b273054f74a9 100644 --- a/crates/task/src/task_template.rs +++ b/crates/task/src/task_template.rs @@ -1,6 +1,6 @@ use anyhow::{Context as _, bail}; use collections::{HashMap, HashSet}; -use schemars::{JsonSchema, r#gen::SchemaSettings}; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use std::path::PathBuf; @@ -9,8 +9,7 @@ use util::{ResultExt, truncate_and_remove_front}; use crate::{ AttachRequest, ResolvedTask, RevealTarget, Shell, SpawnInTerminal, TaskContext, TaskId, - VariableName, ZED_VARIABLE_NAME_PREFIX, - serde_helpers::{non_empty_string_vec, non_empty_string_vec_json_schema}, + VariableName, ZED_VARIABLE_NAME_PREFIX, serde_helpers::non_empty_string_vec, }; /// A template definition of a Zed task to run. @@ -61,7 +60,7 @@ pub struct TaskTemplate { /// Represents the tags which this template attaches to. /// Adding this removes this task from other UI and gives you ability to run it by tag. #[serde(default, deserialize_with = "non_empty_string_vec")] - #[schemars(schema_with = "non_empty_string_vec_json_schema")] + #[schemars(length(min = 1))] pub tags: Vec, /// Which shell to use when spawning the task. #[serde(default)] @@ -116,10 +115,9 @@ pub struct TaskTemplates(pub Vec); impl TaskTemplates { /// Generates JSON schema of Tasks JSON template format. pub fn generate_json_schema() -> serde_json_lenient::Value { - let schema = SchemaSettings::draft07() - .with(|settings| settings.option_add_null_type = false) + let schema = schemars::generate::SchemaSettings::draft07() .into_generator() - .into_root_schema_for::(); + .root_schema_for::(); serde_json_lenient::to_value(schema).unwrap() } diff --git a/crates/terminal/src/terminal_settings.rs b/crates/terminal/src/terminal_settings.rs index bd93b7e0a67dea83cebd45012f8fefa2541bb5c8..d588d3680bbefbc522245a5ca709c2ef99de83f8 100644 --- a/crates/terminal/src/terminal_settings.rs +++ b/crates/terminal/src/terminal_settings.rs @@ -2,14 +2,14 @@ use alacritty_terminal::vte::ansi::{ CursorShape as AlacCursorShape, CursorStyle as AlacCursorStyle, }; use collections::HashMap; -use gpui::{ - AbsoluteLength, App, FontFallbacks, FontFeatures, FontWeight, Pixels, SharedString, px, -}; -use schemars::{JsonSchema, r#gen::SchemaGenerator, schema::RootSchema}; +use gpui::{AbsoluteLength, App, FontFallbacks, FontFeatures, FontWeight, Pixels, px}; +use schemars::JsonSchema; use serde_derive::{Deserialize, Serialize}; -use settings::{SettingsJsonSchemaParams, SettingsSources, add_references_to_properties}; + +use settings::SettingsSources; use std::path::PathBuf; use task::Shell; +use theme::FontFamilyName; #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] #[serde(rename_all = "snake_case")] @@ -29,7 +29,7 @@ pub struct TerminalSettings { pub shell: Shell, pub working_directory: WorkingDirectory, pub font_size: Option, - pub font_family: Option, + pub font_family: Option, pub font_fallbacks: Option, pub font_features: Option, pub font_weight: Option, @@ -147,13 +147,14 @@ pub struct TerminalSettingsContent { /// /// If this option is not included, /// the terminal will default to matching the buffer's font family. - pub font_family: Option, + pub font_family: Option, /// Sets the terminal's font fallbacks. /// /// If this option is not included, /// the terminal will default to matching the buffer's font fallbacks. - pub font_fallbacks: Option>, + #[schemars(extend("uniqueItems" = true))] + pub font_fallbacks: Option>, /// Sets the terminal's line height. /// @@ -234,33 +235,13 @@ impl settings::Settings for TerminalSettings { sources.json_merge() } - fn json_schema( - generator: &mut SchemaGenerator, - params: &SettingsJsonSchemaParams, - _: &App, - ) -> RootSchema { - let mut root_schema = generator.root_schema_for::(); - root_schema.definitions.extend([ - ("FontFamilies".into(), params.font_family_schema()), - ("FontFallbacks".into(), params.font_fallback_schema()), - ]); - - add_references_to_properties( - &mut root_schema, - &[ - ("font_family", "#/definitions/FontFamilies"), - ("font_fallbacks", "#/definitions/FontFallbacks"), - ], - ); - - root_schema - } - fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) { let name = |s| format!("terminal.integrated.{s}"); vscode.f32_setting(&name("fontSize"), &mut current.font_size); - vscode.string_setting(&name("fontFamily"), &mut current.font_family); + if let Some(font_family) = vscode.read_string(&name("fontFamily")) { + current.font_family = Some(FontFamilyName(font_family.into())); + } vscode.bool_setting(&name("copyOnSelection"), &mut current.copy_on_select); vscode.bool_setting("macOptionIsMeta", &mut current.option_as_meta); vscode.usize_setting("scrollback", &mut current.max_scroll_history_lines); diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index c0671048f6e54a1da6a54c1a0aea217706a82571..e85032b460a42f7d69cf8fec80699d8d32fa4aad 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -682,11 +682,10 @@ impl Element for TerminalElement { let terminal_settings = TerminalSettings::get_global(cx); - let font_family = terminal_settings - .font_family - .as_ref() - .unwrap_or(&settings.buffer_font.family) - .clone(); + let font_family = terminal_settings.font_family.as_ref().map_or_else( + || settings.buffer_font.family.clone(), + |font_family| font_family.0.clone().into(), + ); let font_fallbacks = terminal_settings .font_fallbacks diff --git a/crates/theme/Cargo.toml b/crates/theme/Cargo.toml index 43d720b5560e3da3e8ebe9a68914e46b19fcfd4f..998d31bb3c5473de324dbfd34a3b276c81d95aba 100644 --- a/crates/theme/Cargo.toml +++ b/crates/theme/Cargo.toml @@ -24,6 +24,7 @@ fs.workspace = true futures.workspace = true gpui.workspace = true indexmap.workspace = true +inventory.workspace = true log.workspace = true palette = { workspace = true, default-features = false, features = ["std"] } parking_lot.workspace = true diff --git a/crates/theme/src/schema.rs b/crates/theme/src/schema.rs index 01fdafd94df58f182074bc9c5ffeaa49fe36ab62..b2a13b54b662f106018667de9635a4c896e1993c 100644 --- a/crates/theme/src/schema.rs +++ b/crates/theme/src/schema.rs @@ -4,12 +4,11 @@ use anyhow::Result; use gpui::{FontStyle, FontWeight, HighlightStyle, Hsla, WindowBackgroundAppearance}; use indexmap::IndexMap; use palette::FromColor; -use schemars::JsonSchema; -use schemars::r#gen::SchemaGenerator; -use schemars::schema::{Schema, SchemaObject}; +use schemars::{JsonSchema, json_schema}; use serde::{Deserialize, Deserializer, Serialize}; use serde_json::Value; use serde_repr::{Deserialize_repr, Serialize_repr}; +use std::borrow::Cow; use crate::{StatusColorsRefinement, ThemeColorsRefinement}; @@ -1502,30 +1501,15 @@ pub enum FontWeightContent { } impl JsonSchema for FontWeightContent { - fn schema_name() -> String { - "FontWeightContent".to_owned() + fn schema_name() -> Cow<'static, str> { + "FontWeightContent".into() } - fn is_referenceable() -> bool { - false - } - - fn json_schema(_: &mut SchemaGenerator) -> Schema { - SchemaObject { - enum_values: Some(vec![ - 100.into(), - 200.into(), - 300.into(), - 400.into(), - 500.into(), - 600.into(), - 700.into(), - 800.into(), - 900.into(), - ]), - ..Default::default() - } - .into() + fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema { + json_schema!({ + "type": "integer", + "enum": [100, 200, 300, 400, 500, 600, 700, 800, 900] + }) } } diff --git a/crates/theme/src/settings.rs b/crates/theme/src/settings.rs index eedee05592e2c5256a1b3afef46f83183f20b344..42012e080ca82f7fef487916e144ec79a30f9d84 100644 --- a/crates/theme/src/settings.rs +++ b/crates/theme/src/settings.rs @@ -7,17 +7,12 @@ use anyhow::Result; use derive_more::{Deref, DerefMut}; use gpui::{ App, Context, Font, FontFallbacks, FontFeatures, FontStyle, FontWeight, Global, Pixels, - Subscription, Window, px, + SharedString, Subscription, Window, px, }; use refineable::Refineable; -use schemars::{ - JsonSchema, - r#gen::SchemaGenerator, - schema::{InstanceType, Schema, SchemaObject}, -}; +use schemars::{JsonSchema, json_schema}; use serde::{Deserialize, Serialize}; -use serde_json::Value; -use settings::{Settings, SettingsJsonSchemaParams, SettingsSources, add_references_to_properties}; +use settings::{ParameterizedJsonSchema, Settings, SettingsSources, replace_subschema}; use std::sync::Arc; use util::ResultExt as _; @@ -263,25 +258,19 @@ impl Global for AgentFontSize {} #[serde(untagged)] pub enum ThemeSelection { /// A static theme selection, represented by a single theme name. - Static(#[schemars(schema_with = "theme_name_ref")] String), + Static(ThemeName), /// A dynamic theme selection, which can change based the [ThemeMode]. Dynamic { /// The mode used to determine which theme to use. #[serde(default)] mode: ThemeMode, /// The theme to use for light mode. - #[schemars(schema_with = "theme_name_ref")] - light: String, + light: ThemeName, /// The theme to use for dark mode. - #[schemars(schema_with = "theme_name_ref")] - dark: String, + dark: ThemeName, }, } -fn theme_name_ref(_: &mut SchemaGenerator) -> Schema { - Schema::new_ref("#/definitions/ThemeName".into()) -} - // TODO: Rename ThemeMode -> ThemeAppearanceMode /// The mode use to select a theme. /// @@ -306,13 +295,13 @@ impl ThemeSelection { /// Returns the theme name for the selected [ThemeMode]. pub fn theme(&self, system_appearance: Appearance) -> &str { match self { - Self::Static(theme) => theme, + Self::Static(theme) => &theme.0, Self::Dynamic { mode, light, dark } => match mode { - ThemeMode::Light => light, - ThemeMode::Dark => dark, + ThemeMode::Light => &light.0, + ThemeMode::Dark => &dark.0, ThemeMode::System => match system_appearance { - Appearance::Light => light, - Appearance::Dark => dark, + Appearance::Light => &light.0, + Appearance::Dark => &dark.0, }, }, } @@ -327,27 +316,21 @@ impl ThemeSelection { } } -fn icon_theme_name_ref(_: &mut SchemaGenerator) -> Schema { - Schema::new_ref("#/definitions/IconThemeName".into()) -} - /// Represents the selection of an icon theme, which can be either static or dynamic. #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] #[serde(untagged)] pub enum IconThemeSelection { /// A static icon theme selection, represented by a single icon theme name. - Static(#[schemars(schema_with = "icon_theme_name_ref")] String), + Static(IconThemeName), /// A dynamic icon theme selection, which can change based on the [`ThemeMode`]. Dynamic { /// The mode used to determine which theme to use. #[serde(default)] mode: ThemeMode, /// The icon theme to use for light mode. - #[schemars(schema_with = "icon_theme_name_ref")] - light: String, + light: IconThemeName, /// The icon theme to use for dark mode. - #[schemars(schema_with = "icon_theme_name_ref")] - dark: String, + dark: IconThemeName, }, } @@ -355,13 +338,13 @@ impl IconThemeSelection { /// Returns the icon theme name based on the given [`Appearance`]. pub fn icon_theme(&self, system_appearance: Appearance) -> &str { match self { - Self::Static(theme) => theme, + Self::Static(theme) => &theme.0, Self::Dynamic { mode, light, dark } => match mode { - ThemeMode::Light => light, - ThemeMode::Dark => dark, + ThemeMode::Light => &light.0, + ThemeMode::Dark => &dark.0, ThemeMode::System => match system_appearance { - Appearance::Light => light, - Appearance::Dark => dark, + Appearance::Light => &light.0, + Appearance::Dark => &dark.0, }, }, } @@ -384,11 +367,12 @@ pub struct ThemeSettingsContent { pub ui_font_size: Option, /// The name of a font to use for rendering in the UI. #[serde(default)] - pub ui_font_family: Option, + pub ui_font_family: Option, /// The font fallbacks to use for rendering in the UI. #[serde(default)] #[schemars(default = "default_font_fallbacks")] - pub ui_font_fallbacks: Option>, + #[schemars(extend("uniqueItems" = true))] + pub ui_font_fallbacks: Option>, /// The OpenType features to enable for text in the UI. #[serde(default)] #[schemars(default = "default_font_features")] @@ -398,11 +382,11 @@ pub struct ThemeSettingsContent { pub ui_font_weight: Option, /// The name of a font to use for rendering in text buffers. #[serde(default)] - pub buffer_font_family: Option, + pub buffer_font_family: Option, /// The font fallbacks to use for rendering in text buffers. #[serde(default)] - #[schemars(default = "default_font_fallbacks")] - pub buffer_font_fallbacks: Option>, + #[schemars(extend("uniqueItems" = true))] + pub buffer_font_fallbacks: Option>, /// The default font size for rendering in text buffers. #[serde(default)] pub buffer_font_size: Option, @@ -467,9 +451,9 @@ impl ThemeSettingsContent { }, }; - *theme_to_update = theme_name.to_string(); + *theme_to_update = ThemeName(theme_name.into()); } else { - self.theme = Some(ThemeSelection::Static(theme_name.to_string())); + self.theme = Some(ThemeSelection::Static(ThemeName(theme_name.into()))); } } @@ -488,9 +472,11 @@ impl ThemeSettingsContent { }, }; - *icon_theme_to_update = icon_theme_name.to_string(); + *icon_theme_to_update = IconThemeName(icon_theme_name.into()); } else { - self.icon_theme = Some(IconThemeSelection::Static(icon_theme_name.to_string())); + self.icon_theme = Some(IconThemeSelection::Static(IconThemeName( + icon_theme_name.into(), + ))); } } @@ -516,8 +502,8 @@ impl ThemeSettingsContent { } else { self.theme = Some(ThemeSelection::Dynamic { mode, - light: ThemeSettings::DEFAULT_LIGHT_THEME.into(), - dark: ThemeSettings::DEFAULT_DARK_THEME.into(), + light: ThemeName(ThemeSettings::DEFAULT_LIGHT_THEME.into()), + dark: ThemeName(ThemeSettings::DEFAULT_DARK_THEME.into()), }); } @@ -539,7 +525,9 @@ impl ThemeSettingsContent { } => *mode_to_update = mode, } } else { - self.icon_theme = Some(IconThemeSelection::Static(DEFAULT_ICON_THEME_NAME.into())); + self.icon_theme = Some(IconThemeSelection::Static(IconThemeName( + DEFAULT_ICON_THEME_NAME.into(), + ))); } } } @@ -815,26 +803,39 @@ impl settings::Settings for ThemeSettings { let themes = ThemeRegistry::default_global(cx); let system_appearance = SystemAppearance::default_global(cx); + fn font_fallbacks_from_settings( + fallbacks: Option>, + ) -> Option { + fallbacks.map(|fallbacks| { + FontFallbacks::from_fonts( + fallbacks + .into_iter() + .map(|font_family| font_family.0.to_string()) + .collect(), + ) + }) + } + let defaults = sources.default; let mut this = Self { ui_font_size: defaults.ui_font_size.unwrap().into(), ui_font: Font { - family: defaults.ui_font_family.as_ref().unwrap().clone().into(), + family: defaults.ui_font_family.as_ref().unwrap().0.clone().into(), features: defaults.ui_font_features.clone().unwrap(), - fallbacks: defaults - .ui_font_fallbacks - .as_ref() - .map(|fallbacks| FontFallbacks::from_fonts(fallbacks.clone())), + fallbacks: font_fallbacks_from_settings(defaults.ui_font_fallbacks.clone()), weight: defaults.ui_font_weight.map(FontWeight).unwrap(), style: Default::default(), }, buffer_font: Font { - family: defaults.buffer_font_family.as_ref().unwrap().clone().into(), - features: defaults.buffer_font_features.clone().unwrap(), - fallbacks: defaults - .buffer_font_fallbacks + family: defaults + .buffer_font_family .as_ref() - .map(|fallbacks| FontFallbacks::from_fonts(fallbacks.clone())), + .unwrap() + .0 + .clone() + .into(), + features: defaults.buffer_font_features.clone().unwrap(), + fallbacks: font_fallbacks_from_settings(defaults.buffer_font_fallbacks.clone()), weight: defaults.buffer_font_weight.map(FontWeight).unwrap(), style: FontStyle::default(), }, @@ -872,26 +873,26 @@ impl settings::Settings for ThemeSettings { } if let Some(value) = value.buffer_font_family.clone() { - this.buffer_font.family = value.into(); + this.buffer_font.family = value.0.into(); } if let Some(value) = value.buffer_font_features.clone() { this.buffer_font.features = value; } if let Some(value) = value.buffer_font_fallbacks.clone() { - this.buffer_font.fallbacks = Some(FontFallbacks::from_fonts(value)); + this.buffer_font.fallbacks = font_fallbacks_from_settings(Some(value)); } if let Some(value) = value.buffer_font_weight { this.buffer_font.weight = clamp_font_weight(value); } if let Some(value) = value.ui_font_family.clone() { - this.ui_font.family = value.into(); + this.ui_font.family = value.0.into(); } if let Some(value) = value.ui_font_features.clone() { this.ui_font.features = value; } if let Some(value) = value.ui_font_fallbacks.clone() { - this.ui_font.fallbacks = Some(FontFallbacks::from_fonts(value)); + this.ui_font.fallbacks = font_fallbacks_from_settings(Some(value)); } if let Some(value) = value.ui_font_weight { this.ui_font.weight = clamp_font_weight(value); @@ -959,64 +960,73 @@ impl settings::Settings for ThemeSettings { Ok(this) } - fn json_schema( - generator: &mut SchemaGenerator, - params: &SettingsJsonSchemaParams, - cx: &App, - ) -> schemars::schema::RootSchema { - let mut root_schema = generator.root_schema_for::(); - let theme_names = ThemeRegistry::global(cx) - .list_names() - .into_iter() - .map(|theme_name| Value::String(theme_name.to_string())) - .collect(); - - let theme_name_schema = SchemaObject { - instance_type: Some(InstanceType::String.into()), - enum_values: Some(theme_names), - ..Default::default() - }; - - let icon_theme_names = ThemeRegistry::global(cx) - .list_icon_themes() - .into_iter() - .map(|icon_theme| Value::String(icon_theme.name.to_string())) - .collect(); - - let icon_theme_name_schema = SchemaObject { - instance_type: Some(InstanceType::String.into()), - enum_values: Some(icon_theme_names), - ..Default::default() - }; - - root_schema.definitions.extend([ - ("ThemeName".into(), theme_name_schema.into()), - ("IconThemeName".into(), icon_theme_name_schema.into()), - ("FontFamilies".into(), params.font_family_schema()), - ("FontFallbacks".into(), params.font_fallback_schema()), - ]); - - add_references_to_properties( - &mut root_schema, - &[ - ("buffer_font_family", "#/definitions/FontFamilies"), - ("buffer_font_fallbacks", "#/definitions/FontFallbacks"), - ("ui_font_family", "#/definitions/FontFamilies"), - ("ui_font_fallbacks", "#/definitions/FontFallbacks"), - ], - ); - - root_schema - } - fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) { vscode.f32_setting("editor.fontWeight", &mut current.buffer_font_weight); vscode.f32_setting("editor.fontSize", &mut current.buffer_font_size); - vscode.string_setting("editor.font", &mut current.buffer_font_family); + if let Some(font) = vscode.read_string("editor.font") { + current.buffer_font_family = Some(FontFamilyName(font.into())); + } // TODO: possibly map editor.fontLigatures to buffer_font_features? } } +/// Newtype for a theme name. Its `ParameterizedJsonSchema` lists the theme names known at runtime. +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(transparent)] +pub struct ThemeName(pub Arc); + +inventory::submit! { + ParameterizedJsonSchema { + add_and_get_ref: |generator, _params, cx| { + let schema = json_schema!({ + "type": "string", + "enum": ThemeRegistry::global(cx).list_names(), + }); + replace_subschema::(generator, schema) + } + } +} + +/// Newtype for a icon theme name. Its `ParameterizedJsonSchema` lists the icon theme names known at +/// runtime. +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(transparent)] +pub struct IconThemeName(pub Arc); + +inventory::submit! { + ParameterizedJsonSchema { + add_and_get_ref: |generator, _params, cx| { + let schema = json_schema!({ + "type": "string", + "enum": ThemeRegistry::global(cx) + .list_icon_themes() + .into_iter() + .map(|icon_theme| icon_theme.name) + .collect::>(), + }); + replace_subschema::(generator, schema) + } + } +} + +/// Newtype for font family name. Its `ParameterizedJsonSchema` lists the font families known at +/// runtime. +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(transparent)] +pub struct FontFamilyName(pub Arc); + +inventory::submit! { + ParameterizedJsonSchema { + add_and_get_ref: |generator, params, _cx| { + let schema = json_schema!({ + "type": "string", + "enum": params.font_names, + }); + replace_subschema::(generator, schema) + } + } +} + fn merge(target: &mut T, value: Option) { if let Some(value) = value { *target = value; diff --git a/crates/vim/src/normal/paste.rs b/crates/vim/src/normal/paste.rs index 67ca6314af4cbe8068342e8eee8a79de37d8c4c9..86a5392b87bfe3ede9bc518591c95df00d87f9a1 100644 --- a/crates/vim/src/normal/paste.rs +++ b/crates/vim/src/normal/paste.rs @@ -711,7 +711,7 @@ mod test { ); cx.update_global(|store: &mut SettingsStore, cx| { store.update_user_settings::(cx, |settings| { - settings.languages.insert( + settings.languages.0.insert( LanguageName::new("Rust"), LanguageSettingsContent { auto_indent_on_paste: Some(false), diff --git a/tooling/workspace-hack/Cargo.toml b/tooling/workspace-hack/Cargo.toml index 347d59e293130e03b1539a028ea8dc2fa82c061d..84ed00153647dd780d818844dcd6d3cb628859d1 100644 --- a/tooling/workspace-hack/Cargo.toml +++ b/tooling/workspace-hack/Cargo.toml @@ -111,7 +111,7 @@ sea-orm = { version = "1", features = ["runtime-tokio-rustls", "sqlx-postgres", sea-query-binder = { version = "0.7", default-features = false, features = ["postgres-array", "sqlx-postgres", "sqlx-sqlite", "with-bigdecimal", "with-chrono", "with-json", "with-rust_decimal", "with-time", "with-uuid"] } semver = { version = "1", features = ["serde"] } serde = { version = "1", features = ["alloc", "derive", "rc"] } -serde_json = { version = "1", features = ["preserve_order", "raw_value", "unbounded_depth"] } +serde_json = { version = "1", features = ["alloc", "preserve_order", "raw_value", "unbounded_depth"] } sha1 = { version = "0.10", features = ["compress"] } simd-adler32 = { version = "0.3" } smallvec = { version = "1", default-features = false, features = ["const_new", "serde", "union", "write"] } @@ -244,7 +244,7 @@ sea-query-binder = { version = "0.7", default-features = false, features = ["pos semver = { version = "1", features = ["serde"] } serde = { version = "1", features = ["alloc", "derive", "rc"] } serde_derive = { version = "1", features = ["deserialize_in_place"] } -serde_json = { version = "1", features = ["preserve_order", "raw_value", "unbounded_depth"] } +serde_json = { version = "1", features = ["alloc", "preserve_order", "raw_value", "unbounded_depth"] } sha1 = { version = "0.10", features = ["compress"] } simd-adler32 = { version = "0.3" } smallvec = { version = "1", default-features = false, features = ["const_new", "serde", "union", "write"] }