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:
@@ -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 {:?}", ¶ms);
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user