1//! # json_schema_store
2use std::{str::FromStr, sync::Arc};
3
4use anyhow::{Context as _, Result};
5use gpui::{App, AsyncApp, BorrowAppContext as _, Entity, SharedString, 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 let icon_theme_names = &theme::ThemeRegistry::global(cx)
107 .list_icon_themes()
108 .into_iter()
109 .map(|icon_theme| icon_theme.name)
110 .collect::<Vec<SharedString>>();
111 let theme_names = &theme::ThemeRegistry::global(cx).list_names();
112 cx.global::<settings::SettingsStore>().json_schema(
113 &settings::SettingsJsonSchemaParams {
114 language_names,
115 font_names,
116 theme_names,
117 icon_theme_names,
118 },
119 )
120 })?,
121 "keymap" => cx.update(settings::KeymapFile::generate_json_schema_for_registered_actions)?,
122 "action" => {
123 let normalized_action_name = rest.context("No Action name provided")?;
124 let action_name = denormalize_action_name(normalized_action_name);
125 let mut generator = settings::KeymapFile::action_schema_generator();
126 let schema = cx
127 // PERF: cx.action_schema_by_name(action_name, &mut generator)
128 .update(|cx| cx.action_schemas(&mut generator))?
129 .into_iter()
130 .find_map(|(name, schema)| (name == action_name).then_some(schema))
131 .flatten();
132 root_schema_from_action_schema(schema, &mut generator).to_value()
133 }
134 "tasks" => task::TaskTemplates::generate_json_schema(),
135 "debug_tasks" => {
136 let adapter_schemas = cx.read_global::<dap::DapRegistry, _>(|dap_registry, _| {
137 dap_registry.adapters_schema()
138 })?;
139 task::DebugTaskFile::generate_json_schema(&adapter_schemas)
140 }
141 "package_json" => package_json_schema(),
142 "tsconfig" => tsconfig_schema(),
143 "zed_inspector_style" => {
144 if cfg!(debug_assertions) {
145 generate_inspector_style_schema()
146 } else {
147 schemars::json_schema!(true).to_value()
148 }
149 }
150 "snippets" => snippet_provider::format::VsSnippetsFile::generate_json_schema(),
151 _ => {
152 anyhow::bail!("Unrecognized builtin JSON schema: {}", schema_name);
153 }
154 };
155 Ok(schema)
156}
157
158pub fn all_schema_file_associations(cx: &mut App) -> serde_json::Value {
159 let mut file_associations = serde_json::json!([
160 {
161 "fileMatch": [
162 schema_file_match(paths::settings_file()),
163 paths::local_settings_file_relative_path()
164 ],
165 "url": "zed://schemas/settings",
166 },
167 {
168 "fileMatch": [schema_file_match(paths::keymap_file())],
169 "url": "zed://schemas/keymap",
170 },
171 {
172 "fileMatch": [
173 schema_file_match(paths::tasks_file()),
174 paths::local_tasks_file_relative_path()
175 ],
176 "url": "zed://schemas/tasks",
177 },
178 {
179 "fileMatch": [
180 schema_file_match(paths::debug_scenarios_file()),
181 paths::local_debug_file_relative_path()
182 ],
183 "url": "zed://schemas/debug_tasks",
184 },
185 {
186 "fileMatch": [
187 schema_file_match(
188 paths::snippets_dir()
189 .join("*.json")
190 .as_path()
191 )
192 ],
193 "url": "zed://schemas/snippets",
194 },
195 {
196 "fileMatch": ["tsconfig.json"],
197 "url": "zed://schemas/tsconfig"
198 },
199 {
200 "fileMatch": ["package.json"],
201 "url": "zed://schemas/package_json"
202 },
203 ]);
204
205 #[cfg(debug_assertions)]
206 {
207 file_associations
208 .as_array_mut()
209 .unwrap()
210 .push(serde_json::json!({
211 "fileMatch": [
212 "zed-inspector-style.json"
213 ],
214 "url": "zed://schemas/zed_inspector_style"
215 }));
216 }
217
218 file_associations.as_array_mut().unwrap().extend(
219 // ?PERF: use all_action_schemas() and don't include action schemas with no arguments
220 cx.all_action_names().into_iter().map(|&name| {
221 let normalized_name = normalize_action_name(name);
222 let file_name = normalized_action_name_to_file_name(normalized_name.clone());
223 serde_json::json!({
224 "fileMatch": [file_name],
225 "url": format!("zed://schemas/action/{}", normalized_name)
226 })
227 }),
228 );
229
230 file_associations
231}
232
233fn tsconfig_schema() -> serde_json::Value {
234 serde_json::Value::from_str(TSCONFIG_SCHEMA).unwrap()
235}
236
237fn package_json_schema() -> serde_json::Value {
238 serde_json::Value::from_str(PACKAGE_JSON_SCHEMA).unwrap()
239}
240
241fn generate_inspector_style_schema() -> serde_json::Value {
242 let schema = schemars::generate::SchemaSettings::draft2019_09()
243 .with_transform(util::schemars::DefaultDenyUnknownFields)
244 .into_generator()
245 .root_schema_for::<gpui::StyleRefinement>();
246
247 serde_json::to_value(schema).unwrap()
248}
249
250pub fn normalize_action_name(action_name: &str) -> String {
251 action_name.replace("::", "__")
252}
253
254pub fn denormalize_action_name(action_name: &str) -> String {
255 action_name.replace("__", "::")
256}
257
258pub fn normalized_action_file_name(action_name: &str) -> String {
259 normalized_action_name_to_file_name(normalize_action_name(action_name))
260}
261
262pub fn normalized_action_name_to_file_name(mut normalized_action_name: String) -> String {
263 normalized_action_name.push_str(".json");
264 normalized_action_name
265}
266
267fn root_schema_from_action_schema(
268 action_schema: Option<schemars::Schema>,
269 generator: &mut schemars::SchemaGenerator,
270) -> schemars::Schema {
271 let Some(mut action_schema) = action_schema else {
272 return schemars::json_schema!(false);
273 };
274 let meta_schema = generator
275 .settings()
276 .meta_schema
277 .as_ref()
278 .expect("meta_schema should be present in schemars settings")
279 .to_string();
280 let defs = generator.definitions();
281 let mut schema = schemars::json_schema!({
282 "$schema": meta_schema,
283 "allowTrailingCommas": true,
284 "$defs": defs,
285 });
286 schema
287 .ensure_object()
288 .extend(std::mem::take(action_schema.ensure_object()));
289 schema
290}
291
292#[inline]
293fn schema_file_match(path: &std::path::Path) -> String {
294 path.strip_prefix(path.parent().unwrap().parent().unwrap())
295 .unwrap()
296 .display()
297 .to_string()
298 .replace('\\', "/")
299}