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