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
+22 -187
View File
@@ -3,23 +3,20 @@ use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use async_trait::async_trait;
use collections::HashMap;
use dap::DapRegistry;
use futures::StreamExt;
use gpui::{App, AsyncApp, SharedString, Task};
use gpui::{App, AsyncApp, Task};
use http_client::github::{GitHubLspBinaryVersion, latest_github_release};
use language::{
ContextProvider, LanguageName, LanguageRegistry, LocalFile as _, LspAdapter,
LspAdapterDelegate, LspInstaller, Toolchain,
ContextProvider, LanguageName, LocalFile as _, LspAdapter, LspAdapterDelegate, LspInstaller,
Toolchain,
};
use lsp::{LanguageServerBinary, LanguageServerName};
use node_runtime::{NodeRuntime, VersionStrategy};
use project::lsp_store::language_server_settings;
use serde_json::{Value, json};
use settings::{KeymapFile, SettingsJsonSchemaParams, SettingsStore};
use smol::{
fs::{self},
io::BufReader,
lock::RwLock,
};
use std::{
env::consts,
@@ -28,8 +25,7 @@ use std::{
str::FromStr,
sync::Arc,
};
use task::{AdapterSchemas, TaskTemplate, TaskTemplates, VariableName};
use theme::ThemeRegistry;
use task::{TaskTemplate, TaskTemplates, VariableName};
use util::{
ResultExt, archive::extract_zip, fs::remove_matching, maybe, merge_json_value_into,
rel_path::RelPath,
@@ -40,10 +36,6 @@ use crate::PackageJsonData;
const SERVER_PATH: &str =
"node_modules/vscode-langservers-extracted/bin/vscode-json-language-server";
// Origin: https://github.com/SchemaStore/schemastore
const TSCONFIG_SCHEMA: &str = include_str!("json/schemas/tsconfig.json");
const PACKAGE_JSON_SCHEMA: &str = include_str!("json/schemas/package.json");
pub(crate) struct JsonTaskProvider;
impl ContextProvider for JsonTaskProvider {
@@ -138,173 +130,14 @@ fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
pub struct JsonLspAdapter {
node: NodeRuntime,
languages: Arc<LanguageRegistry>,
workspace_config: RwLock<Option<Value>>,
}
impl JsonLspAdapter {
const PACKAGE_NAME: &str = "vscode-langservers-extracted";
pub fn new(node: NodeRuntime, languages: Arc<LanguageRegistry>) -> Self {
Self {
node,
languages,
workspace_config: Default::default(),
}
pub fn new(node: NodeRuntime) -> Self {
Self { node }
}
fn get_workspace_config(
language_names: Vec<String>,
adapter_schemas: AdapterSchemas,
cx: &mut App,
) -> Value {
let keymap_schema = KeymapFile::generate_json_schema_for_registered_actions(cx);
let font_names = &cx.text_system().all_font_names();
let themes = ThemeRegistry::try_global(cx);
let theme_names = &themes.clone().map(|t| t.list_names()).unwrap_or_default();
let icon_theme_names = &themes
.map(|t| {
t.list_icon_themes()
.into_iter()
.map(|icon_theme| icon_theme.name)
.collect::<Vec<SharedString>>()
})
.unwrap_or_default();
let settings_schema = cx
.global::<SettingsStore>()
.json_schema(&SettingsJsonSchemaParams {
language_names: &language_names,
font_names,
theme_names,
icon_theme_names,
});
let tasks_schema = task::TaskTemplates::generate_json_schema();
let debug_schema = task::DebugTaskFile::generate_json_schema(&adapter_schemas);
let snippets_schema = snippet_provider::format::VsSnippetsFile::generate_json_schema();
let tsconfig_schema = serde_json::Value::from_str(TSCONFIG_SCHEMA).unwrap();
let package_json_schema = serde_json::Value::from_str(PACKAGE_JSON_SCHEMA).unwrap();
#[allow(unused_mut)]
let mut schemas = serde_json::json!([
{
"fileMatch": ["tsconfig*.json"],
"schema":tsconfig_schema
},
{
"fileMatch": ["package.json"],
"schema":package_json_schema
},
{
"fileMatch": [
schema_file_match(paths::settings_file()),
paths::local_settings_file_relative_path()
],
"schema": settings_schema,
},
{
"fileMatch": [schema_file_match(paths::keymap_file())],
"schema": keymap_schema,
},
{
"fileMatch": [
schema_file_match(paths::tasks_file()),
paths::local_tasks_file_relative_path()
],
"schema": tasks_schema,
},
{
"fileMatch": [
schema_file_match(
paths::snippets_dir()
.join("*.json")
.as_path()
)
],
"schema": snippets_schema,
},
{
"fileMatch": [
schema_file_match(paths::debug_scenarios_file()),
paths::local_debug_file_relative_path()
],
"schema": debug_schema,
},
]);
#[cfg(debug_assertions)]
{
schemas.as_array_mut().unwrap().push(serde_json::json!(
{
"fileMatch": [
"zed-inspector-style.json"
],
"schema": generate_inspector_style_schema(),
}
))
}
schemas
.as_array_mut()
.unwrap()
.extend(cx.all_action_names().iter().map(|&name| {
project::lsp_store::json_language_server_ext::url_schema_for_action(name)
}));
// This can be viewed via `dev: open language server logs` -> `json-language-server` ->
// `Server Info`
serde_json::json!({
"json": {
"format": {
"enable": true,
},
"validate":
{
"enable": true,
},
"schemas": schemas
}
})
}
async fn get_or_init_workspace_config(&self, cx: &mut AsyncApp) -> Result<Value> {
{
let reader = self.workspace_config.read().await;
if let Some(config) = reader.as_ref() {
return Ok(config.clone());
}
}
let mut writer = self.workspace_config.write().await;
let adapter_schemas = cx
.read_global::<DapRegistry, _>(|dap_registry, _| dap_registry.to_owned())?
.adapters_schema()
.await;
let config = cx.update(|cx| {
Self::get_workspace_config(
self.languages
.language_names()
.into_iter()
.map(|name| name.to_string())
.collect(),
adapter_schemas,
cx,
)
})?;
writer.replace(config.clone());
Ok(config)
}
}
#[cfg(debug_assertions)]
fn generate_inspector_style_schema() -> serde_json_lenient::Value {
let schema = schemars::generate::SchemaSettings::draft2019_09()
.with_transform(util::schemars::DefaultDenyUnknownFields)
.into_generator()
.root_schema_for::<gpui::StyleRefinement>();
serde_json_lenient::to_value(schema).unwrap()
}
impl LspInstaller for JsonLspAdapter {
@@ -420,8 +253,23 @@ impl LspAdapter for JsonLspAdapter {
_: Option<Toolchain>,
cx: &mut AsyncApp,
) -> Result<Value> {
let mut config = self.get_or_init_workspace_config(cx).await?;
let mut config = cx.update(|cx| {
let schemas = json_schema_store::all_schema_file_associations(cx);
// This can be viewed via `dev: open language server logs` -> `json-language-server` ->
// `Server Info`
serde_json::json!({
"json": {
"format": {
"enable": true,
},
"validate": {
"enable": true,
},
"schemas": schemas
}
})
})?;
let project_options = cx.update(|cx| {
language_server_settings(delegate.as_ref(), &self.name(), cx)
.and_then(|s| s.settings.clone())
@@ -446,10 +294,6 @@ impl LspAdapter for JsonLspAdapter {
fn is_primary_zed_json_schema_adapter(&self) -> bool {
true
}
async fn clear_zed_json_schema_cache(&self) {
self.workspace_config.write().await.take();
}
}
async fn get_cached_server_binary(
@@ -482,15 +326,6 @@ async fn get_cached_server_binary(
.log_err()
}
#[inline]
fn schema_file_match(path: &Path) -> String {
path.strip_prefix(path.parent().unwrap().parent().unwrap())
.unwrap()
.display()
.to_string()
.replace('\\', "/")
}
pub struct NodeVersionAdapter;
impl NodeVersionAdapter {
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -88,7 +88,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(), languages.clone()));
let json_lsp_adapter = Arc::new(json::JsonLspAdapter::new(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()));