From fd877a9a3dcfc98a152c35806f00faddbc6d0c1b Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Wed, 14 Jan 2026 19:00:39 -0500 Subject: [PATCH] Use LazyLock for static JSON schemas (#46823) Static schemas (tasks, snippets, jsonc, keymap, action/*, tsconfig, package_json, inspector_style) are now computed once on first access using LazyLock and returned immediately via Task::ready(). These schemas never change at runtime because: - tasks, snippets, jsonc, inspector_style are derived from static Rust types - tsconfig, package_json are bundled JSON files (include_str!) - keymap and action/* depend only on registered actions, which are collected at compile/link time via the inventory crate (extensions cannot add actions) This eliminates foreground thread blocking for these schema requests. **New functions added to KeymapFile:** - `generate_json_schema_from_inventory()`: generates keymap schema without App context - `get_action_schema_by_name()`: looks up single action schema without App context Release Notes: - N/A --------- Co-authored-by: Ben Kunkle --- Cargo.lock | 2 + crates/json_schema_store/Cargo.toml | 2 + .../src/json_schema_store.rs | 146 +++++++++++------- crates/settings/src/keymap_file.rs | 54 ++++++- crates/zed/src/main.rs | 15 +- 5 files changed, 155 insertions(+), 64 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 17916306852a0a596f7a2e36ba95479bb857adba..7cd71e82604bbc5ad37b17d44abfee31375ff529 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8684,10 +8684,12 @@ name = "json_schema_store" version = "0.1.0" dependencies = [ "anyhow", + "collections", "dap", "extension", "gpui", "language", + "parking_lot", "paths", "project", "schemars", diff --git a/crates/json_schema_store/Cargo.toml b/crates/json_schema_store/Cargo.toml index f973fe2ebfbdbac1020f75297c4a469d42505f48..e62c976abb934972b2c81981b13ce3021bef00d0 100644 --- a/crates/json_schema_store/Cargo.toml +++ b/crates/json_schema_store/Cargo.toml @@ -16,7 +16,9 @@ default = [] [dependencies] anyhow.workspace = true +collections.workspace = true dap.workspace = true +parking_lot.workspace = true extension.workspace = true gpui.workspace = true language.workspace = true diff --git a/crates/json_schema_store/src/json_schema_store.rs b/crates/json_schema_store/src/json_schema_store.rs index f728e844e1feb624c666fdc1f99a549e73081698..4b75a9667ff4698c57c8676823898f142ee52136 100644 --- a/crates/json_schema_store/src/json_schema_store.rs +++ b/crates/json_schema_store/src/json_schema_store.rs @@ -1,17 +1,46 @@ //! # json_schema_store -use std::{str::FromStr, sync::Arc}; +use std::sync::{Arc, LazyLock}; use anyhow::{Context as _, Result}; +use collections::HashMap; use gpui::{App, AsyncApp, BorrowAppContext as _, Entity, Task, WeakEntity}; use language::{LanguageRegistry, LspAdapterDelegate, language_settings::all_language_settings}; +use parking_lot::RwLock; use project::{LspStore, lsp_store::LocalLspAdapterDelegate}; use settings::LSP_SETTINGS_SCHEMA_URL_PREFIX; use util::schemars::{AllowTrailingCommas, DefaultDenyUnknownFields}; -// 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"); +static TASKS_SCHEMA: LazyLock = LazyLock::new(|| { + serde_json::to_string(&task::TaskTemplates::generate_json_schema()) + .expect("TaskTemplates schema should serialize") +}); + +static SNIPPETS_SCHEMA: LazyLock = LazyLock::new(|| { + serde_json::to_string(&snippet_provider::format::VsSnippetsFile::generate_json_schema()) + .expect("VsSnippetsFile schema should serialize") +}); + +static JSONC_SCHEMA: LazyLock = LazyLock::new(|| { + serde_json::to_string(&generate_jsonc_schema()).expect("JSONC schema should serialize") +}); + +#[cfg(debug_assertions)] +static INSPECTOR_STYLE_SCHEMA: LazyLock = LazyLock::new(|| { + serde_json::to_string(&generate_inspector_style_schema()) + .expect("Inspector style schema should serialize") +}); + +static KEYMAP_SCHEMA: LazyLock = LazyLock::new(|| { + serde_json::to_string(&settings::KeymapFile::generate_json_schema_from_inventory()) + .expect("Keymap schema should serialize") +}); + +static ACTION_SCHEMA_CACHE: LazyLock>> = + LazyLock::new(|| RwLock::new(HashMap::default())); + pub fn init(cx: &mut App) { cx.set_global(SchemaStore::default()); project::lsp_store::json_language_server_ext::register_schema_handler( @@ -72,37 +101,76 @@ impl SchemaStore { } } -fn handle_schema_request( +pub fn handle_schema_request( lsp_store: Entity, uri: String, cx: &mut AsyncApp, ) -> Task> { - let languages = lsp_store.read_with(cx, |lsp_store, _| lsp_store.languages.clone()); + let path = match uri.strip_prefix("zed://schemas/") { + Some(path) => path, + None => return Task::ready(Err(anyhow::anyhow!("Invalid URI: {}", uri))), + }; + + let (schema_name, rest) = path.split_once('/').unzip(); + let schema_name = schema_name.unwrap_or(path); + + match schema_name { + "tsconfig" => return Task::ready(Ok(TSCONFIG_SCHEMA.to_string())), + "package_json" => return Task::ready(Ok(PACKAGE_JSON_SCHEMA.to_string())), + "tasks" => return Task::ready(Ok(TASKS_SCHEMA.clone())), + "snippets" => return Task::ready(Ok(SNIPPETS_SCHEMA.clone())), + "jsonc" => return Task::ready(Ok(JSONC_SCHEMA.clone())), + "keymap" => return Task::ready(Ok(KEYMAP_SCHEMA.clone())), + "zed_inspector_style" => { + #[cfg(debug_assertions)] + return Task::ready(Ok(INSPECTOR_STYLE_SCHEMA.clone())); + #[cfg(not(debug_assertions))] + return Task::ready(Ok(serde_json::to_string( + &schemars::json_schema!(true).to_value(), + ) + .expect("true schema should serialize"))); + } + "action" => { + let normalized_action_name = match rest { + Some(name) => name, + None => return Task::ready(Err(anyhow::anyhow!("No action name provided"))), + }; + let action_name = denormalize_action_name(normalized_action_name); + if let Some(cached) = ACTION_SCHEMA_CACHE.read().get(&action_name).cloned() { + return Task::ready(Ok(cached)); + } + + let mut generator = settings::KeymapFile::action_schema_generator(); + let schema = + settings::KeymapFile::get_action_schema_by_name(&action_name, &mut generator); + let json = serde_json::to_string( + &root_schema_from_action_schema(schema, &mut generator).to_value(), + ) + .expect("Action schema should serialize"); + + ACTION_SCHEMA_CACHE + .write() + .insert(action_name, json.clone()); + return Task::ready(Ok(json)); + } + _ => {} + } + + let schema_name = schema_name.to_string(); + let rest = rest.map(|s| s.to_string()); cx.spawn(async move |cx| { - let schema = resolve_schema_request(&languages, lsp_store, uri, cx).await?; + let schema = resolve_dynamic_schema(lsp_store, &schema_name, rest.as_deref(), cx).await?; serde_json::to_string(&schema).context("Failed to serialize schema") }) } -pub async fn resolve_schema_request( - languages: &Arc, +async fn resolve_dynamic_schema( lsp_store: Entity, - uri: String, + schema_name: &str, + rest: Option<&str>, cx: &mut AsyncApp, ) -> Result { - let path = uri.strip_prefix("zed://schemas/").context("Invalid URI")?; - resolve_schema_request_inner(languages, lsp_store, path, cx).await -} - -pub async fn resolve_schema_request_inner( - languages: &Arc, - lsp_store: Entity, - path: &str, - cx: &mut AsyncApp, -) -> Result { - let (schema_name, rest) = path.split_once('/').unzip(); - let schema_name = schema_name.unwrap_or(path); - + let languages = lsp_store.read_with(cx, |store, _| store.languages.clone()); let schema = match schema_name { "settings" if rest.is_some_and(|r| r.starts_with("lsp/")) => { let lsp_name = rest @@ -191,37 +259,12 @@ pub async fn resolve_schema_request_inner( ) }) } - "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_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(), - "jsonc" => jsonc_schema(), _ => { anyhow::bail!("Unrecognized builtin JSON schema: {schema_name}"); } @@ -328,15 +371,7 @@ pub fn all_schema_file_associations( 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 jsonc_schema() -> serde_json::Value { +fn generate_jsonc_schema() -> serde_json::Value { let generator = schemars::generate::SchemaSettings::draft2019_09() .with_transform(DefaultDenyUnknownFields) .with_transform(AllowTrailingCommas) @@ -356,6 +391,7 @@ fn jsonc_schema() -> serde_json::Value { serde_json::to_value(schema).unwrap() } +#[cfg(debug_assertions)] fn generate_inspector_style_schema() -> serde_json::Value { let schema = schemars::generate::SchemaSettings::draft2019_09() .with_transform(util::schemars::DefaultDenyUnknownFields) diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index 146fc371b14cb5cba428d3a7beec11cc3008e7dd..569864ee792e0c9e363a6eff52a3f6db499864fa 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -4,7 +4,7 @@ use fs::Fs; use gpui::{ Action, ActionBuildError, App, InvalidKeystrokeError, KEYSTROKE_PARSE_EXPECTED_MESSAGE, KeyBinding, KeyBindingContextPredicate, KeyBindingMetaIndex, KeybindingKeystroke, Keystroke, - NoAction, SharedString, register_action, + NoAction, SharedString, generate_list_of_all_registered_actions, register_action, }; use schemars::{JsonSchema, json_schema}; use serde::Deserialize; @@ -479,6 +479,58 @@ impl KeymapFile { ) } + pub fn generate_json_schema_from_inventory() -> Value { + let mut generator = Self::action_schema_generator(); + + let mut action_schemas = Vec::new(); + let mut documentation = HashMap::default(); + let mut deprecations = HashMap::default(); + let mut deprecation_messages = HashMap::default(); + + for action_data in generate_list_of_all_registered_actions() { + let schema = (action_data.json_schema)(&mut generator); + action_schemas.push((action_data.name, schema)); + + if let Some(doc) = action_data.documentation { + documentation.insert(action_data.name, doc); + } + if let Some(msg) = action_data.deprecation_message { + deprecation_messages.insert(action_data.name, msg); + } + for &alias in action_data.deprecated_aliases { + deprecations.insert(alias, action_data.name); + + let alias_schema = (action_data.json_schema)(&mut generator); + action_schemas.push((alias, alias_schema)); + } + } + + KeymapFile::generate_json_schema( + generator, + action_schemas, + &documentation, + &deprecations, + &deprecation_messages, + ) + } + + pub fn get_action_schema_by_name( + action_name: &str, + generator: &mut schemars::SchemaGenerator, + ) -> Option { + for action_data in generate_list_of_all_registered_actions() { + if action_data.name == action_name { + return (action_data.json_schema)(generator); + } + for &alias in action_data.deprecated_aliases { + if alias == action_name { + return (action_data.json_schema)(generator); + } + } + } + None + } + fn generate_json_schema( mut generator: schemars::SchemaGenerator, action_schemas: Vec<(&'static str, Option)>, diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index e56f9e236853eca1c10f18f76cd6e29b7e4b594b..b4ef673a3338c632133ab21c5ff9c76ea538368e 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -931,16 +931,15 @@ fn handle_open_request(request: OpenRequest, app_state: Arc, cx: &mut .project() .update(cx, |project, _| project.lsp_store()) })?; + let uri = format!("zed://schemas/{}", schema_path); let json_schema_content = - json_schema_store::resolve_schema_request_inner( - &app_state.languages, - lsp_store, - &schema_path, - cx, - ) - .await?; + json_schema_store::handle_schema_request(lsp_store, uri, cx) + .await?; + let json_schema_value: serde_json::Value = + serde_json::from_str(&json_schema_content) + .context("Failed to parse JSON Schema")?; let json_schema_content = - serde_json::to_string_pretty(&json_schema_content) + serde_json::to_string_pretty(&json_schema_value) .context("Failed to serialize JSON Schema as JSON")?; let buffer_task = workspace.update(cx, |workspace, cx| { workspace