json_schema_store.rs

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