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