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