From a350438a21c80e1199ceae78f4e9f7e6f7403330 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 5 Dec 2025 22:26:42 +0200 Subject: [PATCH] Specify a schema to use when dealing with JSONC files (#44250) Follow-up of https://github.com/zed-industries/zed/pull/43854 Closes https://github.com/zed-industries/zed/issues/40970 Seems that json language server does not distinguish between JSONC and JSON files in runtime, but there is a static schema, which accepts globs in its `fileMatch` fields. Use all glob overrides and file suffixes for JSONC inside those match fields, and provide a grammar for such matches, which accepts trailing commas. Release Notes: - Improved JSONC trailing comma handling --- .../src/json_schema_store.rs | 54 +++++++++++++++++-- crates/language/src/language_registry.rs | 4 +- crates/language/src/language_settings.rs | 9 ++-- crates/languages/src/json.rs | 11 ++-- crates/languages/src/lib.rs | 2 +- 5 files changed, 65 insertions(+), 15 deletions(-) diff --git a/crates/json_schema_store/src/json_schema_store.rs b/crates/json_schema_store/src/json_schema_store.rs index b44efb8b1b135850ab78460a428b5088e5fa0928..18041545ccd404eef0035b9b50ff8244d212fa0b 100644 --- a/crates/json_schema_store/src/json_schema_store.rs +++ b/crates/json_schema_store/src/json_schema_store.rs @@ -3,8 +3,9 @@ use std::{str::FromStr, sync::Arc}; use anyhow::{Context as _, Result}; use gpui::{App, AsyncApp, BorrowAppContext as _, Entity, WeakEntity}; -use language::LanguageRegistry; +use language::{LanguageRegistry, language_settings::all_language_settings}; use project::LspStore; +use util::schemars::{AllowTrailingCommas, DefaultDenyUnknownFields}; // Origin: https://github.com/SchemaStore/schemastore const TSCONFIG_SCHEMA: &str = include_str!("schemas/tsconfig.json"); @@ -159,14 +160,35 @@ pub fn resolve_schema_request_inner( } } "snippets" => snippet_provider::format::VsSnippetsFile::generate_json_schema(), + "jsonc" => jsonc_schema(), _ => { - anyhow::bail!("Unrecognized builtin JSON schema: {}", schema_name); + anyhow::bail!("Unrecognized builtin JSON schema: {schema_name}"); } }; Ok(schema) } -pub fn all_schema_file_associations(cx: &mut App) -> serde_json::Value { +const JSONC_LANGUAGE_NAME: &str = "JSONC"; + +pub fn all_schema_file_associations( + languages: &Arc, + cx: &mut App, +) -> serde_json::Value { + let extension_globs = languages + .available_language_for_name(JSONC_LANGUAGE_NAME) + .map(|language| language.matcher().path_suffixes.clone()) + .into_iter() + .flatten() + // Path suffixes can be entire file names or just their extensions. + .flat_map(|path_suffix| [format!("*.{path_suffix}"), path_suffix]); + let override_globs = all_language_settings(None, cx) + .file_types + .get(JSONC_LANGUAGE_NAME) + .into_iter() + .flat_map(|(_, glob_strings)| glob_strings) + .cloned(); + let jsonc_globs = extension_globs.chain(override_globs).collect::>(); + let mut file_associations = serde_json::json!([ { "fileMatch": [ @@ -211,6 +233,10 @@ pub fn all_schema_file_associations(cx: &mut App) -> serde_json::Value { "fileMatch": ["package.json"], "url": "zed://schemas/package_json" }, + { + "fileMatch": &jsonc_globs, + "url": "zed://schemas/jsonc" + }, ]); #[cfg(debug_assertions)] @@ -233,7 +259,7 @@ pub fn all_schema_file_associations(cx: &mut App) -> serde_json::Value { let file_name = normalized_action_name_to_file_name(normalized_name.clone()); serde_json::json!({ "fileMatch": [file_name], - "url": format!("zed://schemas/action/{}", normalized_name) + "url": format!("zed://schemas/action/{normalized_name}") }) }), ); @@ -249,6 +275,26 @@ fn package_json_schema() -> serde_json::Value { serde_json::Value::from_str(PACKAGE_JSON_SCHEMA).unwrap() } +fn jsonc_schema() -> serde_json::Value { + let generator = schemars::generate::SchemaSettings::draft2019_09() + .with_transform(DefaultDenyUnknownFields) + .with_transform(AllowTrailingCommas) + .into_generator(); + let meta_schema = generator + .settings() + .meta_schema + .as_ref() + .expect("meta_schema should be present in schemars settings") + .to_string(); + let defs = generator.definitions(); + let schema = schemars::json_schema!({ + "$schema": meta_schema, + "allowTrailingCommas": true, + "$defs": defs, + }); + serde_json::to_value(schema).unwrap() +} + fn generate_inspector_style_schema() -> serde_json::Value { let schema = schemars::generate::SchemaSettings::draft2019_09() .with_transform(util::schemars::DefaultDenyUnknownFields) diff --git a/crates/language/src/language_registry.rs b/crates/language/src/language_registry.rs index a0b04efd1b1366a101812d8656965637c13769a5..af2b66316d133370a3c27f59da645cfff8d8aa66 100644 --- a/crates/language/src/language_registry.rs +++ b/crates/language/src/language_registry.rs @@ -745,7 +745,7 @@ impl LanguageRegistry { self: &Arc, path: &Path, content: Option<&Rope>, - user_file_types: Option<&FxHashMap, GlobSet>>, + user_file_types: Option<&FxHashMap, (GlobSet, Vec)>>, ) -> Option { let filename = path.file_name().and_then(|filename| filename.to_str()); // `Path.extension()` returns None for files with a leading '.' @@ -788,7 +788,7 @@ impl LanguageRegistry { let path_matches_custom_suffix = || { user_file_types .and_then(|types| types.get(language_name.as_ref())) - .map_or(None, |custom_suffixes| { + .map_or(None, |(custom_suffixes, _)| { path_suffixes .iter() .find(|(_, candidate)| custom_suffixes.is_match_candidate(candidate)) diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 068f8e1aa39ca3422fda8eb5706c00de6f2f62ce..fccaa545b79c1f24589889df8fcd163fbc5b6c7d 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -51,7 +51,7 @@ pub struct AllLanguageSettings { pub edit_predictions: EditPredictionSettings, pub defaults: LanguageSettings, languages: HashMap, - pub(crate) file_types: FxHashMap, GlobSet>, + pub file_types: FxHashMap, (GlobSet, Vec)>, } #[derive(Debug, Clone, PartialEq)] @@ -656,7 +656,7 @@ impl settings::Settings for AllLanguageSettings { let enabled_in_text_threads = edit_predictions.enabled_in_text_threads.unwrap(); - let mut file_types: FxHashMap, GlobSet> = FxHashMap::default(); + let mut file_types: FxHashMap, (GlobSet, Vec)> = FxHashMap::default(); for (language, patterns) in all_languages.file_types.iter().flatten() { let mut builder = GlobSetBuilder::new(); @@ -665,7 +665,10 @@ impl settings::Settings for AllLanguageSettings { builder.add(Glob::new(pattern).unwrap()); } - file_types.insert(language.clone(), builder.build().unwrap()); + file_types.insert( + language.clone(), + (builder.build().unwrap(), patterns.0.clone()), + ); } Self { diff --git a/crates/languages/src/json.rs b/crates/languages/src/json.rs index f695512c1a9ed55289a79bbbd632114a24b82d8d..00bb265967f83ee9a95c034cc0bbcbf63e952647 100644 --- a/crates/languages/src/json.rs +++ b/crates/languages/src/json.rs @@ -7,8 +7,8 @@ use futures::StreamExt; use gpui::{App, AsyncApp, Task}; use http_client::github::{GitHubLspBinaryVersion, latest_github_release}; use language::{ - ContextProvider, LanguageName, LocalFile as _, LspAdapter, LspAdapterDelegate, LspInstaller, - Toolchain, + ContextProvider, LanguageName, LanguageRegistry, LocalFile as _, LspAdapter, + LspAdapterDelegate, LspInstaller, Toolchain, }; use lsp::{LanguageServerBinary, LanguageServerName, Uri}; use node_runtime::{NodeRuntime, VersionStrategy}; @@ -129,14 +129,15 @@ fn server_binary_arguments(server_path: &Path) -> Vec { } pub struct JsonLspAdapter { + languages: Arc, node: NodeRuntime, } impl JsonLspAdapter { const PACKAGE_NAME: &str = "vscode-langservers-extracted"; - pub fn new(node: NodeRuntime) -> Self { - Self { node } + pub fn new(languages: Arc, node: NodeRuntime) -> Self { + Self { languages, node } } } @@ -255,7 +256,7 @@ impl LspAdapter for JsonLspAdapter { cx: &mut AsyncApp, ) -> Result { let mut config = cx.update(|cx| { - let schemas = json_schema_store::all_schema_file_associations(cx); + let schemas = json_schema_store::all_schema_file_associations(&self.languages, cx); // This can be viewed via `dev: open language server logs` -> `json-language-server` -> // `Server Info` diff --git a/crates/languages/src/lib.rs b/crates/languages/src/lib.rs index 9df14fb162e2ed722f5ed7527e179f3aec9b0af6..8ce234a864085a324adeb93a1005a0ed60b1c2b1 100644 --- a/crates/languages/src/lib.rs +++ b/crates/languages/src/lib.rs @@ -89,7 +89,7 @@ pub fn init(languages: Arc, fs: Arc, node: NodeRuntime let go_context_provider = Arc::new(go::GoContextProvider); let go_lsp_adapter = Arc::new(go::GoLspAdapter); let json_context_provider = Arc::new(JsonTaskProvider); - let json_lsp_adapter = Arc::new(json::JsonLspAdapter::new(node.clone())); + let json_lsp_adapter = Arc::new(json::JsonLspAdapter::new(languages.clone(), node.clone())); let node_version_lsp_adapter = Arc::new(json::NodeVersionAdapter); let py_lsp_adapter = Arc::new(python::PyLspAdapter::new()); let ty_lsp_adapter = Arc::new(python::TyLspAdapter::new(fs.clone()));