json_schema_store.rs

  1//! # json_schema_store
  2use std::{str::FromStr, sync::Arc};
  3
  4use anyhow::{Context as _, Result};
  5use gpui::{App, AsyncApp, BorrowAppContext as _, Entity, Task, WeakEntity};
  6use language::{LanguageRegistry, language_settings::all_language_settings};
  7use lsp::LanguageServerBinaryOptions;
  8use project::{LspStore, lsp_store::LocalLspAdapterDelegate};
  9use settings::LSP_SETTINGS_SCHEMA_URL_PREFIX;
 10use util::schemars::{AllowTrailingCommas, DefaultDenyUnknownFields};
 11
 12// Origin: https://github.com/SchemaStore/schemastore
 13const TSCONFIG_SCHEMA: &str = include_str!("schemas/tsconfig.json");
 14const PACKAGE_JSON_SCHEMA: &str = include_str!("schemas/package.json");
 15
 16pub fn init(cx: &mut App) {
 17    cx.set_global(SchemaStore::default());
 18    project::lsp_store::json_language_server_ext::register_schema_handler(
 19        handle_schema_request,
 20        cx,
 21    );
 22
 23    cx.observe_new(|_, _, cx| {
 24        let lsp_store = cx.weak_entity();
 25        cx.global_mut::<SchemaStore>().lsp_stores.push(lsp_store);
 26    })
 27    .detach();
 28
 29    if let Some(extension_events) = extension::ExtensionEvents::try_global(cx) {
 30        cx.subscribe(&extension_events, |_, evt, cx| {
 31            match evt {
 32                extension::Event::ExtensionInstalled(_)
 33                | extension::Event::ExtensionUninstalled(_)
 34                | extension::Event::ConfigureExtensionRequested(_) => return,
 35                extension::Event::ExtensionsInstalledChanged => {}
 36            }
 37            cx.update_global::<SchemaStore, _>(|schema_store, cx| {
 38                schema_store.notify_schema_changed("zed://schemas/settings", cx);
 39            });
 40        })
 41        .detach();
 42    }
 43
 44    cx.observe_global::<dap::DapRegistry>(|cx| {
 45        cx.update_global::<SchemaStore, _>(|schema_store, cx| {
 46            schema_store.notify_schema_changed("zed://schemas/debug_tasks", cx);
 47        });
 48    })
 49    .detach();
 50}
 51
 52#[derive(Default)]
 53pub struct SchemaStore {
 54    lsp_stores: Vec<WeakEntity<LspStore>>,
 55}
 56
 57impl gpui::Global for SchemaStore {}
 58
 59impl SchemaStore {
 60    fn notify_schema_changed(&mut self, uri: &str, cx: &mut App) {
 61        let uri = uri.to_string();
 62        self.lsp_stores.retain(|lsp_store| {
 63            let Some(lsp_store) = lsp_store.upgrade() else {
 64                return false;
 65            };
 66            project::lsp_store::json_language_server_ext::notify_schema_changed(
 67                lsp_store,
 68                uri.clone(),
 69                cx,
 70            );
 71            true
 72        })
 73    }
 74}
 75
 76fn handle_schema_request(
 77    lsp_store: Entity<LspStore>,
 78    uri: String,
 79    cx: &mut AsyncApp,
 80) -> Task<Result<String>> {
 81    let languages = lsp_store.read_with(cx, |lsp_store, _| lsp_store.languages.clone());
 82    cx.spawn(async move |cx| {
 83        let languages = languages?;
 84        let schema = resolve_schema_request(&languages, lsp_store, uri, cx).await?;
 85        serde_json::to_string(&schema).context("Failed to serialize schema")
 86    })
 87}
 88
 89pub async fn resolve_schema_request(
 90    languages: &Arc<LanguageRegistry>,
 91    lsp_store: Entity<LspStore>,
 92    uri: String,
 93    cx: &mut AsyncApp,
 94) -> Result<serde_json::Value> {
 95    let path = uri.strip_prefix("zed://schemas/").context("Invalid URI")?;
 96    resolve_schema_request_inner(languages, lsp_store, path, cx).await
 97}
 98
 99pub async fn resolve_schema_request_inner(
100    languages: &Arc<LanguageRegistry>,
101    lsp_store: Entity<LspStore>,
102    path: &str,
103    cx: &mut AsyncApp,
104) -> Result<serde_json::Value> {
105    let (schema_name, rest) = path.split_once('/').unzip();
106    let schema_name = schema_name.unwrap_or(path);
107
108    let schema = match schema_name {
109        "settings" if rest.is_some_and(|r| r.starts_with("lsp/")) => {
110            let lsp_name = rest
111                .and_then(|r| {
112                    r.strip_prefix(
113                        LSP_SETTINGS_SCHEMA_URL_PREFIX
114                            .strip_prefix("zed://schemas/settings/")
115                            .unwrap(),
116                    )
117                })
118                .context("Invalid LSP schema path")?;
119
120            let adapter = languages
121                .all_lsp_adapters()
122                .into_iter()
123                .find(|adapter| adapter.name().as_ref() as &str == lsp_name)
124                .with_context(|| format!("LSP adapter not found: {}", lsp_name))?;
125
126            let delegate = cx.update(|inner_cx| {
127                lsp_store.update(inner_cx, |lsp_store, inner_cx| {
128                    let Some(local) = lsp_store.as_local() else {
129                        return None;
130                    };
131                    let Some(worktree) = local.worktree_store.read(inner_cx).worktrees().next() else {
132                        return None;
133                    };
134                    Some(LocalLspAdapterDelegate::from_local_lsp(
135                        local, &worktree, inner_cx,
136                    ))
137                })
138            })?.context("Failed to create adapter delegate - either LSP store is not in local mode or no worktree is available")?;
139
140            let adapter_for_schema = adapter.clone();
141
142            let binary = adapter
143                .get_language_server_command(
144                    delegate,
145                    None,
146                    LanguageServerBinaryOptions {
147                        allow_path_lookup: true,
148                        allow_binary_download: false,
149                        pre_release: false,
150                    },
151                    cx,
152                )
153                .await
154                .await
155                .0.with_context(|| format!("Failed to find language server {lsp_name} to generate initialization params schema"))?;
156
157            adapter_for_schema
158                .adapter
159                .clone()
160                .initialization_options_schema(&binary)
161                .await
162                .unwrap_or_else(|| {
163                    serde_json::json!({
164                        "type": "object",
165                        "additionalProperties": true
166                    })
167                })
168        }
169        "settings" => {
170            let lsp_adapter_names = languages
171                .all_lsp_adapters()
172                .into_iter()
173                .map(|adapter| adapter.name().to_string())
174                .collect::<Vec<_>>();
175
176            cx.update(|cx| {
177                let font_names = &cx.text_system().all_font_names();
178                let language_names = &languages
179                    .language_names()
180                    .into_iter()
181                    .map(|name| name.to_string())
182                    .collect::<Vec<_>>();
183
184                let mut icon_theme_names = vec![];
185                let mut theme_names = vec![];
186                if let Some(registry) = theme::ThemeRegistry::try_global(cx) {
187                    icon_theme_names.extend(
188                        registry
189                            .list_icon_themes()
190                            .into_iter()
191                            .map(|icon_theme| icon_theme.name),
192                    );
193                    theme_names.extend(registry.list_names());
194                }
195                let icon_theme_names = icon_theme_names.as_slice();
196                let theme_names = theme_names.as_slice();
197
198                cx.global::<settings::SettingsStore>().json_schema(
199                    &settings::SettingsJsonSchemaParams {
200                        language_names,
201                        font_names,
202                        theme_names,
203                        icon_theme_names,
204                        lsp_adapter_names: &lsp_adapter_names,
205                    },
206                )
207            })?
208        }
209        "keymap" => cx.update(settings::KeymapFile::generate_json_schema_for_registered_actions)?,
210        "action" => {
211            let normalized_action_name = rest.context("No Action name provided")?;
212            let action_name = denormalize_action_name(normalized_action_name);
213            let mut generator = settings::KeymapFile::action_schema_generator();
214            let schema = cx
215                // PERF: cx.action_schema_by_name(action_name, &mut generator)
216                .update(|cx| cx.action_schemas(&mut generator))?
217                .into_iter()
218                .find_map(|(name, schema)| (name == action_name).then_some(schema))
219                .flatten();
220            root_schema_from_action_schema(schema, &mut generator).to_value()
221        }
222        "tasks" => task::TaskTemplates::generate_json_schema(),
223        "debug_tasks" => {
224            let adapter_schemas = cx.read_global::<dap::DapRegistry, _>(|dap_registry, _| {
225                dap_registry.adapters_schema()
226            })?;
227            task::DebugTaskFile::generate_json_schema(&adapter_schemas)
228        }
229        "package_json" => package_json_schema(),
230        "tsconfig" => tsconfig_schema(),
231        "zed_inspector_style" => {
232            if cfg!(debug_assertions) {
233                generate_inspector_style_schema()
234            } else {
235                schemars::json_schema!(true).to_value()
236            }
237        }
238        "snippets" => snippet_provider::format::VsSnippetsFile::generate_json_schema(),
239        "jsonc" => jsonc_schema(),
240        _ => {
241            anyhow::bail!("Unrecognized builtin JSON schema: {schema_name}");
242        }
243    };
244    Ok(schema)
245}
246
247const JSONC_LANGUAGE_NAME: &str = "JSONC";
248
249pub fn all_schema_file_associations(
250    languages: &Arc<LanguageRegistry>,
251    cx: &mut App,
252) -> serde_json::Value {
253    let extension_globs = languages
254        .available_language_for_name(JSONC_LANGUAGE_NAME)
255        .map(|language| language.matcher().path_suffixes.clone())
256        .into_iter()
257        .flatten()
258        // Path suffixes can be entire file names or just their extensions.
259        .flat_map(|path_suffix| [format!("*.{path_suffix}"), path_suffix]);
260    let override_globs = all_language_settings(None, cx)
261        .file_types
262        .get(JSONC_LANGUAGE_NAME)
263        .into_iter()
264        .flat_map(|(_, glob_strings)| glob_strings)
265        .cloned();
266    let jsonc_globs = extension_globs.chain(override_globs).collect::<Vec<_>>();
267
268    let mut file_associations = serde_json::json!([
269        {
270            "fileMatch": [
271                schema_file_match(paths::settings_file()),
272                paths::local_settings_file_relative_path()
273            ],
274            "url": "zed://schemas/settings",
275        },
276        {
277            "fileMatch": [schema_file_match(paths::keymap_file())],
278            "url": "zed://schemas/keymap",
279        },
280        {
281            "fileMatch": [
282                schema_file_match(paths::tasks_file()),
283                paths::local_tasks_file_relative_path()
284            ],
285            "url": "zed://schemas/tasks",
286        },
287        {
288            "fileMatch": [
289                schema_file_match(paths::debug_scenarios_file()),
290                paths::local_debug_file_relative_path()
291            ],
292            "url": "zed://schemas/debug_tasks",
293        },
294        {
295            "fileMatch": [
296                schema_file_match(
297                    paths::snippets_dir()
298                        .join("*.json")
299                        .as_path()
300                )
301            ],
302            "url": "zed://schemas/snippets",
303        },
304        {
305            "fileMatch": ["tsconfig.json"],
306            "url": "zed://schemas/tsconfig"
307        },
308        {
309            "fileMatch": ["package.json"],
310            "url": "zed://schemas/package_json"
311        },
312        {
313            "fileMatch": &jsonc_globs,
314            "url": "zed://schemas/jsonc"
315        },
316    ]);
317
318    #[cfg(debug_assertions)]
319    {
320        file_associations
321            .as_array_mut()
322            .unwrap()
323            .push(serde_json::json!({
324                "fileMatch": [
325                    "zed-inspector-style.json"
326                ],
327                "url": "zed://schemas/zed_inspector_style"
328            }));
329    }
330
331    file_associations.as_array_mut().unwrap().extend(
332        // ?PERF: use all_action_schemas() and don't include action schemas with no arguments
333        cx.all_action_names().into_iter().map(|&name| {
334            let normalized_name = normalize_action_name(name);
335            let file_name = normalized_action_name_to_file_name(normalized_name.clone());
336            serde_json::json!({
337                "fileMatch": [file_name],
338                "url": format!("zed://schemas/action/{normalized_name}")
339            })
340        }),
341    );
342
343    file_associations
344}
345
346fn tsconfig_schema() -> serde_json::Value {
347    serde_json::Value::from_str(TSCONFIG_SCHEMA).unwrap()
348}
349
350fn package_json_schema() -> serde_json::Value {
351    serde_json::Value::from_str(PACKAGE_JSON_SCHEMA).unwrap()
352}
353
354fn jsonc_schema() -> serde_json::Value {
355    let generator = schemars::generate::SchemaSettings::draft2019_09()
356        .with_transform(DefaultDenyUnknownFields)
357        .with_transform(AllowTrailingCommas)
358        .into_generator();
359    let meta_schema = generator
360        .settings()
361        .meta_schema
362        .as_ref()
363        .expect("meta_schema should be present in schemars settings")
364        .to_string();
365    let defs = generator.definitions();
366    let schema = schemars::json_schema!({
367        "$schema": meta_schema,
368        "allowTrailingCommas": true,
369        "$defs": defs,
370    });
371    serde_json::to_value(schema).unwrap()
372}
373
374fn generate_inspector_style_schema() -> serde_json::Value {
375    let schema = schemars::generate::SchemaSettings::draft2019_09()
376        .with_transform(util::schemars::DefaultDenyUnknownFields)
377        .into_generator()
378        .root_schema_for::<gpui::StyleRefinement>();
379
380    serde_json::to_value(schema).unwrap()
381}
382
383pub fn normalize_action_name(action_name: &str) -> String {
384    action_name.replace("::", "__")
385}
386
387pub fn denormalize_action_name(action_name: &str) -> String {
388    action_name.replace("__", "::")
389}
390
391pub fn normalized_action_file_name(action_name: &str) -> String {
392    normalized_action_name_to_file_name(normalize_action_name(action_name))
393}
394
395pub fn normalized_action_name_to_file_name(mut normalized_action_name: String) -> String {
396    normalized_action_name.push_str(".json");
397    normalized_action_name
398}
399
400fn root_schema_from_action_schema(
401    action_schema: Option<schemars::Schema>,
402    generator: &mut schemars::SchemaGenerator,
403) -> schemars::Schema {
404    let Some(mut action_schema) = action_schema else {
405        return schemars::json_schema!(false);
406    };
407    let meta_schema = generator
408        .settings()
409        .meta_schema
410        .as_ref()
411        .expect("meta_schema should be present in schemars settings")
412        .to_string();
413    let defs = generator.definitions();
414    let mut schema = schemars::json_schema!({
415        "$schema": meta_schema,
416        "allowTrailingCommas": true,
417        "$defs": defs,
418    });
419    schema
420        .ensure_object()
421        .extend(std::mem::take(action_schema.ensure_object()));
422    schema
423}
424
425#[inline]
426fn schema_file_match(path: &std::path::Path) -> String {
427    path.strip_prefix(path.parent().unwrap().parent().unwrap())
428        .unwrap()
429        .display()
430        .to_string()
431        .replace('\\', "/")
432}