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::{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}