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