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
127                .update(|inner_cx| {
128                    lsp_store.update(inner_cx, |lsp_store, inner_cx| {
129                        let Some(local) = lsp_store.as_local() else {
130                            return None;
131                        };
132                        let Some(worktree) = local.worktree_store.read(inner_cx).worktrees().next()
133                        else {
134                            return None;
135                        };
136                        Some(LocalLspAdapterDelegate::from_local_lsp(
137                            local, &worktree, inner_cx,
138                        ))
139                    })
140                })?
141                .context(concat!(
142                    "Failed to create adapter delegate - ",
143                    "either LSP store is not in local mode or no worktree is available"
144                ))?;
145
146            let adapter_for_schema = adapter.clone();
147
148            let binary = adapter
149                .get_language_server_command(
150                    delegate,
151                    None,
152                    LanguageServerBinaryOptions {
153                        allow_path_lookup: true,
154                        allow_binary_download: false,
155                        pre_release: false,
156                    },
157                    cx,
158                )
159                .await
160                .await
161                .0
162                .with_context(|| {
163                    format!(
164                        concat!(
165                            "Failed to find language server {} ",
166                            "to generate initialization params schema"
167                        ),
168                        lsp_name
169                    )
170                })?;
171
172            adapter_for_schema
173                .adapter
174                .clone()
175                .initialization_options_schema(&binary)
176                .await
177                .unwrap_or_else(|| {
178                    serde_json::json!({
179                        "type": "object",
180                        "additionalProperties": true
181                    })
182                })
183        }
184        "settings" => {
185            let lsp_adapter_names = languages
186                .all_lsp_adapters()
187                .into_iter()
188                .map(|adapter| adapter.name().to_string())
189                .collect::<Vec<_>>();
190
191            cx.update(|cx| {
192                let font_names = &cx.text_system().all_font_names();
193                let language_names = &languages
194                    .language_names()
195                    .into_iter()
196                    .map(|name| name.to_string())
197                    .collect::<Vec<_>>();
198
199                let mut icon_theme_names = vec![];
200                let mut theme_names = vec![];
201                if let Some(registry) = theme::ThemeRegistry::try_global(cx) {
202                    icon_theme_names.extend(
203                        registry
204                            .list_icon_themes()
205                            .into_iter()
206                            .map(|icon_theme| icon_theme.name),
207                    );
208                    theme_names.extend(registry.list_names());
209                }
210                let icon_theme_names = icon_theme_names.as_slice();
211                let theme_names = theme_names.as_slice();
212
213                cx.global::<settings::SettingsStore>().json_schema(
214                    &settings::SettingsJsonSchemaParams {
215                        language_names,
216                        font_names,
217                        theme_names,
218                        icon_theme_names,
219                        lsp_adapter_names: &lsp_adapter_names,
220                    },
221                )
222            })?
223        }
224        "keymap" => cx.update(settings::KeymapFile::generate_json_schema_for_registered_actions)?,
225        "action" => {
226            let normalized_action_name = rest.context("No Action name provided")?;
227            let action_name = denormalize_action_name(normalized_action_name);
228            let mut generator = settings::KeymapFile::action_schema_generator();
229            let schema = cx
230                // PERF: cx.action_schema_by_name(action_name, &mut generator)
231                .update(|cx| cx.action_schemas(&mut generator))?
232                .into_iter()
233                .find_map(|(name, schema)| (name == action_name).then_some(schema))
234                .flatten();
235            root_schema_from_action_schema(schema, &mut generator).to_value()
236        }
237        "tasks" => task::TaskTemplates::generate_json_schema(),
238        "debug_tasks" => {
239            let adapter_schemas = cx.read_global::<dap::DapRegistry, _>(|dap_registry, _| {
240                dap_registry.adapters_schema()
241            })?;
242            task::DebugTaskFile::generate_json_schema(&adapter_schemas)
243        }
244        "package_json" => package_json_schema(),
245        "tsconfig" => tsconfig_schema(),
246        "zed_inspector_style" => {
247            if cfg!(debug_assertions) {
248                generate_inspector_style_schema()
249            } else {
250                schemars::json_schema!(true).to_value()
251            }
252        }
253        "snippets" => snippet_provider::format::VsSnippetsFile::generate_json_schema(),
254        "jsonc" => jsonc_schema(),
255        _ => {
256            anyhow::bail!("Unrecognized builtin JSON schema: {schema_name}");
257        }
258    };
259    Ok(schema)
260}
261
262const JSONC_LANGUAGE_NAME: &str = "JSONC";
263
264pub fn all_schema_file_associations(
265    languages: &Arc<LanguageRegistry>,
266    cx: &mut App,
267) -> serde_json::Value {
268    let extension_globs = languages
269        .available_language_for_name(JSONC_LANGUAGE_NAME)
270        .map(|language| language.matcher().path_suffixes.clone())
271        .into_iter()
272        .flatten()
273        // Path suffixes can be entire file names or just their extensions.
274        .flat_map(|path_suffix| [format!("*.{path_suffix}"), path_suffix]);
275    let override_globs = all_language_settings(None, cx)
276        .file_types
277        .get(JSONC_LANGUAGE_NAME)
278        .into_iter()
279        .flat_map(|(_, glob_strings)| glob_strings)
280        .cloned();
281    let jsonc_globs = extension_globs.chain(override_globs).collect::<Vec<_>>();
282
283    let mut file_associations = serde_json::json!([
284        {
285            "fileMatch": [
286                schema_file_match(paths::settings_file()),
287                paths::local_settings_file_relative_path()
288            ],
289            "url": "zed://schemas/settings",
290        },
291        {
292            "fileMatch": [schema_file_match(paths::keymap_file())],
293            "url": "zed://schemas/keymap",
294        },
295        {
296            "fileMatch": [
297                schema_file_match(paths::tasks_file()),
298                paths::local_tasks_file_relative_path()
299            ],
300            "url": "zed://schemas/tasks",
301        },
302        {
303            "fileMatch": [
304                schema_file_match(paths::debug_scenarios_file()),
305                paths::local_debug_file_relative_path()
306            ],
307            "url": "zed://schemas/debug_tasks",
308        },
309        {
310            "fileMatch": [
311                schema_file_match(
312                    paths::snippets_dir()
313                        .join("*.json")
314                        .as_path()
315                )
316            ],
317            "url": "zed://schemas/snippets",
318        },
319        {
320            "fileMatch": ["tsconfig.json"],
321            "url": "zed://schemas/tsconfig"
322        },
323        {
324            "fileMatch": ["package.json"],
325            "url": "zed://schemas/package_json"
326        },
327        {
328            "fileMatch": &jsonc_globs,
329            "url": "zed://schemas/jsonc"
330        },
331    ]);
332
333    #[cfg(debug_assertions)]
334    {
335        file_associations
336            .as_array_mut()
337            .unwrap()
338            .push(serde_json::json!({
339                "fileMatch": [
340                    "zed-inspector-style.json"
341                ],
342                "url": "zed://schemas/zed_inspector_style"
343            }));
344    }
345
346    file_associations.as_array_mut().unwrap().extend(
347        // ?PERF: use all_action_schemas() and don't include action schemas with no arguments
348        cx.all_action_names().into_iter().map(|&name| {
349            let normalized_name = normalize_action_name(name);
350            let file_name = normalized_action_name_to_file_name(normalized_name.clone());
351            serde_json::json!({
352                "fileMatch": [file_name],
353                "url": format!("zed://schemas/action/{normalized_name}")
354            })
355        }),
356    );
357
358    file_associations
359}
360
361fn tsconfig_schema() -> serde_json::Value {
362    serde_json::Value::from_str(TSCONFIG_SCHEMA).unwrap()
363}
364
365fn package_json_schema() -> serde_json::Value {
366    serde_json::Value::from_str(PACKAGE_JSON_SCHEMA).unwrap()
367}
368
369fn jsonc_schema() -> serde_json::Value {
370    let generator = schemars::generate::SchemaSettings::draft2019_09()
371        .with_transform(DefaultDenyUnknownFields)
372        .with_transform(AllowTrailingCommas)
373        .into_generator();
374    let meta_schema = generator
375        .settings()
376        .meta_schema
377        .as_ref()
378        .expect("meta_schema should be present in schemars settings")
379        .to_string();
380    let defs = generator.definitions();
381    let schema = schemars::json_schema!({
382        "$schema": meta_schema,
383        "allowTrailingCommas": true,
384        "$defs": defs,
385    });
386    serde_json::to_value(schema).unwrap()
387}
388
389fn generate_inspector_style_schema() -> serde_json::Value {
390    let schema = schemars::generate::SchemaSettings::draft2019_09()
391        .with_transform(util::schemars::DefaultDenyUnknownFields)
392        .into_generator()
393        .root_schema_for::<gpui::StyleRefinement>();
394
395    serde_json::to_value(schema).unwrap()
396}
397
398pub fn normalize_action_name(action_name: &str) -> String {
399    action_name.replace("::", "__")
400}
401
402pub fn denormalize_action_name(action_name: &str) -> String {
403    action_name.replace("__", "::")
404}
405
406pub fn normalized_action_file_name(action_name: &str) -> String {
407    normalized_action_name_to_file_name(normalize_action_name(action_name))
408}
409
410pub fn normalized_action_name_to_file_name(mut normalized_action_name: String) -> String {
411    normalized_action_name.push_str(".json");
412    normalized_action_name
413}
414
415fn root_schema_from_action_schema(
416    action_schema: Option<schemars::Schema>,
417    generator: &mut schemars::SchemaGenerator,
418) -> schemars::Schema {
419    let Some(mut action_schema) = action_schema else {
420        return schemars::json_schema!(false);
421    };
422    let meta_schema = generator
423        .settings()
424        .meta_schema
425        .as_ref()
426        .expect("meta_schema should be present in schemars settings")
427        .to_string();
428    let defs = generator.definitions();
429    let mut schema = schemars::json_schema!({
430        "$schema": meta_schema,
431        "allowTrailingCommas": true,
432        "$defs": defs,
433    });
434    schema
435        .ensure_object()
436        .extend(std::mem::take(action_schema.ensure_object()));
437    schema
438}
439
440#[inline]
441fn schema_file_match(path: &std::path::Path) -> String {
442    path.strip_prefix(path.parent().unwrap().parent().unwrap())
443        .unwrap()
444        .display()
445        .to_string()
446        .replace('\\', "/")
447}