JSON Schema URIs (#38916)

Closes #ISSUE

Improves the efficiency of our interactions with the Zed language
server. Previously, on startup and after every workspace configuration
changed notification, we would send >1MB of JSON Schemas to the JSON
LSP. The only reason this had to happen was due to the case where an
extension was installed that would result in a change to the JSON schema
for settings (i.e. added language, theme, etc).

This PR changes the behavior to use the URI LSP extensions of
`vscode-json-language-server` in order to send the server URI's that it
can then use to fetch the schemas as needed (i.e. the settings schema is
only generated and sent when `settings.json` is opened. This brings the
JSON we send to on startup and after every workspace configuration
changed notification down to a couple of KB.

Additionally, using another LSP extension request we can notify the
server when a schema has changed using the URI as a key, so we no longer
have to send a workspace configuration changed notification, and the
schema contents will only be re-requested and regenerated if the schema
is in use.

Release Notes:

- Improved the efficiency of communication with the builtin JSON LSP.
JSON Schemas are no longer sent to the JSON language server in their
full form. If you wish to view a builtin JSON schema in the language
server info tab of the language server logs (`dev: open language server
logs`), you must now use the `editor: open url` action with your cursor
over the URL that is sent to the server.
- Made it so that Zed urls (`zed://...`) are resolved locally when
opened within the editor instead of being resolved through the OS. Users
who could not previously open `zed://*` URLs in the editor can now do so
by pasting the link into a buffer and using the `editor: open url`
action (please open an issue if this is the case for you!).

---------

Co-authored-by: Michael <michael@zed.dev>
This commit is contained in:
Ben Kunkle
2025-09-26 11:41:26 -04:00
committed by GitHub
co-authored by Michael
parent 30b49cfbf5
commit 4aac5642c1
28 changed files with 743 additions and 489 deletions
@@ -0,0 +1,299 @@
//! # json_schema_store
use std::{str::FromStr, sync::Arc};
use anyhow::{Context as _, Result};
use gpui::{App, AsyncApp, BorrowAppContext as _, Entity, SharedString, WeakEntity};
use language::LanguageRegistry;
use project::LspStore;
// Origin: https://github.com/SchemaStore/schemastore
const TSCONFIG_SCHEMA: &str = include_str!("schemas/tsconfig.json");
const PACKAGE_JSON_SCHEMA: &str = include_str!("schemas/package.json");
pub fn init(cx: &mut App) {
cx.set_global(SchemaStore::default());
project::lsp_store::json_language_server_ext::register_schema_handler(
handle_schema_request,
cx,
);
cx.observe_new(|_, _, cx| {
let lsp_store = cx.weak_entity();
cx.global_mut::<SchemaStore>().lsp_stores.push(lsp_store);
})
.detach();
if let Some(extension_events) = extension::ExtensionEvents::try_global(cx) {
cx.subscribe(&extension_events, |_, evt, cx| {
match evt {
extension::Event::ExtensionInstalled(_)
| extension::Event::ExtensionUninstalled(_)
| extension::Event::ConfigureExtensionRequested(_) => return,
extension::Event::ExtensionsInstalledChanged => {}
}
cx.update_global::<SchemaStore, _>(|schema_store, cx| {
schema_store.notify_schema_changed("zed://schemas/settings", cx);
});
})
.detach();
}
cx.observe_global::<dap::DapRegistry>(|cx| {
cx.update_global::<SchemaStore, _>(|schema_store, cx| {
schema_store.notify_schema_changed("zed://schemas/debug_tasks", cx);
});
})
.detach();
}
#[derive(Default)]
pub struct SchemaStore {
lsp_stores: Vec<WeakEntity<LspStore>>,
}
impl gpui::Global for SchemaStore {}
impl SchemaStore {
fn notify_schema_changed(&mut self, uri: &str, cx: &mut App) {
let uri = uri.to_string();
self.lsp_stores.retain(|lsp_store| {
let Some(lsp_store) = lsp_store.upgrade() else {
return false;
};
project::lsp_store::json_language_server_ext::notify_schema_changed(
lsp_store, &uri, cx,
);
true
})
}
}
fn handle_schema_request(
lsp_store: Entity<LspStore>,
uri: String,
cx: &mut AsyncApp,
) -> Result<String> {
let languages = lsp_store.read_with(cx, |lsp_store, _| lsp_store.languages.clone())?;
let schema = resolve_schema_request(&languages, uri, cx)?;
serde_json::to_string(&schema).context("Failed to serialize schema")
}
pub fn resolve_schema_request(
languages: &Arc<LanguageRegistry>,
uri: String,
cx: &mut AsyncApp,
) -> Result<serde_json::Value> {
let path = uri.strip_prefix("zed://schemas/").context("Invalid URI")?;
resolve_schema_request_inner(languages, path, cx)
}
pub fn resolve_schema_request_inner(
languages: &Arc<LanguageRegistry>,
path: &str,
cx: &mut AsyncApp,
) -> Result<serde_json::Value> {
let (schema_name, rest) = path.split_once('/').unzip();
let schema_name = schema_name.unwrap_or(path);
let schema = match schema_name {
"settings" => cx.update(|cx| {
let font_names = &cx.text_system().all_font_names();
let language_names = &languages
.language_names()
.into_iter()
.map(|name| name.to_string())
.collect::<Vec<_>>();
let icon_theme_names = &theme::ThemeRegistry::global(cx)
.list_icon_themes()
.into_iter()
.map(|icon_theme| icon_theme.name)
.collect::<Vec<SharedString>>();
let theme_names = &theme::ThemeRegistry::global(cx).list_names();
cx.global::<settings::SettingsStore>().json_schema(
&settings::SettingsJsonSchemaParams {
language_names,
font_names,
theme_names,
icon_theme_names,
},
)
})?,
"keymap" => cx.update(settings::KeymapFile::generate_json_schema_for_registered_actions)?,
"action" => {
let normalized_action_name = rest.context("No Action name provided")?;
let action_name = denormalize_action_name(normalized_action_name);
let mut generator = settings::KeymapFile::action_schema_generator();
let schema = cx
// PERF: cx.action_schema_by_name(action_name, &mut generator)
.update(|cx| cx.action_schemas(&mut generator))?
.into_iter()
.find_map(|(name, schema)| (name == action_name).then_some(schema))
.flatten();
root_schema_from_action_schema(schema, &mut generator).to_value()
}
"tasks" => task::TaskTemplates::generate_json_schema(),
"debug_tasks" => {
let adapter_schemas = cx.read_global::<dap::DapRegistry, _>(|dap_registry, _| {
dap_registry.adapters_schema()
})?;
task::DebugTaskFile::generate_json_schema(&adapter_schemas)
}
"package_json" => package_json_schema(),
"tsconfig" => tsconfig_schema(),
"zed_inspector_style" => {
if cfg!(debug_assertions) {
generate_inspector_style_schema()
} else {
schemars::json_schema!(true).to_value()
}
}
"snippets" => snippet_provider::format::VsSnippetsFile::generate_json_schema(),
_ => {
anyhow::bail!("Unrecognized builtin JSON schema: {}", schema_name);
}
};
Ok(schema)
}
pub fn all_schema_file_associations(cx: &mut App) -> serde_json::Value {
let mut file_associations = serde_json::json!([
{
"fileMatch": [
schema_file_match(paths::settings_file()),
paths::local_settings_file_relative_path()
],
"url": "zed://schemas/settings",
},
{
"fileMatch": [schema_file_match(paths::keymap_file())],
"url": "zed://schemas/keymap",
},
{
"fileMatch": [
schema_file_match(paths::tasks_file()),
paths::local_tasks_file_relative_path()
],
"url": "zed://schemas/tasks",
},
{
"fileMatch": [
schema_file_match(paths::debug_scenarios_file()),
paths::local_debug_file_relative_path()
],
"url": "zed://schemas/debug_tasks",
},
{
"fileMatch": [
schema_file_match(
paths::snippets_dir()
.join("*.json")
.as_path()
)
],
"url": "zed://schemas/snippets",
},
{
"fileMatch": ["tsconfig.json"],
"url": "zed://schemas/tsconfig"
},
{
"fileMatch": ["package.json"],
"url": "zed://schemas/package_json"
},
]);
#[cfg(debug_assertions)]
{
file_associations
.as_array_mut()
.unwrap()
.push(serde_json::json!({
"fileMatch": [
"zed-inspector-style.json"
],
"url": "zed://schemas/zed_inspector_style"
}));
}
file_associations.as_array_mut().unwrap().extend(
// ?PERF: use all_action_schemas() and don't include action schemas with no arguments
cx.all_action_names().into_iter().map(|&name| {
let normalized_name = normalize_action_name(name);
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)
})
}),
);
file_associations
}
fn tsconfig_schema() -> serde_json::Value {
serde_json::Value::from_str(TSCONFIG_SCHEMA).unwrap()
}
fn package_json_schema() -> serde_json::Value {
serde_json::Value::from_str(PACKAGE_JSON_SCHEMA).unwrap()
}
fn generate_inspector_style_schema() -> serde_json::Value {
let schema = schemars::generate::SchemaSettings::draft2019_09()
.with_transform(util::schemars::DefaultDenyUnknownFields)
.into_generator()
.root_schema_for::<gpui::StyleRefinement>();
serde_json::to_value(schema).unwrap()
}
pub fn normalize_action_name(action_name: &str) -> String {
action_name.replace("::", "__")
}
pub fn denormalize_action_name(action_name: &str) -> String {
action_name.replace("__", "::")
}
pub fn normalized_action_file_name(action_name: &str) -> String {
normalized_action_name_to_file_name(normalize_action_name(action_name))
}
pub fn normalized_action_name_to_file_name(mut normalized_action_name: String) -> String {
normalized_action_name.push_str(".json");
normalized_action_name
}
fn root_schema_from_action_schema(
action_schema: Option<schemars::Schema>,
generator: &mut schemars::SchemaGenerator,
) -> schemars::Schema {
let Some(mut action_schema) = action_schema else {
return schemars::json_schema!(false);
};
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 mut schema = schemars::json_schema!({
"$schema": meta_schema,
"allowTrailingCommas": true,
"$defs": defs,
});
schema
.ensure_object()
.extend(std::mem::take(action_schema.ensure_object()));
schema
}
#[inline]
fn schema_file_match(path: &std::path::Path) -> String {
path.strip_prefix(path.parent().unwrap().parent().unwrap())
.unwrap()
.display()
.to_string()
.replace('\\', "/")
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff