@@ -1,4 +1,3 @@
-//! # json_schema_store
use std::sync::{Arc, LazyLock};
use anyhow::{Context as _, Result};
@@ -10,6 +9,8 @@ use project::{LspStore, lsp_store::LocalLspAdapterDelegate};
use settings::LSP_SETTINGS_SCHEMA_URL_PREFIX;
use util::schemars::{AllowTrailingCommas, DefaultDenyUnknownFields};
+const SCHEMA_URI_PREFIX: &str = "zed://schemas/";
+
const TSCONFIG_SCHEMA: &str = include_str!("schemas/tsconfig.json");
const PACKAGE_JSON_SCHEMA: &str = include_str!("schemas/package.json");
@@ -41,6 +42,14 @@ static KEYMAP_SCHEMA: LazyLock<String> = LazyLock::new(|| {
static ACTION_SCHEMA_CACHE: LazyLock<RwLock<HashMap<String, String>>> =
LazyLock::new(|| RwLock::new(HashMap::default()));
+// Runtime cache for dynamic schemas that depend on runtime state:
+// - "settings": depends on installed fonts, themes, languages, LSP adapters (extensions can add these)
+// - "settings/lsp/*": depends on LSP adapter initialization options
+// - "debug_tasks": depends on DAP adapters (extensions can add these)
+// Cache is invalidated via notify_schema_changed() when extensions or DAP registry change.
+static DYNAMIC_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(
@@ -55,7 +64,7 @@ pub fn init(cx: &mut App) {
.detach();
if let Some(extension_events) = extension::ExtensionEvents::try_global(cx) {
- cx.subscribe(&extension_events, |_, evt, cx| {
+ cx.subscribe(&extension_events, move |_, evt, cx| {
match evt {
extension::Event::ExtensionInstalled(_)
| extension::Event::ExtensionUninstalled(_)
@@ -63,15 +72,15 @@ pub fn init(cx: &mut App) {
extension::Event::ExtensionsInstalledChanged => {}
}
cx.update_global::<SchemaStore, _>(|schema_store, cx| {
- schema_store.notify_schema_changed("zed://schemas/settings", cx);
+ schema_store.notify_schema_changed(&format!("{SCHEMA_URI_PREFIX}settings"), cx);
});
})
.detach();
}
- cx.observe_global::<dap::DapRegistry>(|cx| {
+ cx.observe_global::<dap::DapRegistry>(move |cx| {
cx.update_global::<SchemaStore, _>(|schema_store, cx| {
- schema_store.notify_schema_changed("zed://schemas/debug_tasks", cx);
+ schema_store.notify_schema_changed(&format!("{SCHEMA_URI_PREFIX}debug_tasks"), cx);
});
})
.detach();
@@ -86,6 +95,8 @@ impl gpui::Global for SchemaStore {}
impl SchemaStore {
fn notify_schema_changed(&mut self, uri: &str, cx: &mut App) {
+ DYNAMIC_SCHEMA_CACHE.write().remove(uri);
+
let uri = uri.to_string();
self.lsp_stores.retain(|lsp_store| {
let Some(lsp_store) = lsp_store.upgrade() else {
@@ -106,38 +117,65 @@ pub fn handle_schema_request(
uri: String,
cx: &mut AsyncApp,
) -> Task<Result<String>> {
- let path = match uri.strip_prefix("zed://schemas/") {
+ let path = match uri.strip_prefix(SCHEMA_URI_PREFIX) {
Some(path) => path,
- None => return Task::ready(Err(anyhow::anyhow!("Invalid URI: {}", uri))),
+ None => return Task::ready(Err(anyhow::anyhow!("Invalid schema URI: {}", uri))),
};
+ if let Some(json) = resolve_static_schema(path) {
+ return Task::ready(Ok(json));
+ }
+
+ if let Some(cached) = DYNAMIC_SCHEMA_CACHE.read().get(&uri).cloned() {
+ return Task::ready(Ok(cached));
+ }
+
+ let path = path.to_string();
+ let uri_clone = uri.clone();
+ cx.spawn(async move |cx| {
+ let schema = resolve_dynamic_schema(lsp_store, &path, cx).await?;
+ let json = serde_json::to_string(&schema).context("Failed to serialize schema")?;
+
+ DYNAMIC_SCHEMA_CACHE.write().insert(uri_clone, json.clone());
+
+ Ok(json)
+ })
+}
+
+fn resolve_static_schema(path: &str) -> Option<String> {
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())),
+ "tsconfig" => Some(TSCONFIG_SCHEMA.to_string()),
+ "package_json" => Some(PACKAGE_JSON_SCHEMA.to_string()),
+ "tasks" => Some(TASKS_SCHEMA.clone()),
+ "snippets" => Some(SNIPPETS_SCHEMA.clone()),
+ "jsonc" => Some(JSONC_SCHEMA.clone()),
+ "keymap" => Some(KEYMAP_SCHEMA.clone()),
"zed_inspector_style" => {
#[cfg(debug_assertions)]
- return Task::ready(Ok(INSPECTOR_STYLE_SCHEMA.clone()));
+ {
+ Some(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")));
+ {
+ Some(
+ 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"))),
+ None => return None,
};
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));
+ return Some(cached);
}
let mut generator = settings::KeymapFile::action_schema_generator();
@@ -151,34 +189,31 @@ pub fn handle_schema_request(
ACTION_SCHEMA_CACHE
.write()
.insert(action_name, json.clone());
- return Task::ready(Ok(json));
+ Some(json)
}
- _ => {}
- }
- let schema_name = schema_name.to_string();
- let rest = rest.map(|s| s.to_string());
- cx.spawn(async move |cx| {
- let schema = resolve_dynamic_schema(lsp_store, &schema_name, rest.as_deref(), cx).await?;
- serde_json::to_string(&schema).context("Failed to serialize schema")
- })
+ _ => None,
+ }
}
async fn resolve_dynamic_schema(
lsp_store: Entity<LspStore>,
- schema_name: &str,
- rest: Option<&str>,
+ path: &str,
cx: &mut AsyncApp,
) -> Result<serde_json::Value> {
- let languages = lsp_store.read_with(cx, |store, _| store.languages.clone());
+ let languages = lsp_store.read_with(cx, |lsp_store, _| lsp_store.languages.clone());
+ let (schema_name, rest) = path.split_once('/').unzip();
+ let schema_name = schema_name.unwrap_or(path);
+
let schema = match schema_name {
"settings" if rest.is_some_and(|r| r.starts_with("lsp/")) => {
let lsp_name = rest
.and_then(|r| {
r.strip_prefix(
LSP_SETTINGS_SCHEMA_URL_PREFIX
- .strip_prefix("zed://schemas/settings/")
- .unwrap(),
+ .strip_prefix(SCHEMA_URI_PREFIX)
+ .and_then(|s| s.strip_prefix("settings/"))
+ .unwrap_or("lsp/"),
)
})
.context("Invalid LSP schema path")?;
@@ -266,7 +301,7 @@ async fn resolve_dynamic_schema(
task::DebugTaskFile::generate_json_schema(&adapter_schemas)
}
_ => {
- anyhow::bail!("Unrecognized builtin JSON schema: {schema_name}");
+ anyhow::bail!("Unrecognized schema: {schema_name}");
}
};
Ok(schema)
@@ -299,25 +334,25 @@ pub fn all_schema_file_associations(
schema_file_match(paths::settings_file()),
paths::local_settings_file_relative_path()
],
- "url": "zed://schemas/settings",
+ "url": format!("{SCHEMA_URI_PREFIX}settings"),
},
{
"fileMatch": [schema_file_match(paths::keymap_file())],
- "url": "zed://schemas/keymap",
+ "url": format!("{SCHEMA_URI_PREFIX}keymap"),
},
{
"fileMatch": [
schema_file_match(paths::tasks_file()),
paths::local_tasks_file_relative_path()
],
- "url": "zed://schemas/tasks",
+ "url": format!("{SCHEMA_URI_PREFIX}tasks"),
},
{
"fileMatch": [
schema_file_match(paths::debug_scenarios_file()),
paths::local_debug_file_relative_path()
],
- "url": "zed://schemas/debug_tasks",
+ "url": format!("{SCHEMA_URI_PREFIX}debug_tasks"),
},
{
"fileMatch": [
@@ -327,19 +362,19 @@ pub fn all_schema_file_associations(
.as_path()
)
],
- "url": "zed://schemas/snippets",
+ "url": format!("{SCHEMA_URI_PREFIX}snippets"),
},
{
"fileMatch": ["tsconfig.json"],
- "url": "zed://schemas/tsconfig"
+ "url": format!("{SCHEMA_URI_PREFIX}tsconfig")
},
{
"fileMatch": ["package.json"],
- "url": "zed://schemas/package_json"
+ "url": format!("{SCHEMA_URI_PREFIX}package_json")
},
{
"fileMatch": &jsonc_globs,
- "url": "zed://schemas/jsonc"
+ "url": format!("{SCHEMA_URI_PREFIX}jsonc")
},
]);
@@ -352,21 +387,21 @@ pub fn all_schema_file_associations(
"fileMatch": [
"zed-inspector-style.json"
],
- "url": "zed://schemas/zed_inspector_style"
+ "url": format!("{SCHEMA_URI_PREFIX}zed_inspector_style")
}));
}
- file_associations.as_array_mut().unwrap().extend(
- // ?PERF: use all_action_schemas() and don't include action schemas with no arguments
- cx.all_action_names().into_iter().map(|&name| {
+ file_associations
+ .as_array_mut()
+ .unwrap()
+ .extend(cx.all_action_names().into_iter().map(|&name| {
let normalized_name = normalize_action_name(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}")
+ "url": format!("{}action/{normalized_name}", SCHEMA_URI_PREFIX)
})
- }),
- );
+ }));
file_associations
}