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