diff --git a/Cargo.lock b/Cargo.lock index 21761f96e24969316fcffbda0b7edb95750223a5..85bc6dfd0bb7e47559b3d040a0b0231c9dc35547 100644 --- a/Cargo.lock +++ b/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" diff --git a/Cargo.toml b/Cargo.toml index adb9f461a61b77e12237b19d180806df19e9be3d..7a883e458641e46694bac722ab86fb3f2363db2e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -109,6 +109,7 @@ members = [ "extensions/clojure", "extensions/csharp", "extensions/dart", + "extensions/deno", "extensions/elm", "extensions/emmet", "extensions/erlang", diff --git a/assets/settings/default.json b/assets/settings/default.json index 2ba2268b43cc40cdcb18640e46e1fb6788c891ed..5e90ba524cf68be14ec3974ea8b7db776696bce9 100644 --- a/assets/settings/default.json +++ b/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 diff --git a/crates/languages/src/deno.rs b/crates/languages/src/deno.rs deleted file mode 100644 index b43411c674bf9b373e395e97fa6e2f7ef5346e22..0000000000000000000000000000000000000000 --- a/crates/languages/src/deno.rs +++ /dev/null @@ -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, -} - -impl Settings for DenoSettings { - const KEY: Option<&'static str> = Some("deno"); - - type FileContent = DenoSettingsContent; - - fn load(sources: SettingsSources, _: &mut AppContext) -> Result { - sources.json_merge() - } -} - -fn deno_server_binary_arguments() -> Vec { - 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> { - 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, - 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, - env: None, - 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, - }) - } - - async fn initialization_options( - self: Arc, - _: &Arc, - ) -> Result> { - Ok(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 { - 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() -} diff --git a/crates/languages/src/lib.rs b/crates/languages/src/lib.rs index 44d9c02b4ad9ab6fcc6ee5365aab0d8d2181adbf..a25368f903e2c374550b219189b0f2652828a4ba 100644 --- a/crates/languages/src/lib.rs +++ b/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(); diff --git a/extensions/deno/Cargo.toml b/extensions/deno/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..59253c6cdce06917bffbb365fb35e5a289d4b263 --- /dev/null +++ b/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" diff --git a/extensions/deno/LICENSE-APACHE b/extensions/deno/LICENSE-APACHE new file mode 120000 index 0000000000000000000000000000000000000000..1cd601d0a3affae83854be02a0afdec3b7a9ec4d --- /dev/null +++ b/extensions/deno/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/extensions/deno/extension.toml b/extensions/deno/extension.toml new file mode 100644 index 0000000000000000000000000000000000000000..8efcb63e065803b4084181ea4b35720e552b2d36 --- /dev/null +++ b/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"] diff --git a/extensions/deno/src/deno.rs b/extensions/deno/src/deno.rs new file mode 100644 index 0000000000000000000000000000000000000000..02231765d52e05da5688f7236bf6a03ada6401f7 --- /dev/null +++ b/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, +} + +impl DenoExtension { + fn language_server_binary_path( + &mut self, + language_server_id: &LanguageServerId, + worktree: &zed::Worktree, + ) -> Result { + 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 { + 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> { + Ok(Some(serde_json::json!({ + "provideFormatter": true, + }))) + } + + fn label_for_completion( + &self, + _language_server_id: &LanguageServerId, + completion: zed::lsp::Completion, + ) -> Option { + 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);