deno.rs

  1use anyhow::{anyhow, bail, Context, Result};
  2use async_trait::async_trait;
  3use collections::HashMap;
  4use futures::StreamExt;
  5use gpui::AppContext;
  6use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
  7use lsp::{CodeActionKind, LanguageServerBinary};
  8use schemars::JsonSchema;
  9use serde_derive::{Deserialize, Serialize};
 10use serde_json::json;
 11use settings::{Settings, SettingsSources};
 12use smol::{fs, fs::File};
 13use std::{any::Any, env::consts, ffi::OsString, path::PathBuf, sync::Arc};
 14use util::{
 15    fs::remove_matching,
 16    github::{latest_github_release, GitHubLspBinaryVersion},
 17    maybe, ResultExt,
 18};
 19
 20#[derive(Clone, Serialize, Deserialize, JsonSchema)]
 21pub struct DenoSettings {
 22    pub enable: bool,
 23}
 24
 25#[derive(Clone, Serialize, Default, Deserialize, JsonSchema)]
 26pub struct DenoSettingsContent {
 27    enable: Option<bool>,
 28}
 29
 30impl Settings for DenoSettings {
 31    const KEY: Option<&'static str> = Some("deno");
 32
 33    type FileContent = DenoSettingsContent;
 34
 35    fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
 36        sources.json_merge()
 37    }
 38}
 39
 40fn deno_server_binary_arguments() -> Vec<OsString> {
 41    vec!["lsp".into()]
 42}
 43
 44pub struct DenoLspAdapter {}
 45
 46impl DenoLspAdapter {
 47    pub fn new() -> Self {
 48        DenoLspAdapter {}
 49    }
 50}
 51
 52#[async_trait(?Send)]
 53impl LspAdapter for DenoLspAdapter {
 54    fn name(&self) -> LanguageServerName {
 55        LanguageServerName("deno-language-server".into())
 56    }
 57
 58    async fn fetch_latest_server_version(
 59        &self,
 60        delegate: &dyn LspAdapterDelegate,
 61    ) -> Result<Box<dyn 'static + Send + Any>> {
 62        let release =
 63            latest_github_release("denoland/deno", true, false, delegate.http_client()).await?;
 64        let os = match consts::OS {
 65            "macos" => "apple-darwin",
 66            "linux" => "unknown-linux-gnu",
 67            "windows" => "pc-windows-msvc",
 68            other => bail!("Running on unsupported os: {other}"),
 69        };
 70        let asset_name = format!("deno-{}-{os}.zip", consts::ARCH);
 71        let asset = release
 72            .assets
 73            .iter()
 74            .find(|asset| asset.name == asset_name)
 75            .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
 76        let version = GitHubLspBinaryVersion {
 77            name: release.tag_name,
 78            url: asset.browser_download_url.clone(),
 79        };
 80        Ok(Box::new(version) as Box<_>)
 81    }
 82
 83    async fn fetch_server_binary(
 84        &self,
 85        version: Box<dyn 'static + Send + Any>,
 86        container_dir: PathBuf,
 87        delegate: &dyn LspAdapterDelegate,
 88    ) -> Result<LanguageServerBinary> {
 89        let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
 90        let zip_path = container_dir.join(format!("deno_{}.zip", version.name));
 91        let version_dir = container_dir.join(format!("deno_{}", version.name));
 92        let binary_path = version_dir.join("deno");
 93
 94        if fs::metadata(&binary_path).await.is_err() {
 95            let mut response = delegate
 96                .http_client()
 97                .get(&version.url, Default::default(), true)
 98                .await
 99                .context("error downloading release")?;
100            let mut file = File::create(&zip_path).await?;
101            if !response.status().is_success() {
102                Err(anyhow!(
103                    "download failed with status {}",
104                    response.status().to_string()
105                ))?;
106            }
107            futures::io::copy(response.body_mut(), &mut file).await?;
108
109            let unzip_status = smol::process::Command::new("unzip")
110                .current_dir(&container_dir)
111                .arg(&zip_path)
112                .arg("-d")
113                .arg(&version_dir)
114                .output()
115                .await?
116                .status;
117            if !unzip_status.success() {
118                Err(anyhow!("failed to unzip deno archive"))?;
119            }
120
121            remove_matching(&container_dir, |entry| entry != version_dir).await;
122        }
123
124        Ok(LanguageServerBinary {
125            path: binary_path,
126            env: None,
127            arguments: deno_server_binary_arguments(),
128        })
129    }
130
131    async fn cached_server_binary(
132        &self,
133        container_dir: PathBuf,
134        _: &dyn LspAdapterDelegate,
135    ) -> Option<LanguageServerBinary> {
136        get_cached_server_binary(container_dir).await
137    }
138
139    async fn installation_test_binary(
140        &self,
141        container_dir: PathBuf,
142    ) -> Option<LanguageServerBinary> {
143        get_cached_server_binary(container_dir).await
144    }
145
146    fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
147        Some(vec![
148            CodeActionKind::QUICKFIX,
149            CodeActionKind::REFACTOR,
150            CodeActionKind::REFACTOR_EXTRACT,
151            CodeActionKind::SOURCE,
152        ])
153    }
154
155    async fn label_for_completion(
156        &self,
157        item: &lsp::CompletionItem,
158        language: &Arc<language::Language>,
159    ) -> Option<language::CodeLabel> {
160        use lsp::CompletionItemKind as Kind;
161        let len = item.label.len();
162        let grammar = language.grammar()?;
163        let highlight_id = match item.kind? {
164            Kind::CLASS | Kind::INTERFACE => grammar.highlight_id_for_name("type"),
165            Kind::CONSTRUCTOR => grammar.highlight_id_for_name("type"),
166            Kind::CONSTANT => grammar.highlight_id_for_name("constant"),
167            Kind::FUNCTION | Kind::METHOD => grammar.highlight_id_for_name("function"),
168            Kind::PROPERTY | Kind::FIELD => grammar.highlight_id_for_name("property"),
169            _ => None,
170        }?;
171
172        let text = match &item.detail {
173            Some(detail) => format!("{} {}", item.label, detail),
174            None => item.label.clone(),
175        };
176
177        Some(language::CodeLabel {
178            text,
179            runs: vec![(0..len, highlight_id)],
180            filter_range: 0..len,
181        })
182    }
183
184    async fn initialization_options(
185        self: Arc<Self>,
186        _: &Arc<dyn LspAdapterDelegate>,
187    ) -> Result<Option<serde_json::Value>> {
188        Ok(Some(json!({
189            "provideFormatter": true,
190        })))
191    }
192
193    fn language_ids(&self) -> HashMap<String, String> {
194        HashMap::from_iter([
195            ("TypeScript".into(), "typescript".into()),
196            ("JavaScript".into(), "javascript".into()),
197            ("TSX".into(), "typescriptreact".into()),
198        ])
199    }
200}
201
202async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
203    maybe!(async {
204        let mut last = None;
205        let mut entries = fs::read_dir(&container_dir).await?;
206        while let Some(entry) = entries.next().await {
207            last = Some(entry?.path());
208        }
209
210        match last {
211            Some(path) if path.is_dir() => {
212                let binary = path.join("deno");
213                if fs::metadata(&binary).await.is_ok() {
214                    return Ok(LanguageServerBinary {
215                        path: binary,
216                        env: None,
217                        arguments: deno_server_binary_arguments(),
218                    });
219                }
220            }
221            _ => {}
222        }
223
224        Err(anyhow!("no cached binary"))
225    })
226    .await
227    .log_err()
228}