json_schema_store.rs

  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}