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