deno.rs

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