1use std::sync::{Arc, LazyLock};
2
3use anyhow::{Context as _, Result};
4use collections::HashMap;
5use gpui::{App, AsyncApp, BorrowAppContext as _, Entity, Task, WeakEntity};
6use language::{LanguageRegistry, LspAdapterDelegate, language_settings::all_language_settings};
7use parking_lot::RwLock;
8use project::{LspStore, lsp_store::LocalLspAdapterDelegate};
9use settings::LSP_SETTINGS_SCHEMA_URL_PREFIX;
10use util::schemars::{AllowTrailingCommas, DefaultDenyUnknownFields};
11
12const SCHEMA_URI_PREFIX: &str = "zed://schemas/";
13
14const TSCONFIG_SCHEMA: &str = include_str!("schemas/tsconfig.json");
15const PACKAGE_JSON_SCHEMA: &str = include_str!("schemas/package.json");
16
17static TASKS_SCHEMA: LazyLock<String> = LazyLock::new(|| {
18 serde_json::to_string(&task::TaskTemplates::generate_json_schema())
19 .expect("TaskTemplates schema should serialize")
20});
21
22static SNIPPETS_SCHEMA: LazyLock<String> = LazyLock::new(|| {
23 serde_json::to_string(&snippet_provider::format::VsSnippetsFile::generate_json_schema())
24 .expect("VsSnippetsFile schema should serialize")
25});
26
27static JSONC_SCHEMA: LazyLock<String> = LazyLock::new(|| {
28 serde_json::to_string(&generate_jsonc_schema()).expect("JSONC schema should serialize")
29});
30
31#[cfg(debug_assertions)]
32static INSPECTOR_STYLE_SCHEMA: LazyLock<String> = LazyLock::new(|| {
33 serde_json::to_string(&generate_inspector_style_schema())
34 .expect("Inspector style schema should serialize")
35});
36
37static KEYMAP_SCHEMA: LazyLock<String> = LazyLock::new(|| {
38 serde_json::to_string(&settings::KeymapFile::generate_json_schema_from_inventory())
39 .expect("Keymap schema should serialize")
40});
41
42static ACTION_SCHEMA_CACHE: LazyLock<RwLock<HashMap<String, String>>> =
43 LazyLock::new(|| RwLock::new(HashMap::default()));
44
45// Runtime cache for dynamic schemas that depend on runtime state:
46// - "settings": depends on installed fonts, themes, languages, LSP adapters (extensions can add these)
47// - "settings/lsp/*": depends on LSP adapter initialization options
48// - "debug_tasks": depends on DAP adapters (extensions can add these)
49// Cache is invalidated via notify_schema_changed() when extensions or DAP registry change.
50static DYNAMIC_SCHEMA_CACHE: LazyLock<RwLock<HashMap<String, String>>> =
51 LazyLock::new(|| RwLock::new(HashMap::default()));
52
53pub fn init(cx: &mut App) {
54 cx.set_global(SchemaStore::default());
55 project::lsp_store::json_language_server_ext::register_schema_handler(
56 handle_schema_request,
57 cx,
58 );
59
60 cx.observe_new(|_, _, cx| {
61 let lsp_store = cx.weak_entity();
62 cx.global_mut::<SchemaStore>().lsp_stores.push(lsp_store);
63 })
64 .detach();
65
66 if let Some(extension_events) = extension::ExtensionEvents::try_global(cx) {
67 cx.subscribe(&extension_events, move |_, evt, cx| {
68 match evt {
69 extension::Event::ExtensionInstalled(_)
70 | extension::Event::ExtensionUninstalled(_)
71 | extension::Event::ConfigureExtensionRequested(_) => return,
72 extension::Event::ExtensionsInstalledChanged => {}
73 }
74 cx.update_global::<SchemaStore, _>(|schema_store, cx| {
75 schema_store.notify_schema_changed(&format!("{SCHEMA_URI_PREFIX}settings"), cx);
76 });
77 })
78 .detach();
79 }
80
81 cx.observe_global::<dap::DapRegistry>(move |cx| {
82 cx.update_global::<SchemaStore, _>(|schema_store, cx| {
83 schema_store.notify_schema_changed(&format!("{SCHEMA_URI_PREFIX}debug_tasks"), cx);
84 });
85 })
86 .detach();
87}
88
89#[derive(Default)]
90pub struct SchemaStore {
91 lsp_stores: Vec<WeakEntity<LspStore>>,
92}
93
94impl gpui::Global for SchemaStore {}
95
96impl SchemaStore {
97 fn notify_schema_changed(&mut self, uri: &str, cx: &mut App) {
98 DYNAMIC_SCHEMA_CACHE.write().remove(uri);
99
100 let uri = uri.to_string();
101 self.lsp_stores.retain(|lsp_store| {
102 let Some(lsp_store) = lsp_store.upgrade() else {
103 return false;
104 };
105 project::lsp_store::json_language_server_ext::notify_schema_changed(
106 lsp_store,
107 uri.clone(),
108 cx,
109 );
110 true
111 })
112 }
113}
114
115pub fn handle_schema_request(
116 lsp_store: Entity<LspStore>,
117 uri: String,
118 cx: &mut AsyncApp,
119) -> Task<Result<String>> {
120 let path = match uri.strip_prefix(SCHEMA_URI_PREFIX) {
121 Some(path) => path,
122 None => return Task::ready(Err(anyhow::anyhow!("Invalid schema URI: {}", uri))),
123 };
124
125 if let Some(json) = resolve_static_schema(path) {
126 return Task::ready(Ok(json));
127 }
128
129 if let Some(cached) = DYNAMIC_SCHEMA_CACHE.read().get(&uri).cloned() {
130 return Task::ready(Ok(cached));
131 }
132
133 let path = path.to_string();
134 let uri_clone = uri.clone();
135 cx.spawn(async move |cx| {
136 let schema = resolve_dynamic_schema(lsp_store, &path, cx).await?;
137 let json = serde_json::to_string(&schema).context("Failed to serialize schema")?;
138
139 DYNAMIC_SCHEMA_CACHE.write().insert(uri_clone, json.clone());
140
141 Ok(json)
142 })
143}
144
145fn resolve_static_schema(path: &str) -> Option<String> {
146 let (schema_name, rest) = path.split_once('/').unzip();
147 let schema_name = schema_name.unwrap_or(path);
148
149 match schema_name {
150 "tsconfig" => Some(TSCONFIG_SCHEMA.to_string()),
151 "package_json" => Some(PACKAGE_JSON_SCHEMA.to_string()),
152 "tasks" => Some(TASKS_SCHEMA.clone()),
153 "snippets" => Some(SNIPPETS_SCHEMA.clone()),
154 "jsonc" => Some(JSONC_SCHEMA.clone()),
155 "keymap" => Some(KEYMAP_SCHEMA.clone()),
156 "zed_inspector_style" => {
157 #[cfg(debug_assertions)]
158 {
159 Some(INSPECTOR_STYLE_SCHEMA.clone())
160 }
161 #[cfg(not(debug_assertions))]
162 {
163 Some(
164 serde_json::to_string(&schemars::json_schema!(true).to_value())
165 .expect("true schema should serialize"),
166 )
167 }
168 }
169
170 "action" => {
171 let normalized_action_name = match rest {
172 Some(name) => name,
173 None => return None,
174 };
175 let action_name = denormalize_action_name(normalized_action_name);
176
177 if let Some(cached) = ACTION_SCHEMA_CACHE.read().get(&action_name).cloned() {
178 return Some(cached);
179 }
180
181 let mut generator = settings::KeymapFile::action_schema_generator();
182 let schema =
183 settings::KeymapFile::get_action_schema_by_name(&action_name, &mut generator);
184 let json = serde_json::to_string(
185 &root_schema_from_action_schema(schema, &mut generator).to_value(),
186 )
187 .expect("Action schema should serialize");
188
189 ACTION_SCHEMA_CACHE
190 .write()
191 .insert(action_name, json.clone());
192 Some(json)
193 }
194
195 _ => None,
196 }
197}
198
199async fn resolve_dynamic_schema(
200 lsp_store: Entity<LspStore>,
201 path: &str,
202 cx: &mut AsyncApp,
203) -> Result<serde_json::Value> {
204 let languages = lsp_store.read_with(cx, |lsp_store, _| lsp_store.languages.clone());
205 let (schema_name, rest) = path.split_once('/').unzip();
206 let schema_name = schema_name.unwrap_or(path);
207
208 let schema = match schema_name {
209 "settings" if rest.is_some_and(|r| r.starts_with("lsp/")) => {
210 let lsp_name = rest
211 .and_then(|r| {
212 r.strip_prefix(
213 LSP_SETTINGS_SCHEMA_URL_PREFIX
214 .strip_prefix(SCHEMA_URI_PREFIX)
215 .and_then(|s| s.strip_prefix("settings/"))
216 .unwrap_or("lsp/"),
217 )
218 })
219 .context("Invalid LSP schema path")?;
220
221 let adapter = languages
222 .all_lsp_adapters()
223 .into_iter()
224 .find(|adapter| adapter.name().as_ref() as &str == lsp_name)
225 .with_context(|| format!("LSP adapter not found: {}", lsp_name))?;
226
227 let delegate: Arc<dyn LspAdapterDelegate> = cx
228 .update(|inner_cx| {
229 lsp_store.update(inner_cx, |lsp_store, cx| {
230 let Some(local) = lsp_store.as_local() else {
231 return None;
232 };
233 let Some(worktree) = local.worktree_store.read(cx).worktrees().next()
234 else {
235 return None;
236 };
237 Some(LocalLspAdapterDelegate::from_local_lsp(
238 local, &worktree, cx,
239 ))
240 })
241 })
242 .context(concat!(
243 "Failed to create adapter delegate - ",
244 "either LSP store is not in local mode or no worktree is available"
245 ))?;
246
247 adapter
248 .initialization_options_schema(&delegate, cx)
249 .await
250 .unwrap_or_else(|| {
251 serde_json::json!({
252 "type": "object",
253 "additionalProperties": true
254 })
255 })
256 }
257 "settings" => {
258 let lsp_adapter_names = languages
259 .all_lsp_adapters()
260 .into_iter()
261 .map(|adapter| adapter.name().to_string())
262 .collect::<Vec<_>>();
263
264 cx.update(|cx| {
265 let font_names = &cx.text_system().all_font_names();
266 let language_names = &languages
267 .language_names()
268 .into_iter()
269 .map(|name| name.to_string())
270 .collect::<Vec<_>>();
271
272 let mut icon_theme_names = vec![];
273 let mut theme_names = vec![];
274 if let Some(registry) = theme::ThemeRegistry::try_global(cx) {
275 icon_theme_names.extend(
276 registry
277 .list_icon_themes()
278 .into_iter()
279 .map(|icon_theme| icon_theme.name),
280 );
281 theme_names.extend(registry.list_names());
282 }
283 let icon_theme_names = icon_theme_names.as_slice();
284 let theme_names = theme_names.as_slice();
285
286 cx.global::<settings::SettingsStore>().json_schema(
287 &settings::SettingsJsonSchemaParams {
288 language_names,
289 font_names,
290 theme_names,
291 icon_theme_names,
292 lsp_adapter_names: &lsp_adapter_names,
293 },
294 )
295 })
296 }
297 "debug_tasks" => {
298 let adapter_schemas = cx.read_global::<dap::DapRegistry, _>(|dap_registry, _| {
299 dap_registry.adapters_schema()
300 });
301 task::DebugTaskFile::generate_json_schema(&adapter_schemas)
302 }
303 _ => {
304 anyhow::bail!("Unrecognized schema: {schema_name}");
305 }
306 };
307 Ok(schema)
308}
309
310const JSONC_LANGUAGE_NAME: &str = "JSONC";
311
312pub fn all_schema_file_associations(
313 languages: &Arc<LanguageRegistry>,
314 cx: &mut App,
315) -> serde_json::Value {
316 let extension_globs = languages
317 .available_language_for_name(JSONC_LANGUAGE_NAME)
318 .map(|language| language.matcher().path_suffixes.clone())
319 .into_iter()
320 .flatten()
321 // Path suffixes can be entire file names or just their extensions.
322 .flat_map(|path_suffix| [format!("*.{path_suffix}"), path_suffix]);
323 let override_globs = all_language_settings(None, cx)
324 .file_types
325 .get(JSONC_LANGUAGE_NAME)
326 .into_iter()
327 .flat_map(|(_, glob_strings)| glob_strings)
328 .cloned();
329 let jsonc_globs = extension_globs.chain(override_globs).collect::<Vec<_>>();
330
331 let mut file_associations = serde_json::json!([
332 {
333 "fileMatch": [
334 schema_file_match(paths::settings_file()),
335 paths::local_settings_file_relative_path()
336 ],
337 "url": format!("{SCHEMA_URI_PREFIX}settings"),
338 },
339 {
340 "fileMatch": [schema_file_match(paths::keymap_file())],
341 "url": format!("{SCHEMA_URI_PREFIX}keymap"),
342 },
343 {
344 "fileMatch": [
345 schema_file_match(paths::tasks_file()),
346 paths::local_tasks_file_relative_path()
347 ],
348 "url": format!("{SCHEMA_URI_PREFIX}tasks"),
349 },
350 {
351 "fileMatch": [
352 schema_file_match(paths::debug_scenarios_file()),
353 paths::local_debug_file_relative_path()
354 ],
355 "url": format!("{SCHEMA_URI_PREFIX}debug_tasks"),
356 },
357 {
358 "fileMatch": [
359 schema_file_match(
360 paths::snippets_dir()
361 .join("*.json")
362 .as_path()
363 )
364 ],
365 "url": format!("{SCHEMA_URI_PREFIX}snippets"),
366 },
367 {
368 "fileMatch": ["tsconfig.json"],
369 "url": format!("{SCHEMA_URI_PREFIX}tsconfig")
370 },
371 {
372 "fileMatch": ["package.json"],
373 "url": format!("{SCHEMA_URI_PREFIX}package_json")
374 },
375 {
376 "fileMatch": &jsonc_globs,
377 "url": format!("{SCHEMA_URI_PREFIX}jsonc")
378 },
379 ]);
380
381 #[cfg(debug_assertions)]
382 {
383 file_associations
384 .as_array_mut()
385 .unwrap()
386 .push(serde_json::json!({
387 "fileMatch": [
388 "zed-inspector-style.json"
389 ],
390 "url": format!("{SCHEMA_URI_PREFIX}zed_inspector_style")
391 }));
392 }
393
394 file_associations
395 .as_array_mut()
396 .unwrap()
397 .extend(cx.all_action_names().into_iter().map(|&name| {
398 let normalized_name = normalize_action_name(name);
399 let file_name = normalized_action_name_to_file_name(normalized_name.clone());
400 serde_json::json!({
401 "fileMatch": [file_name],
402 "url": format!("{}action/{normalized_name}", SCHEMA_URI_PREFIX)
403 })
404 }));
405
406 file_associations
407}
408
409fn generate_jsonc_schema() -> serde_json::Value {
410 let generator = schemars::generate::SchemaSettings::draft2019_09()
411 .with_transform(DefaultDenyUnknownFields)
412 .with_transform(AllowTrailingCommas)
413 .into_generator();
414 let meta_schema = generator
415 .settings()
416 .meta_schema
417 .as_ref()
418 .expect("meta_schema should be present in schemars settings")
419 .to_string();
420 let defs = generator.definitions();
421 let schema = schemars::json_schema!({
422 "$schema": meta_schema,
423 "allowTrailingCommas": true,
424 "$defs": defs,
425 });
426 serde_json::to_value(schema).unwrap()
427}
428
429#[cfg(debug_assertions)]
430fn generate_inspector_style_schema() -> serde_json::Value {
431 let schema = schemars::generate::SchemaSettings::draft2019_09()
432 .with_transform(util::schemars::DefaultDenyUnknownFields)
433 .into_generator()
434 .root_schema_for::<gpui::StyleRefinement>();
435
436 serde_json::to_value(schema).unwrap()
437}
438
439pub fn normalize_action_name(action_name: &str) -> String {
440 action_name.replace("::", "__")
441}
442
443pub fn denormalize_action_name(action_name: &str) -> String {
444 action_name.replace("__", "::")
445}
446
447pub fn normalized_action_file_name(action_name: &str) -> String {
448 normalized_action_name_to_file_name(normalize_action_name(action_name))
449}
450
451pub fn normalized_action_name_to_file_name(mut normalized_action_name: String) -> String {
452 normalized_action_name.push_str(".json");
453 normalized_action_name
454}
455
456fn root_schema_from_action_schema(
457 action_schema: Option<schemars::Schema>,
458 generator: &mut schemars::SchemaGenerator,
459) -> schemars::Schema {
460 let Some(mut action_schema) = action_schema else {
461 return schemars::json_schema!(false);
462 };
463 let meta_schema = generator
464 .settings()
465 .meta_schema
466 .as_ref()
467 .expect("meta_schema should be present in schemars settings")
468 .to_string();
469 let defs = generator.definitions();
470 let mut schema = schemars::json_schema!({
471 "$schema": meta_schema,
472 "allowTrailingCommas": true,
473 "$defs": defs,
474 });
475 schema
476 .ensure_object()
477 .extend(std::mem::take(action_schema.ensure_object()));
478 schema
479}
480
481#[inline]
482fn schema_file_match(path: &std::path::Path) -> String {
483 path.strip_prefix(path.parent().unwrap().parent().unwrap())
484 .unwrap()
485 .display()
486 .to_string()
487 .replace('\\', "/")
488}