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 });
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 path: Option<SettingsLocation<'_>>,
315 cx: &mut App,
316) -> serde_json::Value {
317 let extension_globs = languages
318 .available_language_for_name(JSONC_LANGUAGE_NAME)
319 .map(|language| language.matcher().path_suffixes.clone())
320 .into_iter()
321 .flatten()
322 // Path suffixes can be entire file names or just their extensions.
323 .flat_map(|path_suffix| [format!("*.{path_suffix}"), path_suffix]);
324 let override_globs = AllLanguageSettings::get(path, cx)
325 .file_types
326 .get(JSONC_LANGUAGE_NAME)
327 .into_iter()
328 .flat_map(|(_, glob_strings)| glob_strings)
329 .cloned();
330 let jsonc_globs = extension_globs.chain(override_globs).collect::<Vec<_>>();
331
332 let mut file_associations = serde_json::json!([
333 {
334 "fileMatch": [
335 schema_file_match(paths::settings_file()),
336 paths::local_settings_file_relative_path()
337 ],
338 "url": format!("{SCHEMA_URI_PREFIX}settings"),
339 },
340 {
341 "fileMatch": [schema_file_match(paths::keymap_file())],
342 "url": format!("{SCHEMA_URI_PREFIX}keymap"),
343 },
344 {
345 "fileMatch": [
346 schema_file_match(paths::tasks_file()),
347 paths::local_tasks_file_relative_path()
348 ],
349 "url": format!("{SCHEMA_URI_PREFIX}tasks"),
350 },
351 {
352 "fileMatch": [
353 schema_file_match(paths::debug_scenarios_file()),
354 paths::local_debug_file_relative_path()
355 ],
356 "url": format!("{SCHEMA_URI_PREFIX}debug_tasks"),
357 },
358 {
359 "fileMatch": [
360 schema_file_match(
361 paths::snippets_dir()
362 .join("*.json")
363 .as_path()
364 )
365 ],
366 "url": format!("{SCHEMA_URI_PREFIX}snippets"),
367 },
368 {
369 "fileMatch": ["tsconfig.json"],
370 "url": format!("{SCHEMA_URI_PREFIX}tsconfig")
371 },
372 {
373 "fileMatch": ["package.json"],
374 "url": format!("{SCHEMA_URI_PREFIX}package_json")
375 },
376 {
377 "fileMatch": &jsonc_globs,
378 "url": format!("{SCHEMA_URI_PREFIX}jsonc")
379 },
380 ]);
381
382 #[cfg(debug_assertions)]
383 {
384 file_associations
385 .as_array_mut()
386 .unwrap()
387 .push(serde_json::json!({
388 "fileMatch": [
389 "zed-inspector-style.json"
390 ],
391 "url": format!("{SCHEMA_URI_PREFIX}zed_inspector_style")
392 }));
393 }
394
395 file_associations
396 .as_array_mut()
397 .unwrap()
398 .extend(cx.all_action_names().into_iter().map(|&name| {
399 let normalized_name = normalize_action_name(name);
400 let file_name = normalized_action_name_to_file_name(normalized_name.clone());
401 serde_json::json!({
402 "fileMatch": [file_name],
403 "url": format!("{}action/{normalized_name}", SCHEMA_URI_PREFIX)
404 })
405 }));
406
407 file_associations
408}
409
410fn generate_jsonc_schema() -> serde_json::Value {
411 let generator = schemars::generate::SchemaSettings::draft2019_09()
412 .with_transform(DefaultDenyUnknownFields)
413 .with_transform(AllowTrailingCommas)
414 .into_generator();
415 let meta_schema = generator
416 .settings()
417 .meta_schema
418 .as_ref()
419 .expect("meta_schema should be present in schemars settings")
420 .to_string();
421 let defs = generator.definitions();
422 let schema = schemars::json_schema!({
423 "$schema": meta_schema,
424 "allowTrailingCommas": true,
425 "$defs": defs,
426 });
427 serde_json::to_value(schema).unwrap()
428}
429
430#[cfg(debug_assertions)]
431fn generate_inspector_style_schema() -> serde_json::Value {
432 let schema = schemars::generate::SchemaSettings::draft2019_09()
433 .with_transform(util::schemars::DefaultDenyUnknownFields)
434 .into_generator()
435 .root_schema_for::<gpui::StyleRefinement>();
436
437 serde_json::to_value(schema).unwrap()
438}
439
440pub fn normalize_action_name(action_name: &str) -> String {
441 action_name.replace("::", "__")
442}
443
444pub fn denormalize_action_name(action_name: &str) -> String {
445 action_name.replace("__", "::")
446}
447
448pub fn normalized_action_file_name(action_name: &str) -> String {
449 normalized_action_name_to_file_name(normalize_action_name(action_name))
450}
451
452pub fn normalized_action_name_to_file_name(mut normalized_action_name: String) -> String {
453 normalized_action_name.push_str(".json");
454 normalized_action_name
455}
456
457fn root_schema_from_action_schema(
458 action_schema: Option<schemars::Schema>,
459 generator: &mut schemars::SchemaGenerator,
460) -> schemars::Schema {
461 let Some(mut action_schema) = action_schema else {
462 return schemars::json_schema!(false);
463 };
464 let meta_schema = generator
465 .settings()
466 .meta_schema
467 .as_ref()
468 .expect("meta_schema should be present in schemars settings")
469 .to_string();
470 let defs = generator.definitions();
471 let mut schema = schemars::json_schema!({
472 "$schema": meta_schema,
473 "allowTrailingCommas": true,
474 "$defs": defs,
475 });
476 schema
477 .ensure_object()
478 .extend(std::mem::take(action_schema.ensure_object()));
479 schema
480}
481
482#[inline]
483fn schema_file_match(path: &std::path::Path) -> String {
484 path.strip_prefix(path.parent().unwrap().parent().unwrap())
485 .unwrap()
486 .display()
487 .to_string()
488 .replace('\\', "/")
489}