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