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