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
-103
View File
@@ -3707,15 +3707,6 @@ impl LspStore {
.detach();
cx.subscribe(&toolchain_store, Self::on_toolchain_store_event)
.detach();
if let Some(extension_events) = extension::ExtensionEvents::try_global(cx).as_ref() {
cx.subscribe(
extension_events,
Self::reload_zed_json_schemas_on_extensions_changed,
)
.detach();
} else {
log::debug!("No extension events global found. Skipping JSON schema auto-reload setup");
}
cx.observe_global::<SettingsStore>(Self::on_settings_changed)
.detach();
subscribe_to_binary_statuses(&languages, cx).detach();
@@ -3991,100 +3982,6 @@ impl LspStore {
Ok(())
}
pub fn reload_zed_json_schemas_on_extensions_changed(
&mut self,
_: Entity<extension::ExtensionEvents>,
evt: &extension::Event,
cx: &mut Context<Self>,
) {
match evt {
extension::Event::ExtensionInstalled(_)
| extension::Event::ExtensionUninstalled(_)
| extension::Event::ConfigureExtensionRequested(_) => return,
extension::Event::ExtensionsInstalledChanged => {}
}
if self.as_local().is_none() {
return;
}
cx.spawn(async move |this, cx| {
let weak_ref = this.clone();
let servers = this
.update(cx, |this, cx| {
let local = this.as_local()?;
let mut servers = Vec::new();
for (seed, state) in &local.language_server_ids {
let Some(states) = local.language_servers.get(&state.id) else {
continue;
};
let (json_adapter, json_server) = match states {
LanguageServerState::Running {
adapter, server, ..
} if adapter.adapter.is_primary_zed_json_schema_adapter() => {
(adapter.adapter.clone(), server.clone())
}
_ => continue,
};
let Some(worktree) = this
.worktree_store
.read(cx)
.worktree_for_id(seed.worktree_id, cx)
else {
continue;
};
let json_delegate: Arc<dyn LspAdapterDelegate> =
LocalLspAdapterDelegate::new(
local.languages.clone(),
&local.environment,
weak_ref.clone(),
&worktree,
local.http_client.clone(),
local.fs.clone(),
cx,
);
servers.push((json_adapter, json_server, json_delegate));
}
Some(servers)
})
.ok()
.flatten();
let Some(servers) = servers else {
return;
};
for (adapter, server, delegate) in servers {
adapter.clear_zed_json_schema_cache().await;
let Some(json_workspace_config) = LocalLspStore::workspace_configuration_for_adapter(
adapter,
&delegate,
None,
cx,
)
.await
.context("generate new workspace configuration for JSON language server while trying to refresh JSON Schemas")
.ok()
else {
continue;
};
server
.notify::<lsp::notification::DidChangeConfiguration>(
&lsp::DidChangeConfigurationParams {
settings: json_workspace_config,
},
)
.ok();
}
})
.detach();
}
pub(crate) fn register_buffer_with_language_servers(
&mut self,
buffer: &Entity<Buffer>,
@@ -1,9 +1,11 @@
use anyhow::Context as _;
use collections::HashMap;
use gpui::WeakEntity;
use anyhow::{Context, Result};
use gpui::{App, AsyncApp, Entity, Global, WeakEntity};
use lsp::LanguageServer;
use crate::LspStore;
const LOGGER: zlog::Logger = zlog::scoped!("json-schema");
/// https://github.com/Microsoft/vscode/blob/main/extensions/json-language-features/server/README.md#schema-content-request
///
/// Represents a "JSON language server-specific, non-standardized, extension to the LSP" with which the vscode-json-language-server
@@ -20,82 +22,77 @@ impl lsp::request::Request for SchemaContentRequest {
const METHOD: &'static str = "vscode/content";
}
pub fn register_requests(_lsp_store: WeakEntity<LspStore>, language_server: &LanguageServer) {
language_server
.on_request::<SchemaContentRequest, _, _>(|params, cx| {
// PERF: Use a cache (`OnceLock`?) to avoid recomputing the action schemas
let mut generator = settings::KeymapFile::action_schema_generator();
let all_schemas = cx.update(|cx| HashMap::from_iter(cx.action_schemas(&mut generator)));
async move {
let all_schemas = all_schemas?;
let Some(uri) = params.get(0) else {
anyhow::bail!("No URI");
};
let normalized_action_name = uri
.strip_prefix("zed://schemas/action/")
.context("Invalid URI")?;
let action_name = denormalize_action_name(normalized_action_name);
let schema = root_schema_from_action_schema(
all_schemas
.get(action_name.as_str())
.and_then(Option::as_ref),
&mut generator,
)
.to_value();
type SchemaRequestHandler = fn(Entity<LspStore>, String, &mut AsyncApp) -> Result<String>;
pub struct SchemaHandlingImpl(SchemaRequestHandler);
serde_json::to_string(&schema).context("Failed to serialize schema")
impl Global for SchemaHandlingImpl {}
pub fn register_schema_handler(handler: SchemaRequestHandler, cx: &mut App) {
debug_assert!(
!cx.has_global::<SchemaHandlingImpl>(),
"SchemaHandlingImpl already registered"
);
cx.set_global(SchemaHandlingImpl(handler));
}
struct SchemaContentsChanged {}
impl lsp::notification::Notification for SchemaContentsChanged {
const METHOD: &'static str = "json/schemaContent";
type Params = String;
}
pub fn notify_schema_changed(lsp_store: Entity<LspStore>, uri: &String, cx: &App) {
zlog::trace!(LOGGER => "Notifying schema changed for URI: {:?}", uri);
let servers = lsp_store.read_with(cx, |lsp_store, _| {
let mut servers = Vec::new();
let Some(local) = lsp_store.as_local() else {
return servers;
};
for states in local.language_servers.values() {
let json_server = match states {
super::LanguageServerState::Running {
adapter, server, ..
} if adapter.adapter.is_primary_zed_json_schema_adapter() => server.clone(),
_ => continue,
};
servers.push(json_server);
}
servers
});
for server in servers {
zlog::trace!(LOGGER => "Notifying server {:?} of schema change for URI: {:?}", server.server_id(), &uri);
// TODO: handle errors
server.notify::<SchemaContentsChanged>(uri).ok();
}
}
pub fn register_requests(lsp_store: WeakEntity<LspStore>, language_server: &LanguageServer) {
language_server
.on_request::<SchemaContentRequest, _, _>(move |params, cx| {
let handler = cx.try_read_global::<SchemaHandlingImpl, _>(|handler, _| {
handler.0
});
let mut cx = cx.clone();
let uri = params.clone().pop();
let lsp_store = lsp_store.clone();
let resolution = async move {
let lsp_store = lsp_store.upgrade().context("LSP store has been dropped")?;
let uri = uri.context("No URI")?;
let handle_schema_request = handler.context("No schema handler registered")?;
handle_schema_request(lsp_store, uri, &mut cx)
};
async move {
zlog::trace!(LOGGER => "Handling schema request for {:?}", &params);
let result = resolution.await;
match &result {
Ok(content) => {zlog::trace!(LOGGER => "Schema request resolved with {}B schema", content.len());},
Err(err) => {zlog::warn!(LOGGER => "Schema request failed: {}", err);},
}
result
}
})
.detach();
}
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
}
pub fn url_schema_for_action(action_name: &str) -> serde_json::Value {
let normalized_name = normalize_action_name(action_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)
})
}
fn root_schema_from_action_schema(
action_schema: Option<&schemars::Schema>,
generator: &mut schemars::SchemaGenerator,
) -> schemars::Schema {
let Some(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.clone().ensure_object()));
schema
}