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