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