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