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_name = 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            let adapter = languages
224                .all_lsp_adapters()
225                .into_iter()
226                .find(|adapter| adapter.name().as_ref() as &str == lsp_name)
227                .with_context(|| format!("LSP adapter not found: {}", lsp_name))?;
228
229            let delegate: Arc<dyn LspAdapterDelegate> = cx
230                .update(|inner_cx| {
231                    lsp_store.update(inner_cx, |lsp_store, cx| {
232                        let Some(local) = lsp_store.as_local() else {
233                            return None;
234                        };
235                        let Some(worktree) = local.worktree_store.read(cx).worktrees().next()
236                        else {
237                            return None;
238                        };
239                        Some(LocalLspAdapterDelegate::from_local_lsp(
240                            local, &worktree, cx,
241                        ))
242                    })
243                })
244                .context(concat!(
245                    "Failed to create adapter delegate - ",
246                    "either LSP store is not in local mode or no worktree is available"
247                ))?;
248
249            adapter
250                .initialization_options_schema(&delegate, cx)
251                .await
252                .unwrap_or_else(|| {
253                    serde_json::json!({
254                        "type": "object",
255                        "additionalProperties": true
256                    })
257                })
258        }
259        "settings" => {
260            let lsp_adapter_names = languages
261                .all_lsp_adapters()
262                .into_iter()
263                .map(|adapter| adapter.name().to_string())
264                .collect::<Vec<_>>();
265
266            cx.update(|cx| {
267                let font_names = &cx.text_system().all_font_names();
268                let language_names = &languages
269                    .language_names()
270                    .into_iter()
271                    .map(|name| name.to_string())
272                    .collect::<Vec<_>>();
273
274                let mut icon_theme_names = vec![];
275                let mut theme_names = vec![];
276                if let Some(registry) = theme::ThemeRegistry::try_global(cx) {
277                    icon_theme_names.extend(
278                        registry
279                            .list_icon_themes()
280                            .into_iter()
281                            .map(|icon_theme| icon_theme.name),
282                    );
283                    theme_names.extend(registry.list_names());
284                }
285                let icon_theme_names = icon_theme_names.as_slice();
286                let theme_names = theme_names.as_slice();
287
288                cx.global::<settings::SettingsStore>().json_schema(
289                    &settings::SettingsJsonSchemaParams {
290                        language_names,
291                        font_names,
292                        theme_names,
293                        icon_theme_names,
294                        lsp_adapter_names: &lsp_adapter_names,
295                    },
296                )
297            })
298        }
299        "project_settings" => {
300            let lsp_adapter_names = languages
301                .all_lsp_adapters()
302                .into_iter()
303                .map(|adapter| adapter.name().to_string())
304                .collect::<Vec<_>>();
305
306            cx.update(|cx| {
307                let language_names = &languages
308                    .language_names()
309                    .into_iter()
310                    .map(|name| name.to_string())
311                    .collect::<Vec<_>>();
312
313                cx.global::<settings::SettingsStore>().project_json_schema(
314                    &settings::SettingsJsonSchemaParams {
315                        language_names,
316                        lsp_adapter_names: &lsp_adapter_names,
317                        // These are not allowed in project-specific settings but
318                        // they're still fields required by the
319                        // `SettingsJsonSchemaParams` struct.
320                        font_names: &[],
321                        theme_names: &[],
322                        icon_theme_names: &[],
323                    },
324                )
325            })
326        }
327        "debug_tasks" => {
328            let adapter_schemas = cx.read_global::<dap::DapRegistry, _>(|dap_registry, _| {
329                dap_registry.adapters_schema()
330            });
331            task::DebugTaskFile::generate_json_schema(&adapter_schemas)
332        }
333        "keymap" => cx.update(settings::KeymapFile::generate_json_schema_for_registered_actions),
334        "action" => {
335            let normalized_action_name = rest.context("No Action name provided")?;
336            let action_name = denormalize_action_name(normalized_action_name);
337            let mut generator = settings::KeymapFile::action_schema_generator();
338            let schema = cx
339                .update(|cx| cx.action_schema_by_name(&action_name, &mut generator))
340                .flatten();
341            root_schema_from_action_schema(schema, &mut generator).to_value()
342        }
343        "tasks" => task::TaskTemplates::generate_json_schema(),
344        _ => {
345            anyhow::bail!("Unrecognized schema: {schema_name}");
346        }
347    };
348    Ok(schema)
349}
350
351const JSONC_LANGUAGE_NAME: &str = "JSONC";
352
353pub fn all_schema_file_associations(
354    languages: &Arc<LanguageRegistry>,
355    path: Option<SettingsLocation<'_>>,
356    cx: &mut App,
357) -> serde_json::Value {
358    let extension_globs = languages
359        .available_language_for_name(JSONC_LANGUAGE_NAME)
360        .map(|language| language.matcher().path_suffixes.clone())
361        .into_iter()
362        .flatten()
363        // Path suffixes can be entire file names or just their extensions.
364        .flat_map(|path_suffix| [format!("*.{path_suffix}"), path_suffix]);
365    let override_globs = AllLanguageSettings::get(path, cx)
366        .file_types
367        .get(JSONC_LANGUAGE_NAME)
368        .into_iter()
369        .flat_map(|(_, glob_strings)| glob_strings)
370        .cloned();
371    let jsonc_globs = extension_globs.chain(override_globs).collect::<Vec<_>>();
372
373    let mut file_associations = serde_json::json!([
374        {
375            "fileMatch": [
376                schema_file_match(paths::settings_file()),
377            ],
378            "url": format!("{SCHEMA_URI_PREFIX}settings"),
379        },
380        {
381            "fileMatch": [
382            paths::local_settings_file_relative_path()],
383            "url": format!("{SCHEMA_URI_PREFIX}project_settings"),
384        },
385        {
386            "fileMatch": [schema_file_match(paths::keymap_file())],
387            "url": format!("{SCHEMA_URI_PREFIX}keymap"),
388        },
389        {
390            "fileMatch": [
391                schema_file_match(paths::tasks_file()),
392                paths::local_tasks_file_relative_path()
393            ],
394            "url": format!("{SCHEMA_URI_PREFIX}tasks"),
395        },
396        {
397            "fileMatch": [
398                schema_file_match(paths::debug_scenarios_file()),
399                paths::local_debug_file_relative_path()
400            ],
401            "url": format!("{SCHEMA_URI_PREFIX}debug_tasks"),
402        },
403        {
404            "fileMatch": [
405                schema_file_match(
406                    paths::snippets_dir()
407                        .join("*.json")
408                        .as_path()
409                )
410            ],
411            "url": format!("{SCHEMA_URI_PREFIX}snippets"),
412        },
413        {
414            "fileMatch": ["tsconfig.json"],
415            "url": format!("{SCHEMA_URI_PREFIX}tsconfig")
416        },
417        {
418            "fileMatch": ["package.json"],
419            "url": format!("{SCHEMA_URI_PREFIX}package_json")
420        },
421        {
422            "fileMatch": &jsonc_globs,
423            "url": format!("{SCHEMA_URI_PREFIX}jsonc")
424        },
425    ]);
426
427    #[cfg(debug_assertions)]
428    {
429        file_associations
430            .as_array_mut()
431            .unwrap()
432            .push(serde_json::json!({
433                "fileMatch": [
434                    "zed-inspector-style.json"
435                ],
436                "url": format!("{SCHEMA_URI_PREFIX}zed_inspector_style")
437            }));
438    }
439
440    file_associations
441        .as_array_mut()
442        .unwrap()
443        .extend(cx.all_action_names().into_iter().map(|&name| {
444            let normalized_name = normalize_action_name(name);
445            let file_name = normalized_action_name_to_file_name(normalized_name.clone());
446            serde_json::json!({
447                "fileMatch": [file_name],
448                "url": format!("{}action/{normalized_name}", SCHEMA_URI_PREFIX)
449            })
450        }));
451
452    file_associations
453}
454
455fn generate_jsonc_schema() -> serde_json::Value {
456    let generator = schemars::generate::SchemaSettings::draft2019_09()
457        .with_transform(DefaultDenyUnknownFields)
458        .with_transform(AllowTrailingCommas)
459        .into_generator();
460    let meta_schema = generator
461        .settings()
462        .meta_schema
463        .as_ref()
464        .expect("meta_schema should be present in schemars settings")
465        .to_string();
466    let defs = generator.definitions();
467    let schema = schemars::json_schema!({
468        "$schema": meta_schema,
469        "allowTrailingCommas": true,
470        "$defs": defs,
471    });
472    serde_json::to_value(schema).unwrap()
473}
474
475#[cfg(debug_assertions)]
476fn generate_inspector_style_schema() -> serde_json::Value {
477    let schema = schemars::generate::SchemaSettings::draft2019_09()
478        .with_transform(util::schemars::DefaultDenyUnknownFields)
479        .into_generator()
480        .root_schema_for::<gpui::StyleRefinement>();
481
482    serde_json::to_value(schema).unwrap()
483}
484
485pub fn normalize_action_name(action_name: &str) -> String {
486    action_name.replace("::", "__")
487}
488
489pub fn denormalize_action_name(action_name: &str) -> String {
490    action_name.replace("__", "::")
491}
492
493pub fn normalized_action_file_name(action_name: &str) -> String {
494    normalized_action_name_to_file_name(normalize_action_name(action_name))
495}
496
497pub fn normalized_action_name_to_file_name(mut normalized_action_name: String) -> String {
498    normalized_action_name.push_str(".json");
499    normalized_action_name
500}
501
502fn root_schema_from_action_schema(
503    action_schema: Option<schemars::Schema>,
504    generator: &mut schemars::SchemaGenerator,
505) -> schemars::Schema {
506    let Some(mut action_schema) = action_schema else {
507        return schemars::json_schema!(false);
508    };
509    let meta_schema = generator
510        .settings()
511        .meta_schema
512        .as_ref()
513        .expect("meta_schema should be present in schemars settings")
514        .to_string();
515    let defs = generator.definitions();
516    let mut schema = schemars::json_schema!({
517        "$schema": meta_schema,
518        "allowTrailingCommas": true,
519        "$defs": defs,
520    });
521    schema
522        .ensure_object()
523        .extend(std::mem::take(action_schema.ensure_object()));
524    schema
525}
526
527#[inline]
528fn schema_file_match(path: &std::path::Path) -> String {
529    path.strip_prefix(path.parent().unwrap().parent().unwrap())
530        .unwrap()
531        .display()
532        .to_string()
533        .replace('\\', "/")
534}