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",
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>
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(-)
@@ -8684,10 +8684,12 @@ name = "json_schema_store"
version = "0.1.0"
dependencies = [
"anyhow",
+ "collections",
"dap",
"extension",
"gpui",
"language",
+ "parking_lot",
"paths",
"project",
"schemars",
@@ -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
@@ -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)
@@ -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>)>,
@@ -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