Skip to repository content574 lines · 18.4 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T02:33:20.166Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
json.rs
1use anyhow::{Context as _, Result, bail};
2use async_compression::futures::bufread::GzipDecoder;
3use async_tar::Archive;
4use async_trait::async_trait;
5use collections::HashMap;
6use futures::StreamExt;
7use gpui::{App, AsyncApp, Entity, Task};
8use http_client::github::{GitHubLspBinaryVersion, latest_github_release};
9use language::{
10 Buffer, ContextProvider, LanguageName, LanguageRegistry, LocalFile as _, LspAdapter,
11 LspAdapterDelegate, LspInstaller, Toolchain,
12};
13use lsp::{LanguageServerBinary, LanguageServerName, Uri};
14use node_runtime::{NodeRuntime, VersionStrategy};
15use project::lsp_store::language_server_settings;
16use semver::Version;
17use serde_json::{Value, json};
18use settings::SettingsLocation;
19use smol::{
20 fs::{self},
21 io::BufReader,
22};
23use std::{
24 borrow::Cow,
25 env::consts,
26 ffi::OsString,
27 future::Future,
28 path::{Path, PathBuf},
29 str::FromStr,
30 sync::Arc,
31};
32use task::{TaskTemplate, TaskTemplates, VariableName};
33use util::{
34 ResultExt, archive::extract_zip, fs::remove_matching, maybe, merge_json_value_into,
35 paths::PathStyle, rel_path::RelPath,
36};
37
38use crate::PackageJsonData;
39
40const SERVER_PATH: &str =
41 "node_modules/vscode-langservers-extracted/bin/vscode-json-language-server";
42
43pub(crate) struct JsonTaskProvider;
44
45impl ContextProvider for JsonTaskProvider {
46 fn associated_tasks(
47 &self,
48 buffer: Option<Entity<Buffer>>,
49 cx: &App,
50 ) -> gpui::Task<Option<TaskTemplates>> {
51 let file = buffer.as_ref().and_then(|buf| buf.read(cx).file());
52 let Some(file) = project::File::from_dyn(file).cloned() else {
53 return Task::ready(None);
54 };
55 let is_package_json = file
56 .path
57 .ends_with(RelPath::from_unix_str("package.json").unwrap());
58 let is_composer_json = file
59 .path
60 .ends_with(RelPath::from_unix_str("composer.json").unwrap());
61 if !is_package_json && !is_composer_json {
62 return Task::ready(None);
63 }
64
65 cx.spawn(async move |cx| {
66 let contents = file
67 .worktree
68 .update(cx, |this, cx| this.load_file(&file.path, cx))
69 .await
70 .ok()?;
71 let path = cx.update(|cx| file.abs_path(cx)).as_path().into();
72
73 let task_templates = if is_package_json {
74 let package_json = serde_json_lenient::from_str::<
75 HashMap<String, serde_json_lenient::Value>,
76 >(&contents.text)
77 .ok()?;
78 let package_json = PackageJsonData::new(path, package_json);
79 let command = package_json.package_manager.unwrap_or("npm").to_owned();
80 package_json
81 .scripts
82 .into_iter()
83 .map(|(_, key)| TaskTemplate {
84 label: format!("run {key}"),
85 command: command.clone(),
86 args: vec!["run".into(), key],
87 cwd: Some(VariableName::Dirname.template_value()),
88 ..TaskTemplate::default()
89 })
90 .chain([TaskTemplate {
91 label: "package script $ZED_CUSTOM_script".to_owned(),
92 command: command.clone(),
93 args: vec![
94 "run".into(),
95 VariableName::Custom("script".into()).template_value(),
96 ],
97 cwd: Some(VariableName::Dirname.template_value()),
98 tags: vec!["package-script".into()],
99 ..TaskTemplate::default()
100 }])
101 .collect()
102 } else if is_composer_json {
103 serde_json_lenient::Value::from_str(&contents.text)
104 .ok()?
105 .get("scripts")?
106 .as_object()?
107 .keys()
108 .map(|key| TaskTemplate {
109 label: format!("run {key}"),
110 command: "composer".to_owned(),
111 args: vec!["-d".into(), "$ZED_DIRNAME".into(), key.into()],
112 ..TaskTemplate::default()
113 })
114 .chain([TaskTemplate {
115 label: "composer script $ZED_CUSTOM_script".to_owned(),
116 command: "composer".to_owned(),
117 args: vec![
118 "-d".into(),
119 "$ZED_DIRNAME".into(),
120 VariableName::Custom("script".into()).template_value(),
121 ],
122 tags: vec!["composer-script".into()],
123 ..TaskTemplate::default()
124 }])
125 .collect()
126 } else {
127 vec![]
128 };
129
130 Some(TaskTemplates(task_templates))
131 })
132 }
133}
134
135fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
136 vec![server_path.into(), "--stdio".into()]
137}
138
139pub struct JsonLspAdapter {
140 languages: Arc<LanguageRegistry>,
141 node: NodeRuntime,
142}
143
144impl JsonLspAdapter {
145 const PACKAGE_NAME: &str = "vscode-langservers-extracted";
146
147 pub fn new(languages: Arc<LanguageRegistry>, node: NodeRuntime) -> Self {
148 Self { languages, node }
149 }
150}
151
152impl LspInstaller for JsonLspAdapter {
153 type BinaryVersion = Version;
154
155 async fn fetch_latest_server_version(
156 &self,
157 _: &Arc<dyn LspAdapterDelegate>,
158 _: bool,
159 _: &mut AsyncApp,
160 ) -> Result<Self::BinaryVersion> {
161 self.node
162 .npm_package_latest_version(Self::PACKAGE_NAME)
163 .await
164 }
165
166 async fn check_if_user_installed(
167 &self,
168 delegate: &Arc<dyn LspAdapterDelegate>,
169 _: Option<Toolchain>,
170 _: &AsyncApp,
171 ) -> Option<LanguageServerBinary> {
172 let path = delegate
173 .which("vscode-json-language-server".as_ref())
174 .await?;
175 let env = delegate.shell_env().await;
176
177 Some(LanguageServerBinary {
178 path,
179 env: Some(env),
180 arguments: vec!["--stdio".into()],
181 })
182 }
183
184 fn check_if_version_installed(
185 &self,
186 version: &Self::BinaryVersion,
187 container_dir: &PathBuf,
188 _: &Arc<dyn LspAdapterDelegate>,
189 ) -> impl Send + Future<Output = Option<LanguageServerBinary>> + use<> {
190 let node = self.node.clone();
191 let version = version.clone();
192 let container_dir = container_dir.clone();
193
194 async move {
195 let server_path = container_dir.join(SERVER_PATH);
196
197 let should_install_language_server = node
198 .should_install_npm_package(
199 Self::PACKAGE_NAME,
200 &server_path,
201 &container_dir,
202 VersionStrategy::Latest(&version),
203 )
204 .await;
205
206 if should_install_language_server {
207 None
208 } else {
209 Some(LanguageServerBinary {
210 path: node.binary_path().await.ok()?,
211 env: None,
212 arguments: server_binary_arguments(&server_path),
213 })
214 }
215 }
216 }
217
218 fn fetch_server_binary(
219 &self,
220 _latest_version: Self::BinaryVersion,
221 container_dir: PathBuf,
222 _: &Arc<dyn LspAdapterDelegate>,
223 ) -> impl Send + Future<Output = Result<LanguageServerBinary>> + use<> {
224 let node = self.node.clone();
225
226 async move {
227 let server_path = container_dir.join(SERVER_PATH);
228
229 node.npm_install_latest_packages(&container_dir, &[Self::PACKAGE_NAME])
230 .await?;
231
232 Ok(LanguageServerBinary {
233 path: node.binary_path().await?,
234 env: None,
235 arguments: server_binary_arguments(&server_path),
236 })
237 }
238 }
239
240 async fn cached_server_binary(
241 &self,
242 container_dir: PathBuf,
243 _: &dyn LspAdapterDelegate,
244 ) -> Option<LanguageServerBinary> {
245 get_cached_server_binary(container_dir, &self.node).await
246 }
247}
248
249#[async_trait(?Send)]
250impl LspAdapter for JsonLspAdapter {
251 fn name(&self) -> LanguageServerName {
252 LanguageServerName("json-language-server".into())
253 }
254
255 async fn initialization_options(
256 self: Arc<Self>,
257 _: &Arc<dyn LspAdapterDelegate>,
258 _: &mut AsyncApp,
259 ) -> Result<Option<serde_json::Value>> {
260 Ok(Some(json!({
261 "provideFormatter": true
262 })))
263 }
264
265 async fn workspace_configuration(
266 self: Arc<Self>,
267 delegate: &Arc<dyn LspAdapterDelegate>,
268 _: Option<Toolchain>,
269 requested_uri: Option<Uri>,
270 cx: &mut AsyncApp,
271 ) -> Result<Value> {
272 let requested_path = requested_uri.as_ref().and_then(|uri| {
273 (uri.scheme() == "file")
274 .then(|| uri.to_file_path().ok())
275 .flatten()
276 });
277 let path_in_worktree = requested_path
278 .as_ref()
279 .and_then(|abs_path| {
280 let rel_path = abs_path.strip_prefix(delegate.worktree_root_path()).ok()?;
281 RelPath::new(rel_path, PathStyle::local()).ok()
282 })
283 .unwrap_or_else(|| Cow::Borrowed(RelPath::empty()));
284 let mut config = cx.update(|cx| {
285 let schemas = json_schema_store::all_schema_file_associations(
286 &self.languages,
287 Some(SettingsLocation {
288 worktree_id: delegate.worktree_id(),
289 path: path_in_worktree.as_ref(),
290 }),
291 cx,
292 );
293
294 // This can be viewed via `dev: open language server logs` -> `json-language-server` ->
295 // `Server Info`
296 serde_json::json!({
297 "json": {
298 "format": {
299 "enable": true,
300 },
301 "validate": {
302 "enable": true,
303 },
304 "schemas": schemas
305 }
306 })
307 });
308
309 if let Some(proxy_settings) = cx.update(|cx| {
310 json_schema_proxy_settings(cx.http_client().proxy().map(ToString::to_string))
311 }) {
312 merge_json_value_into(proxy_settings, &mut config);
313 }
314
315 let project_options = cx.update(|cx| {
316 language_server_settings(delegate.as_ref(), &self.name(), cx)
317 .and_then(|s| worktree_root(delegate, s.settings.clone()))
318 });
319
320 if let Some(override_options) = project_options {
321 merge_json_value_into(override_options, &mut config);
322 }
323
324 Ok(config)
325 }
326
327 fn language_ids(&self) -> HashMap<LanguageName, String> {
328 [
329 (LanguageName::new_static("JSON"), "json".into()),
330 (LanguageName::new_static("JSONC"), "jsonc".into()),
331 ]
332 .into_iter()
333 .collect()
334 }
335
336 fn is_primary_zed_json_schema_adapter(&self) -> bool {
337 true
338 }
339}
340
341fn worktree_root(delegate: &Arc<dyn LspAdapterDelegate>, settings: Option<Value>) -> Option<Value> {
342 let Some(Value::Object(mut settings_map)) = settings else {
343 return settings;
344 };
345
346 let Some(Value::Object(json_config)) = settings_map.get_mut("json") else {
347 return Some(Value::Object(settings_map));
348 };
349
350 let Some(Value::Array(schemas)) = json_config.get_mut("schemas") else {
351 return Some(Value::Object(settings_map));
352 };
353
354 for schema in schemas.iter_mut() {
355 let Value::Object(schema_map) = schema else {
356 continue;
357 };
358 let Some(Value::String(url)) = schema_map.get_mut("url") else {
359 continue;
360 };
361
362 if !url.starts_with(".") && !url.starts_with("~") {
363 continue;
364 }
365
366 *url = delegate
367 .resolve_relative_path(url.clone().into())
368 .to_string_lossy()
369 .into_owned();
370 }
371
372 Some(Value::Object(settings_map))
373}
374
375fn json_schema_proxy_settings(proxy: Option<String>) -> Option<Value> {
376 proxy.map(|proxy| {
377 json!({
378 "http": {
379 "proxy": proxy,
380 }
381 })
382 })
383}
384
385async fn get_cached_server_binary(
386 container_dir: PathBuf,
387 node: &NodeRuntime,
388) -> Option<LanguageServerBinary> {
389 maybe!(async {
390 let server_path = container_dir.join(SERVER_PATH);
391 anyhow::ensure!(
392 server_path.exists(),
393 "missing executable in directory {server_path:?}"
394 );
395 Ok(LanguageServerBinary {
396 path: node.binary_path().await?,
397 env: None,
398 arguments: server_binary_arguments(&server_path),
399 })
400 })
401 .await
402 .log_err()
403}
404
405#[cfg(test)]
406mod tests {
407 use serde_json::json;
408
409 use super::json_schema_proxy_settings;
410
411 #[test]
412 fn test_json_schema_proxy_settings_includes_proxy() {
413 assert_eq!(
414 json_schema_proxy_settings(Some("http://proxy.example:8080".to_string())),
415 Some(json!({
416 "http": {
417 "proxy": "http://proxy.example:8080",
418 }
419 }))
420 );
421 }
422
423 #[test]
424 fn test_json_schema_proxy_settings_ignores_missing_proxy() {
425 assert_eq!(json_schema_proxy_settings(None), None);
426 }
427}
428
429pub struct NodeVersionAdapter;
430
431impl NodeVersionAdapter {
432 const SERVER_NAME: LanguageServerName =
433 LanguageServerName::new_static("package-version-server");
434}
435
436impl LspInstaller for NodeVersionAdapter {
437 type BinaryVersion = GitHubLspBinaryVersion;
438
439 async fn fetch_latest_server_version(
440 &self,
441 delegate: &Arc<dyn LspAdapterDelegate>,
442 _: bool,
443 _: &mut AsyncApp,
444 ) -> Result<GitHubLspBinaryVersion> {
445 let release = latest_github_release(
446 "zed-industries/package-version-server",
447 true,
448 false,
449 delegate.http_client(),
450 )
451 .await?;
452 let os = match consts::OS {
453 "macos" => "apple-darwin",
454 "linux" => "unknown-linux-gnu",
455 "windows" => "pc-windows-msvc",
456 other => bail!("Running on unsupported os: {other}"),
457 };
458 let suffix = if consts::OS == "windows" {
459 ".zip"
460 } else {
461 ".tar.gz"
462 };
463 let asset_name = format!("{}-{}-{os}{suffix}", Self::SERVER_NAME, consts::ARCH);
464 let asset = release
465 .assets
466 .iter()
467 .find(|asset| asset.name == asset_name)
468 .with_context(|| format!("no asset found matching `{asset_name:?}`"))?;
469 Ok(GitHubLspBinaryVersion {
470 name: release.tag_name,
471 url: asset.browser_download_url.clone(),
472 digest: asset.digest.clone(),
473 })
474 }
475
476 async fn check_if_user_installed(
477 &self,
478 delegate: &Arc<dyn LspAdapterDelegate>,
479 _: Option<Toolchain>,
480 _: &AsyncApp,
481 ) -> Option<LanguageServerBinary> {
482 let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;
483 Some(LanguageServerBinary {
484 path,
485 env: None,
486 arguments: Default::default(),
487 })
488 }
489
490 fn fetch_server_binary(
491 &self,
492 latest_version: GitHubLspBinaryVersion,
493 container_dir: PathBuf,
494 delegate: &Arc<dyn LspAdapterDelegate>,
495 ) -> impl Send + Future<Output = Result<LanguageServerBinary>> + use<> {
496 let delegate = delegate.clone();
497
498 async move {
499 let version = &latest_version;
500 let destination_path = container_dir.join(format!(
501 "{}-{}{}",
502 Self::SERVER_NAME,
503 version.name,
504 std::env::consts::EXE_SUFFIX
505 ));
506 let destination_container_path =
507 container_dir.join(format!("{}-{}-tmp", Self::SERVER_NAME, version.name));
508 if fs::metadata(&destination_path).await.is_err() {
509 let mut response = delegate
510 .http_client()
511 .get(&version.url, Default::default(), true)
512 .await
513 .context("downloading release")?;
514 if version.url.ends_with(".zip") {
515 extract_zip(&destination_container_path, response.body_mut()).await?;
516 } else if version.url.ends_with(".tar.gz") {
517 let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
518 let archive = Archive::new(decompressed_bytes);
519 archive.unpack(&destination_container_path).await?;
520 }
521
522 fs::copy(
523 destination_container_path.join(format!(
524 "{}{}",
525 Self::SERVER_NAME,
526 std::env::consts::EXE_SUFFIX
527 )),
528 &destination_path,
529 )
530 .await?;
531 remove_matching(&container_dir, |entry| entry != destination_path).await;
532 }
533 Ok(LanguageServerBinary {
534 path: destination_path,
535 env: None,
536 arguments: Default::default(),
537 })
538 }
539 }
540
541 async fn cached_server_binary(
542 &self,
543 container_dir: PathBuf,
544 _delegate: &dyn LspAdapterDelegate,
545 ) -> Option<LanguageServerBinary> {
546 get_cached_version_server_binary(container_dir).await
547 }
548}
549
550#[async_trait(?Send)]
551impl LspAdapter for NodeVersionAdapter {
552 fn name(&self) -> LanguageServerName {
553 Self::SERVER_NAME
554 }
555}
556
557async fn get_cached_version_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
558 maybe!(async {
559 let mut last = None;
560 let mut entries = fs::read_dir(&container_dir).await?;
561 while let Some(entry) = entries.next().await {
562 last = Some(entry?.path());
563 }
564
565 anyhow::Ok(LanguageServerBinary {
566 path: last.context("no cached binary")?,
567 env: None,
568 arguments: Default::default(),
569 })
570 })
571 .await
572 .log_err()
573}
574