Extract Deno extension (#10912)

Marshall Bowers created

This PR extracts Deno support into an extension and removes the built-in
Deno support from Zed.

When using the Deno extension, you'll want to add the following to your
settings to disable the built-in TypeScript and ESLint language servers
so that they don't conflict with Deno's functionality:

```json
{
  "languages": {
    "TypeScript": {
      "language_servers": ["deno", "!typescript-language-server", "!eslint", "..."]
    },
    "TSX": {
      "language_servers": ["deno", "!typescript-language-server", "!eslint", "..."]
    }
  }
}

```

Release Notes:

- Removed built-in support for Deno, in favor of making it available as
an extension.

Change summary

Cargo.lock                     |   7 +
Cargo.toml                     |   1 
assets/settings/default.json   |   4 
crates/languages/src/deno.rs   | 228 ------------------------------------
crates/languages/src/lib.rs    | 119 ++++++-----------
extensions/deno/Cargo.toml     |  16 ++
extensions/deno/LICENSE-APACHE |   1 
extensions/deno/extension.toml |  13 ++
extensions/deno/src/deno.rs    | 154 ++++++++++++++++++++++++
9 files changed, 236 insertions(+), 307 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -12728,6 +12728,13 @@ dependencies = [
  "zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "zed_deno"
+version = "0.0.1"
+dependencies = [
+ "zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "zed_elm"
 version = "0.0.1"

Cargo.toml 🔗

@@ -109,6 +109,7 @@ members = [
     "extensions/clojure",
     "extensions/csharp",
     "extensions/dart",
+    "extensions/deno",
     "extensions/elm",
     "extensions/emmet",
     "extensions/erlang",

assets/settings/default.json 🔗

@@ -576,10 +576,6 @@
     //
     "lsp": "elixir_ls"
   },
-  // Settings specific to our deno integration
-  "deno": {
-    "enable": false
-  },
   "code_actions_on_format": {},
   // An object whose keys are language names, and whose values
   // are arrays of filenames or extensions of files that should

crates/languages/src/deno.rs 🔗

@@ -1,228 +0,0 @@
-use anyhow::{anyhow, bail, Context, Result};
-use async_trait::async_trait;
-use collections::HashMap;
-use futures::StreamExt;
-use gpui::AppContext;
-use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
-use lsp::{CodeActionKind, LanguageServerBinary};
-use schemars::JsonSchema;
-use serde_derive::{Deserialize, Serialize};
-use serde_json::json;
-use settings::{Settings, SettingsSources};
-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, GitHubLspBinaryVersion},
-    maybe, ResultExt,
-};
-
-#[derive(Clone, Serialize, Deserialize, JsonSchema)]
-pub struct DenoSettings {
-    pub enable: bool,
-}
-
-#[derive(Clone, Serialize, Default, Deserialize, JsonSchema)]
-pub struct DenoSettingsContent {
-    enable: Option<bool>,
-}
-
-impl Settings for DenoSettings {
-    const KEY: Option<&'static str> = Some("deno");
-
-    type FileContent = DenoSettingsContent;
-
-    fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
-        sources.json_merge()
-    }
-}
-
-fn deno_server_binary_arguments() -> Vec<OsString> {
-    vec!["lsp".into()]
-}
-
-pub struct DenoLspAdapter {}
-
-impl DenoLspAdapter {
-    pub fn new() -> Self {
-        DenoLspAdapter {}
-    }
-}
-
-#[async_trait(?Send)]
-impl LspAdapter for DenoLspAdapter {
-    fn name(&self) -> LanguageServerName {
-        LanguageServerName("deno-language-server".into())
-    }
-
-    async fn fetch_latest_server_version(
-        &self,
-        delegate: &dyn LspAdapterDelegate,
-    ) -> Result<Box<dyn 'static + Send + Any>> {
-        let release =
-            latest_github_release("denoland/deno", true, false, delegate.http_client()).await?;
-        let os = match consts::OS {
-            "macos" => "apple-darwin",
-            "linux" => "unknown-linux-gnu",
-            "windows" => "pc-windows-msvc",
-            other => bail!("Running on unsupported os: {other}"),
-        };
-        let asset_name = format!("deno-{}-{os}.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.tag_name,
-            url: asset.browser_download_url.clone(),
-        };
-        Ok(Box::new(version) as Box<_>)
-    }
-
-    async fn fetch_server_binary(
-        &self,
-        version: Box<dyn 'static + Send + Any>,
-        container_dir: PathBuf,
-        delegate: &dyn LspAdapterDelegate,
-    ) -> Result<LanguageServerBinary> {
-        let version = version.downcast::<GitHubLspBinaryVersion>().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,
-            env: None,
-            arguments: deno_server_binary_arguments(),
-        })
-    }
-
-    async fn cached_server_binary(
-        &self,
-        container_dir: PathBuf,
-        _: &dyn LspAdapterDelegate,
-    ) -> Option<LanguageServerBinary> {
-        get_cached_server_binary(container_dir).await
-    }
-
-    async fn installation_test_binary(
-        &self,
-        container_dir: PathBuf,
-    ) -> Option<LanguageServerBinary> {
-        get_cached_server_binary(container_dir).await
-    }
-
-    fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
-        Some(vec![
-            CodeActionKind::QUICKFIX,
-            CodeActionKind::REFACTOR,
-            CodeActionKind::REFACTOR_EXTRACT,
-            CodeActionKind::SOURCE,
-        ])
-    }
-
-    async fn label_for_completion(
-        &self,
-        item: &lsp::CompletionItem,
-        language: &Arc<language::Language>,
-    ) -> Option<language::CodeLabel> {
-        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,
-        })
-    }
-
-    async fn initialization_options(
-        self: Arc<Self>,
-        _: &Arc<dyn LspAdapterDelegate>,
-    ) -> Result<Option<serde_json::Value>> {
-        Ok(Some(json!({
-            "provideFormatter": true,
-        })))
-    }
-
-    fn language_ids(&self) -> HashMap<String, String> {
-        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<LanguageServerBinary> {
-    maybe!(async {
-        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,
-                        env: None,
-                        arguments: deno_server_binary_arguments(),
-                    });
-                }
-            }
-            _ => {}
-        }
-
-        Err(anyhow!("no cached binary"))
-    })
-    .await
-    .log_err()
-}

