From 81aac492bdd07efa426117f8ef704823cb10f402 Mon Sep 17 00:00:00 2001 From: Lino Le Van <11367844+lino-levan@users.noreply.github.com> Date: Fri, 26 Jan 2024 02:44:51 +0100 Subject: [PATCH] Add Deno LSP support (#5816) This PR adds support for the deno LSP. Should be reviewable now. Release Notes: - Added support for the Deno LSP ([#5361](https://github.com/zed-industries/zed/issues/5361)). --- assets/settings/default.json | 4 + crates/zed/src/languages.rs | 83 ++++++++---- crates/zed/src/languages/deno.rs | 223 +++++++++++++++++++++++++++++++ 3 files changed, 283 insertions(+), 27 deletions(-) create mode 100644 crates/zed/src/languages/deno.rs diff --git a/assets/settings/default.json b/assets/settings/default.json index a920a9c68a..01165c9e31 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -447,6 +447,10 @@ // "lsp": "elixir_ls" }, + // Settings specific to our deno integration + "deno": { + "enable": false + }, // Different settings for specific languages. "languages": { "Plain Text": { diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index 0ad95b9fde..bba9137708 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -7,10 +7,11 @@ use settings::Settings; use std::{borrow::Cow, str, sync::Arc}; use util::{asset_str, paths::PLUGINS_DIR}; -use self::elixir::ElixirSettings; +use self::{deno::DenoSettings, elixir::ElixirSettings}; mod c; mod css; +mod deno; mod elixir; mod gleam; mod go; @@ -51,6 +52,7 @@ pub fn init( cx: &mut AppContext, ) { ElixirSettings::register(cx); + DenoSettings::register(cx); let language = |name, grammar, adapters| { languages.register(name, load_config(name), grammar, adapters, load_queries) @@ -140,32 +142,59 @@ pub fn init( vec![Arc::new(rust::RustLspAdapter)], ); language("toml", tree_sitter_toml::language(), vec![]); - language( - "tsx", - tree_sitter_typescript::language_tsx(), - vec![ - Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())), - Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())), - Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), - ], - ); - language( - "typescript", - tree_sitter_typescript::language_typescript(), - vec![ - Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())), - Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())), - ], - ); - language( - "javascript", - tree_sitter_typescript::language_tsx(), - vec![ - Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())), - Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())), - Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), - ], - ); + match &DenoSettings::get(None, cx).enable { + true => { + language( + "tsx", + tree_sitter_typescript::language_tsx(), + vec![ + Arc::new(deno::DenoLspAdapter::new()), + Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), + ], + ); + language( + "typescript", + tree_sitter_typescript::language_typescript(), + vec![Arc::new(deno::DenoLspAdapter::new())], + ); + language( + "javascript", + tree_sitter_typescript::language_tsx(), + vec![ + Arc::new(deno::DenoLspAdapter::new()), + Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), + ], + ); + } + false => { + language( + "tsx", + tree_sitter_typescript::language_tsx(), + vec![ + Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())), + Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())), + Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), + ], + ); + language( + "typescript", + tree_sitter_typescript::language_typescript(), + vec![ + Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())), + Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())), + ], + ); + language( + "javascript", + tree_sitter_typescript::language_tsx(), + vec![ + Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())), + Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())), + Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), + ], + ); + } + } language( "html", tree_sitter_html::language(), diff --git a/crates/zed/src/languages/deno.rs b/crates/zed/src/languages/deno.rs new file mode 100644 index 0000000000..671248007a --- /dev/null +++ b/crates/zed/src/languages/deno.rs @@ -0,0 +1,223 @@ +use anyhow::{anyhow, Context, Result}; +use async_trait::async_trait; +use collections::HashMap; +use futures::StreamExt; +use language::{LanguageServerName, LspAdapter, LspAdapterDelegate}; +use lsp::{CodeActionKind, LanguageServerBinary}; +use schemars::JsonSchema; +use serde_derive::{Deserialize, Serialize}; +use serde_json::json; +use settings::Settings; +use smol::{fs, fs::File}; +use std::{any::Any, env::consts, ffi::OsString, path::PathBuf, sync::Arc}; +use util::{fs::remove_matching, github::latest_github_release}; +use util::{github::GitHubLspBinaryVersion, ResultExt}; + +#[derive(Clone, Serialize, Deserialize, JsonSchema)] +pub struct DenoSettings { + pub enable: bool, +} + +#[derive(Clone, Serialize, Default, Deserialize, JsonSchema)] +pub struct DenoSettingsContent { + enable: Option, +} + +impl Settings for DenoSettings { + const KEY: Option<&'static str> = Some("deno"); + + type FileContent = DenoSettingsContent; + + fn load( + default_value: &Self::FileContent, + user_values: &[&Self::FileContent], + _: &mut gpui::AppContext, + ) -> Result + where + Self: Sized, + { + Self::load_via_json_merge(default_value, user_values) + } +} + +fn deno_server_binary_arguments() -> Vec { + vec!["lsp".into()] +} + +pub struct DenoLspAdapter {} + +impl DenoLspAdapter { + pub fn new() -> Self { + DenoLspAdapter {} + } +} + +#[async_trait] +impl LspAdapter for DenoLspAdapter { + fn name(&self) -> LanguageServerName { + LanguageServerName("deno-language-server".into()) + } + + fn short_name(&self) -> &'static str { + "deno-ts" + } + + async fn fetch_latest_server_version( + &self, + delegate: &dyn LspAdapterDelegate, + ) -> Result> { + let release = latest_github_release("denoland/deno", false, delegate.http_client()).await?; + let asset_name = format!("deno-{}-apple-darwin.zip", consts::ARCH); + let asset = release + .assets + .iter() + .find(|asset| asset.name == asset_name) + .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?; + let version = GitHubLspBinaryVersion { + name: release.name, + url: asset.browser_download_url.clone(), + }; + Ok(Box::new(version) as Box<_>) + } + + async fn fetch_server_binary( + &self, + version: Box, + container_dir: PathBuf, + delegate: &dyn LspAdapterDelegate, + ) -> Result { + let version = version.downcast::().unwrap(); + let zip_path = container_dir.join(format!("deno_{}.zip", version.name)); + let version_dir = container_dir.join(format!("deno_{}", version.name)); + let binary_path = version_dir.join("deno"); + + if fs::metadata(&binary_path).await.is_err() { + let mut response = delegate + .http_client() + .get(&version.url, Default::default(), true) + .await + .context("error downloading release")?; + let mut file = File::create(&zip_path).await?; + if !response.status().is_success() { + Err(anyhow!( + "download failed with status {}", + response.status().to_string() + ))?; + } + futures::io::copy(response.body_mut(), &mut file).await?; + + let unzip_status = smol::process::Command::new("unzip") + .current_dir(&container_dir) + .arg(&zip_path) + .arg("-d") + .arg(&version_dir) + .output() + .await? + .status; + if !unzip_status.success() { + Err(anyhow!("failed to unzip deno archive"))?; + } + + remove_matching(&container_dir, |entry| entry != version_dir).await; + } + + Ok(LanguageServerBinary { + path: binary_path, + arguments: deno_server_binary_arguments(), + }) + } + + async fn cached_server_binary( + &self, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + get_cached_server_binary(container_dir).await + } + + async fn installation_test_binary( + &self, + container_dir: PathBuf, + ) -> Option { + get_cached_server_binary(container_dir).await + } + + fn code_action_kinds(&self) -> Option> { + Some(vec![ + CodeActionKind::QUICKFIX, + CodeActionKind::REFACTOR, + CodeActionKind::REFACTOR_EXTRACT, + CodeActionKind::SOURCE, + ]) + } + + async fn label_for_completion( + &self, + item: &lsp::CompletionItem, + language: &Arc, + ) -> Option { + use lsp::CompletionItemKind as Kind; + let len = item.label.len(); + let grammar = language.grammar()?; + let highlight_id = match item.kind? { + Kind::CLASS | Kind::INTERFACE => grammar.highlight_id_for_name("type"), + Kind::CONSTRUCTOR => grammar.highlight_id_for_name("type"), + Kind::CONSTANT => grammar.highlight_id_for_name("constant"), + Kind::FUNCTION | Kind::METHOD => grammar.highlight_id_for_name("function"), + Kind::PROPERTY | Kind::FIELD => grammar.highlight_id_for_name("property"), + _ => None, + }?; + + let text = match &item.detail { + Some(detail) => format!("{} {}", item.label, detail), + None => item.label.clone(), + }; + + Some(language::CodeLabel { + text, + runs: vec![(0..len, highlight_id)], + filter_range: 0..len, + }) + } + + fn initialization_options(&self) -> Option { + Some(json!({ + "provideFormatter": true, + })) + } + + fn language_ids(&self) -> HashMap { + HashMap::from_iter([ + ("TypeScript".into(), "typescript".into()), + ("JavaScript".into(), "javascript".into()), + ("TSX".into(), "typescriptreact".into()), + ]) + } +} + +async fn get_cached_server_binary(container_dir: PathBuf) -> Option { + (|| async move { + let mut last = None; + let mut entries = fs::read_dir(&container_dir).await?; + while let Some(entry) = entries.next().await { + last = Some(entry?.path()); + } + + match last { + Some(path) if path.is_dir() => { + let binary = path.join("deno"); + if fs::metadata(&binary).await.is_ok() { + return Ok(LanguageServerBinary { + path: binary, + arguments: deno_server_binary_arguments(), + }); + } + } + _ => {} + } + + Err(anyhow!("no cached binary")) + })() + .await + .log_err() +}