Use LazyLock for static JSON schemas (#46823)

Richard Feldman and Ben Kunkle created

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 <ben@zed.dev>

Change summary

Cargo.lock                                        |   2 
crates/json_schema_store/Cargo.toml               |   2 
crates/json_schema_store/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(-)

Detailed changes

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",

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

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<String> = LazyLock::new(|| {
+    serde_json::to_string(&task::TaskTemplates::generate_json_schema())
+        .expect("TaskTemplates schema should serialize")
+});
+
+static SNIPPETS_SCHEMA: LazyLock<String> = LazyLock::new(|| {
+    serde_json::to_string(&snippet_provider::format::VsSnippetsFile::generate_json_schema())
+        .expect("VsSnippetsFile schema should serialize")
+});
+
+static JSONC_SCHEMA: LazyLock<String> = LazyLock::new(|| {
+    serde_json::to_string(&generate_jsonc_schema()).expect("JSONC schema should serialize")
+});
+
+#[cfg(debug_assertions)]
+static INSPECTOR_STYLE_SCHEMA: LazyLock<String> = LazyLock::new(|| {
+    serde_json::to_string(&generate_inspector_style_schema())
+        .expect("Inspector style schema should serialize")
+});
+
+static KEYMAP_SCHEMA: LazyLock<String> = LazyLock::new(|| {
+    serde_json::to_string(&settings::KeymapFile::generate_json_schema_from_inventory())
+        .expect("Keymap schema should serialize")
+});
+
+static ACTION_SCHEMA_CACHE: LazyLock<RwLock<HashMap<String, String>>> =
+    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<LspStore>,
     uri: String,
     cx: &mut AsyncApp,
 ) -> Task<Result<String>> {
-    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<LanguageRegistry>,
+async fn resolve_dynamic_schema(
     lsp_store: Entity<LspStore>,
-    uri: String,
+    schema_name: &str,
+    rest: Option<&str>,
     cx: &mut AsyncApp,
 ) -> Result<serde_json::Value> {
-    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<LanguageRegistry>,
-    lsp_store: Entity<LspStore>,
-    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 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::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(),
-        "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)

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<schemars::Schema> {
+        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<schemars::Schema>)>,

crates/zed/src/main.rs 🔗

@@ -931,16 +931,15 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, 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