crates/languages/src/lib.rs 🔗

@@ -13,12 +13,11 @@ use crate::{
     rust::RustContextProvider,
 };
 
-use self::{deno::DenoSettings, elixir::ElixirSettings};
+use self::elixir::ElixirSettings;
 
 mod bash;
 mod c;
 mod css;
-mod deno;
 mod elixir;
 mod go;
 mod json;
@@ -49,7 +48,6 @@ pub fn init(
     cx: &mut AppContext,
 ) {
     ElixirSettings::register(cx);
-    DenoSettings::register(cx);
 
     languages.register_native_grammars([
         ("bash", tree_sitter_bash::language()),
@@ -193,58 +191,33 @@ pub fn init(
         vec![Arc::new(rust::RustLspAdapter)],
         RustContextProvider
     );
-    match &DenoSettings::get(None, cx).enable {
-        true => {
-            language!(
-                "tsx",
-                vec![
-                    Arc::new(deno::DenoLspAdapter::new()),
-                    Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
-                ]
-            );
-            language!("typescript", vec![Arc::new(deno::DenoLspAdapter::new())]);
-            language!(
-                "javascript",
-                vec![
-                    Arc::new(deno::DenoLspAdapter::new()),
-                    Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
-                ]
-            );
-            language!("jsdoc", vec![Arc::new(deno::DenoLspAdapter::new())]);
-        }
-        false => {
-            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",
-                vec![
-                    Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())),
-                    Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())),
-                ]
-            );
-            language!(
-                "javascript",
-                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!(
-                "jsdoc",
-                vec![Arc::new(typescript::TypeScriptLspAdapter::new(
-                    node_runtime.clone(),
-                ))]
-            );
-        }
-    }
-
+    language!(
+        "tsx",
+        vec![
+            Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())),
+            Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())),
+        ]
+    );
+    language!(
+        "typescript",
+        vec![
+            Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())),
+            Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())),
+        ]
+    );
+    language!(
+        "javascript",
+        vec![
+            Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())),
+            Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())),
+        ]
+    );
+    language!(
+        "jsdoc",
+        vec![Arc::new(typescript::TypeScriptLspAdapter::new(
+            node_runtime.clone(),
+        ))]
+    );
     language!("ruby", vec![Arc::new(ruby::RubyLanguageServer)]);
     language!(
         "erb",
@@ -260,26 +233,22 @@ pub fn init(
     );
     language!("proto");
 
-    languages.register_secondary_lsp_adapter(
-        "Astro".into(),
-        Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
-    );
-    languages.register_secondary_lsp_adapter(
-        "HTML".into(),
-        Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
-    );
-    languages.register_secondary_lsp_adapter(
-        "PHP".into(),
-        Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
-    );
-    languages.register_secondary_lsp_adapter(
-        "Svelte".into(),
-        Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
-    );
-    languages.register_secondary_lsp_adapter(
-        "Vue.js".into(),
-        Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
-    );
+    let tailwind_languages = [
+        "Astro",
+        "HTML",
+        "PHP",
+        "Svelte",
+        "TSX",
+        "JavaScript",
+        "Vue.js",
+    ];
+
+    for language in tailwind_languages {
+        languages.register_secondary_lsp_adapter(
+            language.into(),
+            Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
+        );
+    }
 
     let mut subscription = languages.subscribe();
     let mut prev_language_settings = languages.language_settings();

extensions/deno/Cargo.toml 🔗

@@ -0,0 +1,16 @@
+[package]
+name = "zed_deno"
+version = "0.0.1"
+edition = "2021"
+publish = false
+license = "Apache-2.0"
+
+[lints]
+workspace = true
+
+[lib]
+path = "src/deno.rs"
+crate-type = ["cdylib"]
+
+[dependencies]
+zed_extension_api = "0.0.6"

extensions/deno/extension.toml 🔗

@@ -0,0 +1,13 @@
+id = "deno"
+name = "Deno"
+description = "Deno support."
+version = "0.0.1"
+schema_version = 1
+authors = ["Lino Le Van <11367844+lino-levan@users.noreply.github.com>"]
+repository = "https://github.com/zed-industries/zed"
+
+[language_servers.deno]
+name = "Deno Language Server"
+languages = ["TypeScript", "TSX", "JavaScript", "JSDoc"]
+language_ids = { "TypeScript" = "typescript", "TSX" = "typescriptreact", "JavaScript" = "javascript" }
+code_action_kinds = ["quickfix", "refactor", "refactor.extract", "source"]

extensions/deno/src/deno.rs 🔗

@@ -0,0 +1,154 @@
+use std::fs;
+use zed::lsp::CompletionKind;
+use zed::{serde_json, CodeLabel, CodeLabelSpan, LanguageServerId};
+use zed_extension_api::{self as zed, Result};
+
+struct DenoExtension {
+    cached_binary_path: Option<String>,
+}
+
+impl DenoExtension {
+    fn language_server_binary_path(
+        &mut self,
+        language_server_id: &LanguageServerId,
+        worktree: &zed::Worktree,
+    ) -> Result<String> {
+        if let Some(path) = worktree.which("deno") {
+            return Ok(path);
+        }
+
+        if let Some(path) = &self.cached_binary_path {
+            if fs::metadata(path).map_or(false, |stat| stat.is_file()) {
+                return Ok(path.clone());
+            }
+        }
+
+        zed::set_language_server_installation_status(
+            &language_server_id,
+            &zed::LanguageServerInstallationStatus::CheckingForUpdate,
+        );
+        let release = zed::latest_github_release(
+            "denoland/deno",
+            zed::GithubReleaseOptions {
+                require_assets: true,
+                pre_release: false,
+            },
+        )?;
+
+        let (platform, arch) = zed::current_platform();
+        let asset_name = format!(
+            "deno-{arch}-{os}.zip",
+            arch = match arch {
+                zed::Architecture::Aarch64 => "aarch64",
+                zed::Architecture::X8664 => "x86_64",
+                zed::Architecture::X86 =>
+                    return Err(format!("unsupported architecture: {arch:?}")),
+            },
+            os = match platform {
+                zed::Os::Mac => "apple-darwin",
+                zed::Os::Linux => "unknown-linux-gnu",
+                zed::Os::Windows => "pc-windows-msvc",
+            },
+        );
+
+        let asset = release
+            .assets
+            .iter()
+            .find(|asset| asset.name == asset_name)
+            .ok_or_else(|| format!("no asset found matching {:?}", asset_name))?;
+
+        let version_dir = format!("deno-{}", release.version);
+        let binary_path = format!("{version_dir}/deno");
+
+        if !fs::metadata(&binary_path).map_or(false, |stat| stat.is_file()) {
+            zed::set_language_server_installation_status(
+                &language_server_id,
+                &zed::LanguageServerInstallationStatus::Downloading,
+            );
+
+            zed::download_file(
+                &asset.download_url,
+                &version_dir,
+                zed::DownloadedFileType::Zip,
+            )
+            .map_err(|e| format!("failed to download file: {e}"))?;
+
+            let entries =
+                fs::read_dir(".").map_err(|e| format!("failed to list working directory {e}"))?;
+            for entry in entries {
+                let entry = entry.map_err(|e| format!("failed to load directory entry {e}"))?;
+                if entry.file_name().to_str() != Some(&version_dir) {
+                    fs::remove_dir_all(&entry.path()).ok();
+                }
+            }
+        }
+
+        self.cached_binary_path = Some(binary_path.clone());
+        Ok(binary_path)
+    }
+}
+
+impl zed::Extension for DenoExtension {
+    fn new() -> Self {
+        Self {
+            cached_binary_path: None,
+        }
+    }
+
+    fn language_server_command(
+        &mut self,
+        language_server_id: &LanguageServerId,
+        worktree: &zed::Worktree,
+    ) -> Result<zed::Command> {
+        Ok(zed::Command {
+            command: self.language_server_binary_path(language_server_id, worktree)?,
+            args: vec!["lsp".to_string()],
+            env: Default::default(),
+        })
+    }
+
+    fn language_server_initialization_options(
+        &mut self,
+        _language_server_id: &zed::LanguageServerId,
+        _worktree: &zed::Worktree,
+    ) -> Result<Option<serde_json::Value>> {
+        Ok(Some(serde_json::json!({
+            "provideFormatter": true,
+        })))
+    }
+
+    fn label_for_completion(
+        &self,
+        _language_server_id: &LanguageServerId,
+        completion: zed::lsp::Completion,
+    ) -> Option<CodeLabel> {
+        let highlight_name = match completion.kind? {
+            CompletionKind::Class | CompletionKind::Interface | CompletionKind::Constructor => {
+                "type"
+            }
+            CompletionKind::Constant => "constant",
+            CompletionKind::Function | CompletionKind::Method => "function",
+            CompletionKind::Property | CompletionKind::Field => "property",
+            _ => return None,
+        };
+
+        let len = completion.label.len();
+        let name_span = CodeLabelSpan::literal(completion.label, Some(highlight_name.to_string()));
+
+        Some(zed::CodeLabel {
+            code: Default::default(),
+            spans: if let Some(detail) = completion.detail {
+                vec![
+                    name_span,
+                    CodeLabelSpan::literal(" ", None),
+                    CodeLabelSpan::literal(detail, None),
+                ]
+            } else {
+                vec![name_span]
+            },
+            filter_range: (0..len).into(),
+        })
+    }
+}
+
+zed::register_extension!(DenoExtension);