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