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