Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T03:31:23.471Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

json.rs

574 lines · 18.4 KB · rust
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
Served at tenant.openagents/omega Member data and write actions are omitted.