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