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
This commit is contained in:
Kirill Bulatov
2025-12-05 20:26:42 +00:00
committed by GitHub
parent bd6ca841ad
commit a350438a21
5 changed files with 65 additions and 15 deletions
@@ -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<LanguageRegistry>,
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::<Vec<_>>();
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)
+2 -2
View File
@@ -745,7 +745,7 @@ impl LanguageRegistry {
self: &Arc<Self>,
path: &Path,
content: Option<&Rope>,
user_file_types: Option<&FxHashMap<Arc<str>, GlobSet>>,
user_file_types: Option<&FxHashMap<Arc<str>, (GlobSet, Vec<String>)>>,
) -> Option<AvailableLanguage> {
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))
+6 -3
View File
@@ -51,7 +51,7 @@ pub struct AllLanguageSettings {
pub edit_predictions: EditPredictionSettings,
pub defaults: LanguageSettings,
languages: HashMap<LanguageName, LanguageSettings>,
pub(crate) file_types: FxHashMap<Arc<str>, GlobSet>,
pub file_types: FxHashMap<Arc<str>, (GlobSet, Vec<String>)>,
}
#[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<Arc<str>, GlobSet> = FxHashMap::default();
let mut file_types: FxHashMap<Arc<str>, (GlobSet, Vec<String>)> = 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 {
+6 -5
View File
@@ -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<OsString> {
}
pub struct JsonLspAdapter {
languages: Arc<LanguageRegistry>,
node: NodeRuntime,
}
impl JsonLspAdapter {
const PACKAGE_NAME: &str = "vscode-langservers-extracted";
pub fn new(node: NodeRuntime) -> Self {
Self { node }
pub fn new(languages: Arc<LanguageRegistry>, node: NodeRuntime) -> Self {
Self { languages, node }
}
}
@@ -255,7 +256,7 @@ impl LspAdapter for JsonLspAdapter {
cx: &mut AsyncApp,
) -> Result<Value> {
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`
+1 -1
View File
@@ -89,7 +89,7 @@ pub fn init(languages: Arc<LanguageRegistry>, fs: Arc<dyn Fs>, 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()));