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