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