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, &uri, cx,
65 );
66 true
67 })
68 }
69}
70
71fn handle_schema_request(
72 lsp_store: Entity<LspStore>,
73 uri: String,
74 cx: &mut AsyncApp,
75) -> Result<String> {
76 let languages = lsp_store.read_with(cx, |lsp_store, _| lsp_store.languages.clone())?;
77 let schema = resolve_schema_request(&languages, uri, cx)?;
78 serde_json::to_string(&schema).context("Failed to serialize schema")
79}
80
81pub fn resolve_schema_request(
82 languages: &Arc<LanguageRegistry>,
83 uri: String,
84 cx: &mut AsyncApp,
85) -> Result<serde_json::Value> {
86 let path = uri.strip_prefix("zed://schemas/").context("Invalid URI")?;
87 resolve_schema_request_inner(languages, path, cx)
88}
89
90pub fn resolve_schema_request_inner(
91 languages: &Arc<LanguageRegistry>,
92 path: &str,
93 cx: &mut AsyncApp,
94) -> Result<serde_json::Value> {
95 let (schema_name, rest) = path.split_once('/').unzip();
96 let schema_name = schema_name.unwrap_or(path);
97
98 let schema = match schema_name {
99 "settings" => cx.update(|cx| {
100 let font_names = &cx.text_system().all_font_names();
101 let language_names = &languages
102 .language_names()
103 .into_iter()
104 .map(|name| name.to_string())
105 .collect::<Vec<_>>();
106
107 let mut icon_theme_names = vec![];
108 let mut theme_names = vec![];
109 if let Some(registry) = theme::ThemeRegistry::try_global(cx) {
110 icon_theme_names.extend(
111 registry
112 .list_icon_themes()
113 .into_iter()
114 .map(|icon_theme| icon_theme.name),
115 );
116 theme_names.extend(registry.list_names());
117 }
118 let icon_theme_names = icon_theme_names.as_slice();
119 let theme_names = theme_names.as_slice();
120
121 cx.global::<settings::SettingsStore>().json_schema(
122 &settings::SettingsJsonSchemaParams {
123 language_names,
124 font_names,
125 theme_names,
126 icon_theme_names,
127 },
128 )
129 })?,
130 "keymap" => cx.update(settings::KeymapFile::generate_json_schema_for_registered_actions)?,
131 "action" => {
132 let normalized_action_name = rest.context("No Action name provided")?;
133 let action_name = denormalize_action_name(normalized_action_name);
134 let mut generator = settings::KeymapFile::action_schema_generator();
135 let schema = cx
136 // PERF: cx.action_schema_by_name(action_name, &mut generator)
137 .update(|cx| cx.action_schemas(&mut generator))?
138 .into_iter()
139 .find_map(|(name, schema)| (name == action_name).then_some(schema))
140 .flatten();
141 root_schema_from_action_schema(schema, &mut generator).to_value()
142 }
143 "tasks" => task::TaskTemplates::generate_json_schema(),
144 "debug_tasks" => {
145 let adapter_schemas = cx.read_global::<dap::DapRegistry, _>(|dap_registry, _| {
146 dap_registry.adapters_schema()
147 })?;
148 task::DebugTaskFile::generate_json_schema(&adapter_schemas)
149 }
150 "package_json" => package_json_schema(),
151 "tsconfig" => tsconfig_schema(),
152 "zed_inspector_style" => {
153 if cfg!(debug_assertions) {
154 generate_inspector_style_schema()
155 } else {
156 schemars::json_schema!(true).to_value()
157 }
158 }
159 "snippets" => snippet_provider::format::VsSnippetsFile::generate_json_schema(),
160 _ => {
161 anyhow::bail!("Unrecognized builtin JSON schema: {}", schema_name);
162 }
163 };
164 Ok(schema)
165}
166
167pub fn all_schema_file_associations(cx: &mut App) -> serde_json::Value {
168 let mut file_associations = serde_json::json!([
169 {
170 "fileMatch": [
171 schema_file_match(paths::settings_file()),
172 paths::local_settings_file_relative_path()
173 ],
174 "url": "zed://schemas/settings",
175 },
176 {
177 "fileMatch": [schema_file_match(paths::keymap_file())],
178 "url": "zed://schemas/keymap",
179 },
180 {
181 "fileMatch": [
182 schema_file_match(paths::tasks_file()),
183 paths::local_tasks_file_relative_path()
184 ],
185 "url": "zed://schemas/tasks",
186 },
187 {
188 "fileMatch": [
189 schema_file_match(paths::debug_scenarios_file()),
190 paths::local_debug_file_relative_path()
191 ],
192 "url": "zed://schemas/debug_tasks",
193 },
194 {
195 "fileMatch": [
196 schema_file_match(
197 paths::snippets_dir()
198 .join("*.json")
199 .as_path()
200 )
201 ],
202 "url": "zed://schemas/snippets",
203 },
204 {
205 "fileMatch": ["tsconfig.json"],
206 "url": "zed://schemas/tsconfig"
207 },
208 {
209 "fileMatch": ["package.json"],
210 "url": "zed://schemas/package_json"
211 },
212 ]);
213
214 #[cfg(debug_assertions)]
215 {
216 file_associations
217 .as_array_mut()
218 .unwrap()
219 .push(serde_json::json!({
220 "fileMatch": [
221 "zed-inspector-style.json"
222 ],
223 "url": "zed://schemas/zed_inspector_style"
224 }));
225 }
226
227 file_associations.as_array_mut().unwrap().extend(
228 // ?PERF: use all_action_schemas() and don't include action schemas with no arguments
229 cx.all_action_names().into_iter().map(|&name| {
230 let normalized_name = normalize_action_name(name);
231 let file_name = normalized_action_name_to_file_name(normalized_name.clone());
232 serde_json::json!({
233 "fileMatch": [file_name],
234 "url": format!("zed://schemas/action/{}", normalized_name)
235 })
236 }),
237 );
238
239 file_associations
240}
241
242fn tsconfig_schema() -> serde_json::Value {
243 serde_json::Value::from_str(TSCONFIG_SCHEMA).unwrap()
244}
245
246fn package_json_schema() -> serde_json::Value {
247 serde_json::Value::from_str(PACKAGE_JSON_SCHEMA).unwrap()
248}
249
250fn generate_inspector_style_schema() -> serde_json::Value {
251 let schema = schemars::generate::SchemaSettings::draft2019_09()
252 .with_transform(util::schemars::DefaultDenyUnknownFields)
253 .into_generator()
254 .root_schema_for::<gpui::StyleRefinement>();
255
256 serde_json::to_value(schema).unwrap()
257}
258
259pub fn normalize_action_name(action_name: &str) -> String {
260 action_name.replace("::", "__")
261}
262
263pub fn denormalize_action_name(action_name: &str) -> String {
264 action_name.replace("__", "::")
265}
266
267pub fn normalized_action_file_name(action_name: &str) -> String {
268 normalized_action_name_to_file_name(normalize_action_name(action_name))
269}
270
271pub fn normalized_action_name_to_file_name(mut normalized_action_name: String) -> String {
272 normalized_action_name.push_str(".json");
273 normalized_action_name
274}
275
276fn root_schema_from_action_schema(
277 action_schema: Option<schemars::Schema>,
278 generator: &mut schemars::SchemaGenerator,
279) -> schemars::Schema {
280 let Some(mut action_schema) = action_schema else {
281 return schemars::json_schema!(false);
282 };
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 mut schema = schemars::json_schema!({
291 "$schema": meta_schema,
292 "allowTrailingCommas": true,
293 "$defs": defs,
294 });
295 schema
296 .ensure_object()
297 .extend(std::mem::take(action_schema.ensure_object()));
298 schema
299}
300
301#[inline]
302fn schema_file_match(path: &std::path::Path) -> String {
303 path.strip_prefix(path.parent().unwrap().parent().unwrap())
304 .unwrap()
305 .display()
306 .to_string()
307 .replace('\\', "/")
308}