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::{
7 LanguageRegistry, LanguageServerName, LspAdapterDelegate,
8 language_settings::AllLanguageSettings,
9};
10use parking_lot::RwLock;
11use project::{LspStore, lsp_store::LocalLspAdapterDelegate};
12use settings::{LSP_SETTINGS_SCHEMA_URL_PREFIX, Settings as _, SettingsLocation};
13use util::schemars::{AllowTrailingCommas, DefaultDenyUnknownFields};
14
15const SCHEMA_URI_PREFIX: &str = "zed://schemas/";
16
17const TSCONFIG_SCHEMA: &str = include_str!("schemas/tsconfig.json");
18const PACKAGE_JSON_SCHEMA: &str = include_str!("schemas/package.json");
19
20static TASKS_SCHEMA: LazyLock<String> = LazyLock::new(|| {
21 serde_json::to_string(&task::TaskTemplates::generate_json_schema())
22 .expect("TaskTemplates schema should serialize")
23});
24
25static SNIPPETS_SCHEMA: LazyLock<String> = LazyLock::new(|| {
26 serde_json::to_string(&snippet_provider::format::VsSnippetsFile::generate_json_schema())
27 .expect("VsSnippetsFile schema should serialize")
28});
29
30static JSONC_SCHEMA: LazyLock<String> = LazyLock::new(|| {
31 serde_json::to_string(&generate_jsonc_schema()).expect("JSONC schema should serialize")
32});
33
34#[cfg(debug_assertions)]
35static INSPECTOR_STYLE_SCHEMA: LazyLock<String> = LazyLock::new(|| {
36 serde_json::to_string(&generate_inspector_style_schema())
37 .expect("Inspector style schema should serialize")
38});
39
40static KEYMAP_SCHEMA: LazyLock<String> = LazyLock::new(|| {
41 serde_json::to_string(&settings::KeymapFile::generate_json_schema_from_inventory())
42 .expect("Keymap schema should serialize")
43});
44
45static ACTION_SCHEMA_CACHE: LazyLock<RwLock<HashMap<String, String>>> =
46 LazyLock::new(|| RwLock::new(HashMap::default()));
47
48// Runtime cache for dynamic schemas that depend on runtime state:
49// - "settings": depends on installed fonts, themes, languages, LSP adapters (extensions can add these)
50// - "settings/lsp/*": depends on LSP adapter initialization options
51// - "debug_tasks": depends on DAP adapters (extensions can add these)
52// Cache is invalidated via notify_schema_changed() when extensions or DAP registry change.
53static DYNAMIC_SCHEMA_CACHE: LazyLock<RwLock<HashMap<String, String>>> =
54 LazyLock::new(|| RwLock::new(HashMap::default()));
55
56pub fn init(cx: &mut App) {
57 cx.set_global(SchemaStore::default());
58 project::lsp_store::json_language_server_ext::register_schema_handler(
59 handle_schema_request,
60 cx,
61 );
62
63 cx.observe_new(|_, _, cx| {
64 let lsp_store = cx.weak_entity();
65 cx.global_mut::<SchemaStore>().lsp_stores.push(lsp_store);
66 })
67 .detach();
68
69 if let Some(extension_events) = extension::ExtensionEvents::try_global(cx) {
70 cx.subscribe(&extension_events, move |_, evt, cx| match evt {
71 extension::Event::ExtensionsInstalledChanged => {
72 cx.update_global::<SchemaStore, _>(|schema_store, cx| {
73 schema_store.notify_schema_changed(ChangedSchemas::Settings, cx);
74 });
75 }
76 extension::Event::ExtensionUninstalled(_)
77 | extension::Event::ExtensionInstalled(_)
78 | extension::Event::ConfigureExtensionRequested(_) => {}
79 })
80 .detach();
81 }
82
83 cx.observe_global::<dap::DapRegistry>(move |cx| {
84 cx.update_global::<SchemaStore, _>(|schema_store, cx| {
85 schema_store.notify_schema_changed(ChangedSchemas::DebugTasks, cx);
86 });
87 })
88 .detach();
89}
90
91#[derive(Default)]
92pub struct SchemaStore {
93 lsp_stores: Vec<WeakEntity<LspStore>>,
94}
95
96impl gpui::Global for SchemaStore {}
97
98enum ChangedSchemas {
99 Settings,
100 DebugTasks,
101}
102
103impl SchemaStore {
104 fn notify_schema_changed(&mut self, changed_schemas: ChangedSchemas, cx: &mut App) {
105 let uris_to_invalidate = match changed_schemas {
106 ChangedSchemas::Settings => {
107 let settings_uri_prefix = &format!("{SCHEMA_URI_PREFIX}settings");
108 let project_settings_uri = &format!("{SCHEMA_URI_PREFIX}project_settings");
109 DYNAMIC_SCHEMA_CACHE
110 .write()
111 .extract_if(|uri, _| {
112 uri == project_settings_uri || uri.starts_with(settings_uri_prefix)
113 })
114 .map(|(url, _)| url)
115 .collect()
116 }
117 ChangedSchemas::DebugTasks => DYNAMIC_SCHEMA_CACHE
118 .write()
119 .remove_entry(&format!("{SCHEMA_URI_PREFIX}debug_tasks"))
120 .map_or_else(Vec::new, |(uri, _)| vec![uri]),
121 };
122
123 if uris_to_invalidate.is_empty() {
124 return;
125 }
126
127 self.lsp_stores.retain(|lsp_store| {
128 let Some(lsp_store) = lsp_store.upgrade() else {
129 return false;
130 };
131 project::lsp_store::json_language_server_ext::notify_schemas_changed(
132 lsp_store,
133 &uris_to_invalidate,
134 cx,
135 );
136 true
137 })
138 }
139}
140
141pub fn handle_schema_request(
142 lsp_store: Entity<LspStore>,
143 uri: String,
144 cx: &mut AsyncApp,
145) -> Task<Result<String>> {
146 let path = match uri.strip_prefix(SCHEMA_URI_PREFIX) {
147 Some(path) => path,
148 None => return Task::ready(Err(anyhow::anyhow!("Invalid schema URI: {}", uri))),
149 };
150
151 if let Some(json) = resolve_static_schema(path) {
152 return Task::ready(Ok(json));
153 }
154
155 if let Some(cached) = DYNAMIC_SCHEMA_CACHE.read().get(&uri).cloned() {
156 return Task::ready(Ok(cached));
157 }
158
159 let path = path.to_string();
160 let uri_clone = uri.clone();
161 cx.spawn(async move |cx| {
162 let schema = resolve_dynamic_schema(lsp_store, &path, cx).await?;
163 let json = serde_json::to_string(&schema).context("Failed to serialize schema")?;
164
165 DYNAMIC_SCHEMA_CACHE.write().insert(uri_clone, json.clone());
166
167 Ok(json)
168 })
169}
170
171fn resolve_static_schema(path: &str) -> Option<String> {
172 let (schema_name, rest) = path.split_once('/').unzip();
173 let schema_name = schema_name.unwrap_or(path);
174
175 match schema_name {
176 "tsconfig" => Some(TSCONFIG_SCHEMA.to_string()),
177 "package_json" => Some(PACKAGE_JSON_SCHEMA.to_string()),
178 "tasks" => Some(TASKS_SCHEMA.clone()),
179 "snippets" => Some(SNIPPETS_SCHEMA.clone()),
180 "jsonc" => Some(JSONC_SCHEMA.clone()),
181 "keymap" => Some(KEYMAP_SCHEMA.clone()),
182 "zed_inspector_style" => {
183 #[cfg(debug_assertions)]
184 {
185 Some(INSPECTOR_STYLE_SCHEMA.clone())
186 }
187 #[cfg(not(debug_assertions))]
188 {
189 Some(
190 serde_json::to_string(&schemars::json_schema!(true).to_value())
191 .expect("true schema should serialize"),
192 )
193 }
194 }
195
196 "action" => {
197 let normalized_action_name = match rest {
198 Some(name) => name,
199 None => return None,
200 };
201 let action_name = denormalize_action_name(normalized_action_name);
202
203 if let Some(cached) = ACTION_SCHEMA_CACHE.read().get(&action_name).cloned() {
204 return Some(cached);
205 }
206
207 let mut generator = settings::KeymapFile::action_schema_generator();
208 let schema =
209 settings::KeymapFile::get_action_schema_by_name(&action_name, &mut generator);
210 let json = serde_json::to_string(
211 &root_schema_from_action_schema(schema, &mut generator).to_value(),
212 )
213 .expect("Action schema should serialize");
214
215 ACTION_SCHEMA_CACHE
216 .write()
217 .insert(action_name, json.clone());
218 Some(json)
219 }
220
221 _ => None,
222 }
223}
224
225async fn resolve_dynamic_schema(
226 lsp_store: Entity<LspStore>,
227 path: &str,
228 cx: &mut AsyncApp,
229) -> Result<serde_json::Value> {
230 let languages = lsp_store.read_with(cx, |lsp_store, _| lsp_store.languages.clone());
231 let (schema_name, rest) = path.split_once('/').unzip();
232 let schema_name = schema_name.unwrap_or(path);
233
234 let schema = match schema_name {
235 "settings" if rest.is_some_and(|r| r.starts_with("lsp/")) => {
236 let lsp_path = rest
237 .and_then(|r| {
238 r.strip_prefix(
239 LSP_SETTINGS_SCHEMA_URL_PREFIX
240 .strip_prefix(SCHEMA_URI_PREFIX)
241 .and_then(|s| s.strip_prefix("settings/"))
242 .unwrap_or("lsp/"),
243 )
244 })
245 .context("Invalid LSP schema path")?;
246
247 // Parse the schema type from the path:
248 // - "rust-analyzer/initialization_options" → initialization_options_schema
249 // - "rust-analyzer/settings" → settings_schema
250 enum LspSchemaKind {
251 InitializationOptions,
252 Settings,
253 }
254 let (lsp_name, schema_kind) = if let Some(adapter_name) =
255 lsp_path.strip_suffix("/initialization_options")
256 {
257 (adapter_name, LspSchemaKind::InitializationOptions)
258 } else if let Some(adapter_name) = lsp_path.strip_suffix("/settings") {
259 (adapter_name, LspSchemaKind::Settings)
260 } else {
261 anyhow::bail!(
262 "Invalid LSP schema path: \
263 Expected '{{adapter}}/initialization_options' or '{{adapter}}/settings', got '{}'",
264 lsp_path
265 );
266 };
267
268 let adapter = languages
269 .all_lsp_adapters()
270 .into_iter()
271 .find(|adapter| adapter.name().as_ref() as &str == lsp_name)
272 .or_else(|| {
273 languages.load_available_lsp_adapter(&LanguageServerName::from(lsp_name))
274 })
275 .with_context(|| format!("LSP adapter not found: {}", lsp_name))?;
276
277 let delegate: Arc<dyn LspAdapterDelegate> = cx
278 .update(|inner_cx| {
279 lsp_store.update(inner_cx, |lsp_store, cx| {
280 let Some(local) = lsp_store.as_local() else {
281 return None;
282 };
283 let Some(worktree) = local.worktree_store.read(cx).worktrees().next()
284 else {
285 return None;
286 };
287 Some(LocalLspAdapterDelegate::from_local_lsp(
288 local, &worktree, cx,
289 ))
290 })
291 })
292 .context(concat!(
293 "Failed to create adapter delegate - ",
294 "either LSP store is not in local mode or no worktree is available"
295 ))?;
296
297 let schema = match schema_kind {
298 LspSchemaKind::InitializationOptions => {
299 adapter.initialization_options_schema(&delegate, cx).await
300 }
301 LspSchemaKind::Settings => adapter.settings_schema(&delegate, cx).await,
302 };
303
304 schema.unwrap_or_else(|| {
305 serde_json::json!({
306 "type": "object",
307 "additionalProperties": true
308 })
309 })
310 }
311 "settings" => {
312 let mut lsp_adapter_names: Vec<String> = languages
313 .all_lsp_adapters()
314 .into_iter()
315 .map(|adapter| adapter.name())
316 .chain(languages.available_lsp_adapter_names().into_iter())
317 .map(|name| name.to_string())
318 .collect();
319
320 let mut i = 0;
321 while i < lsp_adapter_names.len() {
322 let mut j = i + 1;
323 while j < lsp_adapter_names.len() {
324 if lsp_adapter_names[i] == lsp_adapter_names[j] {
325 lsp_adapter_names.swap_remove(j);
326 } else {
327 j += 1;
328 }
329 }
330 i += 1;
331 }
332
333 cx.update(|cx| {
334 let font_names = &cx.text_system().all_font_names();
335 let language_names = &languages
336 .language_names()
337 .into_iter()
338 .map(|name| name.to_string())
339 .collect::<Vec<_>>();
340
341 let mut icon_theme_names = vec![];
342 let mut theme_names = vec![];
343 if let Some(registry) = theme::ThemeRegistry::try_global(cx) {
344 icon_theme_names.extend(
345 registry
346 .list_icon_themes()
347 .into_iter()
348 .map(|icon_theme| icon_theme.name),
349 );
350 theme_names.extend(registry.list_names());
351 }
352 let icon_theme_names = icon_theme_names.as_slice();
353 let theme_names = theme_names.as_slice();
354
355 let action_names = cx.all_action_names();
356 let action_documentation = cx.action_documentation();
357 let deprecations = cx.deprecated_actions_to_preferred_actions();
358 let deprecation_messages = cx.action_deprecation_messages();
359
360 let mut schema =
361 settings::SettingsStore::json_schema(&settings::SettingsJsonSchemaParams {
362 language_names,
363 font_names,
364 theme_names,
365 icon_theme_names,
366 lsp_adapter_names: &lsp_adapter_names,
367 action_names,
368 action_documentation,
369 deprecations,
370 deprecation_messages,
371 });
372 inject_feature_flags_schema(&mut schema);
373 schema
374 })
375 }
376 "project_settings" => {
377 let lsp_adapter_names = languages
378 .all_lsp_adapters()
379 .into_iter()
380 .map(|adapter| adapter.name().to_string())
381 .collect::<Vec<_>>();
382
383 let language_names = &languages
384 .language_names()
385 .into_iter()
386 .map(|name| name.to_string())
387 .collect::<Vec<_>>();
388
389 let mut schema =
390 settings::SettingsStore::project_json_schema(&settings::SettingsJsonSchemaParams {
391 language_names,
392 lsp_adapter_names: &lsp_adapter_names,
393 // These are not allowed in project-specific settings but
394 // they're still fields required by the
395 // `SettingsJsonSchemaParams` struct.
396 font_names: &[],
397 theme_names: &[],
398 icon_theme_names: &[],
399 action_names: &[],
400 action_documentation: &HashMap::default(),
401 deprecations: &HashMap::default(),
402 deprecation_messages: &HashMap::default(),
403 });
404 inject_feature_flags_schema(&mut schema);
405 schema
406 }
407 "debug_tasks" => {
408 let adapter_schemas = cx.read_global::<dap::DapRegistry, _>(|dap_registry, _| {
409 dap_registry.adapters_schema()
410 });
411 task::DebugTaskFile::generate_json_schema(&adapter_schemas)
412 }
413 "keymap" => cx.update(settings::KeymapFile::generate_json_schema_for_registered_actions),
414 "action" => {
415 let normalized_action_name = rest.context("No Action name provided")?;
416 let action_name = denormalize_action_name(normalized_action_name);
417 let mut generator = settings::KeymapFile::action_schema_generator();
418 let schema = cx
419 .update(|cx| cx.action_schema_by_name(&action_name, &mut generator))
420 .flatten();
421 root_schema_from_action_schema(schema, &mut generator).to_value()
422 }
423 "tasks" => task::TaskTemplates::generate_json_schema(),
424 _ => {
425 anyhow::bail!("Unrecognized schema: {schema_name}");
426 }
427 };
428 Ok(schema)
429}
430
431const JSONC_LANGUAGE_NAME: &str = "JSONC";
432
433pub fn all_schema_file_associations(
434 languages: &Arc<LanguageRegistry>,
435 path: Option<SettingsLocation<'_>>,
436 cx: &mut App,
437) -> serde_json::Value {
438 let extension_globs = languages
439 .available_language_for_name(JSONC_LANGUAGE_NAME)
440 .map(|language| language.matcher().path_suffixes.clone())
441 .into_iter()
442 .flatten()
443 // Path suffixes can be entire file names or just their extensions.
444 .flat_map(|path_suffix| [format!("*.{path_suffix}"), path_suffix]);
445 let override_globs = AllLanguageSettings::get(path, cx)
446 .file_types
447 .get(JSONC_LANGUAGE_NAME)
448 .into_iter()
449 .flat_map(|(_, glob_strings)| glob_strings)
450 .cloned();
451 let jsonc_globs = extension_globs.chain(override_globs).collect::<Vec<_>>();
452
453 let mut file_associations = serde_json::json!([
454 {
455 "fileMatch": [
456 schema_file_match(paths::settings_file()),
457 ],
458 "url": format!("{SCHEMA_URI_PREFIX}settings"),
459 },
460 {
461 "fileMatch": [
462 paths::local_settings_file_relative_path()],
463 "url": format!("{SCHEMA_URI_PREFIX}project_settings"),
464 },
465 {
466 "fileMatch": [schema_file_match(paths::keymap_file())],
467 "url": format!("{SCHEMA_URI_PREFIX}keymap"),
468 },
469 {
470 "fileMatch": [
471 schema_file_match(paths::tasks_file()),
472 paths::local_tasks_file_relative_path()
473 ],
474 "url": format!("{SCHEMA_URI_PREFIX}tasks"),
475 },
476 {
477 "fileMatch": [
478 schema_file_match(paths::debug_scenarios_file()),
479 paths::local_debug_file_relative_path()
480 ],
481 "url": format!("{SCHEMA_URI_PREFIX}debug_tasks"),
482 },
483 {
484 "fileMatch": [
485 schema_file_match(
486 paths::snippets_dir()
487 .join("*.json")
488 .as_path()
489 )
490 ],
491 "url": format!("{SCHEMA_URI_PREFIX}snippets"),
492 },
493 {
494 "fileMatch": ["tsconfig.json"],
495 "url": format!("{SCHEMA_URI_PREFIX}tsconfig")
496 },
497 {
498 "fileMatch": ["package.json"],
499 "url": format!("{SCHEMA_URI_PREFIX}package_json")
500 },
501 {
502 "fileMatch": &jsonc_globs,
503 "url": format!("{SCHEMA_URI_PREFIX}jsonc")
504 },
505 ]);
506
507 #[cfg(debug_assertions)]
508 {
509 file_associations
510 .as_array_mut()
511 .unwrap()
512 .push(serde_json::json!({
513 "fileMatch": [
514 "zed-inspector-style.json"
515 ],
516 "url": format!("{SCHEMA_URI_PREFIX}zed_inspector_style")
517 }));
518 }
519
520 file_associations
521 .as_array_mut()
522 .unwrap()
523 .extend(cx.all_action_names().into_iter().map(|&name| {
524 let normalized_name = normalize_action_name(name);
525 let file_name = normalized_action_name_to_file_name(normalized_name.clone());
526 serde_json::json!({
527 "fileMatch": [file_name],
528 "url": format!("{SCHEMA_URI_PREFIX}action/{normalized_name}")
529 })
530 }));
531
532 file_associations
533}
534
535/// Swaps the placeholder [`settings::FeatureFlagsMap`] subschema produced by
536/// schemars for an enriched one that lists each known flag's variants. The
537/// placeholder is registered in the `settings_content` crate so the
538/// `settings` crate doesn't need a reverse dependency on `feature_flags`.
539fn inject_feature_flags_schema(schema: &mut serde_json::Value) {
540 use schemars::JsonSchema;
541
542 let Some(defs) = schema.get_mut("$defs").and_then(|d| d.as_object_mut()) else {
543 return;
544 };
545 let schema_name = settings::FeatureFlagsMap::schema_name();
546 let enriched = feature_flags::generate_feature_flags_schema().to_value();
547 defs.insert(schema_name.into_owned(), enriched);
548}
549
550fn generate_jsonc_schema() -> serde_json::Value {
551 let generator = schemars::generate::SchemaSettings::draft2019_09()
552 .with_transform(DefaultDenyUnknownFields)
553 .with_transform(AllowTrailingCommas)
554 .into_generator();
555 let meta_schema = generator
556 .settings()
557 .meta_schema
558 .as_ref()
559 .expect("meta_schema should be present in schemars settings")
560 .to_string();
561 let defs = generator.definitions();
562 let schema = schemars::json_schema!({
563 "$schema": meta_schema,
564 "allowTrailingCommas": true,
565 "$defs": defs,
566 });
567 serde_json::to_value(schema).unwrap()
568}
569
570#[cfg(debug_assertions)]
571fn generate_inspector_style_schema() -> serde_json::Value {
572 let schema = schemars::generate::SchemaSettings::draft2019_09()
573 .with_transform(util::schemars::DefaultDenyUnknownFields)
574 .into_generator()
575 .root_schema_for::<gpui::StyleRefinement>();
576
577 serde_json::to_value(schema).unwrap()
578}
579
580pub fn normalize_action_name(action_name: &str) -> String {
581 action_name.replace("::", "__")
582}
583
584pub fn denormalize_action_name(action_name: &str) -> String {
585 action_name.replace("__", "::")
586}
587
588pub fn normalized_action_file_name(action_name: &str) -> String {
589 normalized_action_name_to_file_name(normalize_action_name(action_name))
590}
591
592pub fn normalized_action_name_to_file_name(mut normalized_action_name: String) -> String {
593 normalized_action_name.push_str(".json");
594 normalized_action_name
595}
596
597fn root_schema_from_action_schema(
598 action_schema: Option<schemars::Schema>,
599 generator: &mut schemars::SchemaGenerator,
600) -> schemars::Schema {
601 let Some(mut action_schema) = action_schema else {
602 return schemars::json_schema!(false);
603 };
604 let meta_schema = generator
605 .settings()
606 .meta_schema
607 .as_ref()
608 .expect("meta_schema should be present in schemars settings")
609 .to_string();
610 let defs = generator.definitions();
611 let mut schema = schemars::json_schema!({
612 "$schema": meta_schema,
613 "allowTrailingCommas": true,
614 "$defs": defs,
615 });
616 schema
617 .ensure_object()
618 .extend(std::mem::take(action_schema.ensure_object()));
619 schema
620}
621
622#[inline]
623fn schema_file_match(path: &std::path::Path) -> String {
624 path.strip_prefix(path.parent().unwrap().parent().unwrap())
625 .unwrap()
626 .display()
627 .to_string()
628 .replace('\\', "/")
629}