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