From b5b872656bb92d989043e388f9907ee582c99dea Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 12 Apr 2024 11:49:49 -0400 Subject: [PATCH] Extract Terraform extension (#10479) This PR extracts Terraform support into an extension and removes the built-in Terraform support from Zed. Release Notes: - Removed built-in support for Terraform, in favor of making it available as an extension. The Terraform extension will be suggested for download when you open a `.tf`, `.tfvars`, or `.hcl` file. --- Cargo.lock | 17 +- Cargo.toml | 2 +- crates/extension/src/extension_lsp_adapter.rs | 20 +- crates/extensions_ui/src/extension_suggest.rs | 1 + crates/languages/Cargo.toml | 1 - crates/languages/src/lib.rs | 8 - crates/languages/src/terraform.rs | 181 ------------------ extensions/terraform/Cargo.toml | 16 ++ extensions/terraform/LICENSE-APACHE | 1 + extensions/terraform/extension.toml | 16 ++ .../terraform/languages}/hcl/config.toml | 0 .../terraform/languages}/hcl/highlights.scm | 0 .../terraform/languages}/hcl/indents.scm | 0 .../terraform/languages}/hcl/injections.scm | 0 .../languages}/terraform-vars/config.toml | 0 .../languages}/terraform-vars/highlights.scm | 0 .../languages}/terraform-vars/indents.scm | 0 .../languages}/terraform-vars/injections.scm | 0 .../languages}/terraform/config.toml | 0 .../languages}/terraform/highlights.scm | 0 .../languages}/terraform/indents.scm | 0 .../languages}/terraform/injections.scm | 0 extensions/terraform/src/terraform.rs | 101 ++++++++++ script/licenses/zed-licenses.toml | 6 - 24 files changed, 162 insertions(+), 208 deletions(-) delete mode 100644 crates/languages/src/terraform.rs create mode 100644 extensions/terraform/Cargo.toml create mode 120000 extensions/terraform/LICENSE-APACHE create mode 100644 extensions/terraform/extension.toml rename {crates/languages/src => extensions/terraform/languages}/hcl/config.toml (100%) rename {crates/languages/src => extensions/terraform/languages}/hcl/highlights.scm (100%) rename {crates/languages/src => extensions/terraform/languages}/hcl/indents.scm (100%) rename {crates/languages/src => extensions/terraform/languages}/hcl/injections.scm (100%) rename {crates/languages/src => extensions/terraform/languages}/terraform-vars/config.toml (100%) rename {crates/languages/src => extensions/terraform/languages}/terraform-vars/highlights.scm (100%) rename {crates/languages/src => extensions/terraform/languages}/terraform-vars/indents.scm (100%) rename {crates/languages/src => extensions/terraform/languages}/terraform-vars/injections.scm (100%) rename {crates/languages/src => extensions/terraform/languages}/terraform/config.toml (100%) rename {crates/languages/src => extensions/terraform/languages}/terraform/highlights.scm (100%) rename {crates/languages/src => extensions/terraform/languages}/terraform/indents.scm (100%) rename {crates/languages/src => extensions/terraform/languages}/terraform/injections.scm (100%) create mode 100644 extensions/terraform/src/terraform.rs diff --git a/Cargo.lock b/Cargo.lock index 1bfbc9ca57..cb35413633 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5463,7 +5463,6 @@ dependencies = [ "tree-sitter-go", "tree-sitter-gomod", "tree-sitter-gowork", - "tree-sitter-hcl", "tree-sitter-heex", "tree-sitter-jsdoc", "tree-sitter-json 0.20.0", @@ -10417,15 +10416,6 @@ dependencies = [ "tree-sitter", ] -[[package]] -name = "tree-sitter-hcl" -version = "0.0.1" -source = "git+https://github.com/MichaHoffmann/tree-sitter-hcl?rev=v1.1.0#636dbe70301ecbab8f353c8c78b3406fe4f185f5" -dependencies = [ - "cc", - "tree-sitter", -] - [[package]] name = "tree-sitter-heex" version = "0.0.1" @@ -12683,6 +12673,13 @@ dependencies = [ "zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "zed_terraform" +version = "0.0.1" +dependencies = [ + "zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "zed_toml" version = "0.0.2" diff --git a/Cargo.toml b/Cargo.toml index 17a96c5a36..f58d998a8b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -117,6 +117,7 @@ members = [ "extensions/prisma", "extensions/purescript", "extensions/svelte", + "extensions/terraform", "extensions/toml", "extensions/uiua", "extensions/zig", @@ -322,7 +323,6 @@ tree-sitter-embedded-template = "0.20.0" tree-sitter-go = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "aeb2f33b366fd78d5789ff104956ce23508b85db" } tree-sitter-gomod = { git = "https://github.com/camdencheek/tree-sitter-go-mod" } tree-sitter-gowork = { git = "https://github.com/d1y/tree-sitter-go-work" } -tree-sitter-hcl = { git = "https://github.com/MichaHoffmann/tree-sitter-hcl", rev = "v1.1.0" } rustc-demangle = "0.1.23" tree-sitter-heex = { git = "https://github.com/phoenixframework/tree-sitter-heex", rev = "2e1348c3cf2c9323e87c2744796cf3f3868aa82a" } tree-sitter-html = "0.19.0" diff --git a/crates/extension/src/extension_lsp_adapter.rs b/crates/extension/src/extension_lsp_adapter.rs index 457ec30cc9..655f701115 100644 --- a/crates/extension/src/extension_lsp_adapter.rs +++ b/crates/extension/src/extension_lsp_adapter.rs @@ -10,7 +10,7 @@ use gpui::AsyncAppContext; use language::{ CodeLabel, HighlightId, Language, LanguageServerName, LspAdapter, LspAdapterDelegate, }; -use lsp::LanguageServerBinary; +use lsp::{CodeActionKind, LanguageServerBinary}; use serde::Serialize; use serde_json::Value; use std::ops::Range; @@ -129,6 +129,24 @@ impl LspAdapter for ExtensionLspAdapter { None } + fn code_action_kinds(&self) -> Option> { + if self.extension.manifest.id.as_ref() == "terraform" { + // This is taken from the original Terraform implementation, including + // the TODOs: + // TODO: file issue for server supported code actions + // TODO: reenable default actions / delete override + return Some(vec![]); + } + + Some(vec![ + CodeActionKind::EMPTY, + CodeActionKind::QUICKFIX, + CodeActionKind::REFACTOR, + CodeActionKind::REFACTOR_EXTRACT, + CodeActionKind::SOURCE, + ]) + } + fn language_ids(&self) -> HashMap { // TODO: The language IDs can be provided via the language server options // in `extension.toml now but we're leaving these existing usages in place temporarily diff --git a/crates/extensions_ui/src/extension_suggest.rs b/crates/extensions_ui/src/extension_suggest.rs index 2d4d164020..ce4911712d 100644 --- a/crates/extensions_ui/src/extension_suggest.rs +++ b/crates/extensions_ui/src/extension_suggest.rs @@ -59,6 +59,7 @@ const SUGGESTIONS_BY_EXTENSION_ID: &[(&str, &[&str])] = &[ ("svelte", &["svelte"]), ("swift", &["swift"]), ("templ", &["templ"]), + ("terraform", &["tf", "tfvars", "hcl"]), ("toml", &["Cargo.lock", "toml"]), ("wgsl", &["wgsl"]), ("zig", &["zig"]), diff --git a/crates/languages/Cargo.toml b/crates/languages/Cargo.toml index d43c733902..08b8f0afcb 100644 --- a/crates/languages/Cargo.toml +++ b/crates/languages/Cargo.toml @@ -45,7 +45,6 @@ tree-sitter-embedded-template.workspace = true tree-sitter-go.workspace = true tree-sitter-gomod.workspace = true tree-sitter-gowork.workspace = true -tree-sitter-hcl.workspace = true tree-sitter-heex.workspace = true tree-sitter-jsdoc.workspace = true tree-sitter-json.workspace = true diff --git a/crates/languages/src/lib.rs b/crates/languages/src/lib.rs index 38d8fc50c3..634d54db8e 100644 --- a/crates/languages/src/lib.rs +++ b/crates/languages/src/lib.rs @@ -23,7 +23,6 @@ mod python; mod ruby; mod rust; mod tailwind; -mod terraform; mod typescript; mod vue; mod yaml; @@ -63,7 +62,6 @@ pub fn init( ("go", tree_sitter_go::language()), ("gomod", tree_sitter_gomod::language()), ("gowork", tree_sitter_gowork::language()), - ("hcl", tree_sitter_hcl::language()), ("heex", tree_sitter_heex::language()), ("jsdoc", tree_sitter_jsdoc::language()), ("json", tree_sitter_json::language()), @@ -280,12 +278,6 @@ pub fn init( ] ); language!("proto"); - language!("terraform", vec![Arc::new(terraform::TerraformLspAdapter)]); - language!( - "terraform-vars", - vec![Arc::new(terraform::TerraformLspAdapter)] - ); - language!("hcl", vec![]); languages.register_secondary_lsp_adapter( "Astro".into(), diff --git a/crates/languages/src/terraform.rs b/crates/languages/src/terraform.rs deleted file mode 100644 index a7e6b69f95..0000000000 --- a/crates/languages/src/terraform.rs +++ /dev/null @@ -1,181 +0,0 @@ -use anyhow::{anyhow, Context, Result}; -use async_trait::async_trait; -use collections::HashMap; -use futures::StreamExt; -pub use language::*; -use lsp::{CodeActionKind, LanguageServerBinary}; -use smol::fs::{self, File}; -use std::{any::Any, ffi::OsString, path::PathBuf}; -use util::{ - fs::remove_matching, - github::{latest_github_release, GitHubLspBinaryVersion}, - maybe, ResultExt, -}; - -fn terraform_ls_binary_arguments() -> Vec { - vec!["serve".into()] -} - -pub struct TerraformLspAdapter; - -#[async_trait(?Send)] -impl LspAdapter for TerraformLspAdapter { - fn name(&self) -> LanguageServerName { - LanguageServerName("terraform-ls".into()) - } - - async fn fetch_latest_server_version( - &self, - delegate: &dyn LspAdapterDelegate, - ) -> Result> { - // TODO: maybe use release API instead - // https://api.releases.hashicorp.com/v1/releases/terraform-ls?limit=1 - let release = latest_github_release( - "hashicorp/terraform-ls", - false, - false, - delegate.http_client(), - ) - .await?; - - Ok(Box::new(GitHubLspBinaryVersion { - name: release.tag_name, - url: Default::default(), - })) - } - - 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!("terraform-ls_{}.zip", version.name)); - let version_dir = container_dir.join(format!("terraform-ls_{}", version.name)); - let binary_path = version_dir.join("terraform-ls"); - let url = build_download_url(version.name)?; - - if fs::metadata(&binary_path).await.is_err() { - let mut response = delegate - .http_client() - .get(&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 Terraform LS archive"))?; - } - - remove_matching(&container_dir, |entry| entry != version_dir).await; - } - - Ok(LanguageServerBinary { - path: binary_path, - env: None, - arguments: terraform_ls_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 - .map(|mut binary| { - binary.arguments = vec!["version".into()]; - binary - }) - } - - fn code_action_kinds(&self) -> Option> { - // TODO: file issue for server supported code actions - // TODO: reenable default actions / delete override - Some(vec![]) - } - - fn language_ids(&self) -> HashMap { - HashMap::from_iter([ - ("Terraform".into(), "terraform".into()), - ("Terraform Vars".into(), "terraform-vars".into()), - ]) - } -} - -fn build_download_url(version: String) -> Result { - let v = version.strip_prefix('v').unwrap_or(&version); - let os = match std::env::consts::OS { - "linux" => "linux", - "macos" => "darwin", - "win" => "windows", - _ => Err(anyhow!("unsupported OS {}", std::env::consts::OS))?, - } - .to_string(); - let arch = match std::env::consts::ARCH { - "x86" => "386", - "x86_64" => "amd64", - "arm" => "arm", - "aarch64" => "arm64", - _ => Err(anyhow!("unsupported ARCH {}", std::env::consts::ARCH))?, - } - .to_string(); - - let url = format!( - "https://releases.hashicorp.com/terraform-ls/{v}/terraform-ls_{v}_{os}_{arch}.zip", - ); - - Ok(url) -} - -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("terraform-ls"); - if fs::metadata(&binary).await.is_ok() { - return Ok(LanguageServerBinary { - path: binary, - env: None, - arguments: terraform_ls_binary_arguments(), - }); - } - } - _ => {} - } - - Err(anyhow!("no cached binary")) - }) - .await - .log_err() -} diff --git a/extensions/terraform/Cargo.toml b/extensions/terraform/Cargo.toml new file mode 100644 index 0000000000..f957a435ae --- /dev/null +++ b/extensions/terraform/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "zed_terraform" +version = "0.0.1" +edition = "2021" +publish = false +license = "Apache-2.0" + +[lints] +workspace = true + +[lib] +path = "src/terraform.rs" +crate-type = ["cdylib"] + +[dependencies] +zed_extension_api = "0.0.6" diff --git a/extensions/terraform/LICENSE-APACHE b/extensions/terraform/LICENSE-APACHE new file mode 120000 index 0000000000..1cd601d0a3 --- /dev/null +++ b/extensions/terraform/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/extensions/terraform/extension.toml b/extensions/terraform/extension.toml new file mode 100644 index 0000000000..a14180c41e --- /dev/null +++ b/extensions/terraform/extension.toml @@ -0,0 +1,16 @@ +id = "terraform" +name = "Terraform" +description = "Terraform support." +version = "0.0.1" +schema_version = 1 +authors = ["Caius Durling ", "Daniel Banck "] +repository = "https://github.com/zed-industries/zed" + +[language_servers.terraform-ls] +name = "Terraform Language Server" +languages = ["Terraform", "Terraform Vars"] +language_ids = { Terraform = "terraform", "Terraform Vars" = "terraform-vars" } + +[grammars.hcl] +repository = "https://github.com/MichaHoffmann/tree-sitter-hcl" +commit = "e936d3fef8bac884661472dce71ad82284761eb1" diff --git a/crates/languages/src/hcl/config.toml b/extensions/terraform/languages/hcl/config.toml similarity index 100% rename from crates/languages/src/hcl/config.toml rename to extensions/terraform/languages/hcl/config.toml diff --git a/crates/languages/src/hcl/highlights.scm b/extensions/terraform/languages/hcl/highlights.scm similarity index 100% rename from crates/languages/src/hcl/highlights.scm rename to extensions/terraform/languages/hcl/highlights.scm diff --git a/crates/languages/src/hcl/indents.scm b/extensions/terraform/languages/hcl/indents.scm similarity index 100% rename from crates/languages/src/hcl/indents.scm rename to extensions/terraform/languages/hcl/indents.scm diff --git a/crates/languages/src/hcl/injections.scm b/extensions/terraform/languages/hcl/injections.scm similarity index 100% rename from crates/languages/src/hcl/injections.scm rename to extensions/terraform/languages/hcl/injections.scm diff --git a/crates/languages/src/terraform-vars/config.toml b/extensions/terraform/languages/terraform-vars/config.toml similarity index 100% rename from crates/languages/src/terraform-vars/config.toml rename to extensions/terraform/languages/terraform-vars/config.toml diff --git a/crates/languages/src/terraform-vars/highlights.scm b/extensions/terraform/languages/terraform-vars/highlights.scm similarity index 100% rename from crates/languages/src/terraform-vars/highlights.scm rename to extensions/terraform/languages/terraform-vars/highlights.scm diff --git a/crates/languages/src/terraform-vars/indents.scm b/extensions/terraform/languages/terraform-vars/indents.scm similarity index 100% rename from crates/languages/src/terraform-vars/indents.scm rename to extensions/terraform/languages/terraform-vars/indents.scm diff --git a/crates/languages/src/terraform-vars/injections.scm b/extensions/terraform/languages/terraform-vars/injections.scm similarity index 100% rename from crates/languages/src/terraform-vars/injections.scm rename to extensions/terraform/languages/terraform-vars/injections.scm diff --git a/crates/languages/src/terraform/config.toml b/extensions/terraform/languages/terraform/config.toml similarity index 100% rename from crates/languages/src/terraform/config.toml rename to extensions/terraform/languages/terraform/config.toml diff --git a/crates/languages/src/terraform/highlights.scm b/extensions/terraform/languages/terraform/highlights.scm similarity index 100% rename from crates/languages/src/terraform/highlights.scm rename to extensions/terraform/languages/terraform/highlights.scm diff --git a/crates/languages/src/terraform/indents.scm b/extensions/terraform/languages/terraform/indents.scm similarity index 100% rename from crates/languages/src/terraform/indents.scm rename to extensions/terraform/languages/terraform/indents.scm diff --git a/crates/languages/src/terraform/injections.scm b/extensions/terraform/languages/terraform/injections.scm similarity index 100% rename from crates/languages/src/terraform/injections.scm rename to extensions/terraform/languages/terraform/injections.scm diff --git a/extensions/terraform/src/terraform.rs b/extensions/terraform/src/terraform.rs new file mode 100644 index 0000000000..8dfabce23e --- /dev/null +++ b/extensions/terraform/src/terraform.rs @@ -0,0 +1,101 @@ +use std::fs; +use zed::LanguageServerId; +use zed_extension_api::{self as zed, Result}; + +struct TerraformExtension { + cached_binary_path: Option, +} + +impl TerraformExtension { + fn language_server_binary_path( + &mut self, + language_server_id: &LanguageServerId, + worktree: &zed::Worktree, + ) -> Result { + if let Some(path) = &self.cached_binary_path { + if fs::metadata(path).map_or(false, |stat| stat.is_file()) { + return Ok(path.clone()); + } + } + + if let Some(path) = worktree.which("terraform-ls") { + self.cached_binary_path = Some(path.clone()); + return Ok(path); + } + + zed::set_language_server_installation_status( + &language_server_id, + &zed::LanguageServerInstallationStatus::CheckingForUpdate, + ); + let release = zed::latest_github_release( + "hashicorp/terraform-ls", + zed::GithubReleaseOptions { + require_assets: false, + pre_release: false, + }, + )?; + + let (platform, arch) = zed::current_platform(); + let download_url = format!( + "https://releases.hashicorp.com/terraform-ls/{version}/terraform-ls_{version}_{os}_{arch}.zip", + version = release.version.strip_prefix('v').unwrap_or(&release.version), + os = match platform { + zed::Os::Mac => "darwin", + zed::Os::Linux => "linux", + zed::Os::Windows => "windows", + }, + arch = match arch { + zed::Architecture::Aarch64 => "arm64", + zed::Architecture::X86 => "386", + zed::Architecture::X8664 => "amd64", + }, + ); + + let version_dir = format!("terraform-ls-{}", release.version); + let binary_path = format!("{version_dir}/terraform-ls"); + + 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(&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 TerraformExtension { + 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!["serve".to_string()], + env: Default::default(), + }) + } +} + +zed::register_extension!(TerraformExtension); diff --git a/script/licenses/zed-licenses.toml b/script/licenses/zed-licenses.toml index c4557a8c20..3459fee3e5 100644 --- a/script/licenses/zed-licenses.toml +++ b/script/licenses/zed-licenses.toml @@ -36,9 +36,3 @@ license = "BSD-3-Clause" [[fuchsia-cprng.clarify.files]] path = 'LICENSE' checksum = '03b114f53e6587a398931762ee11e2395bfdba252a329940e2c8c9e81813845b' - -[tree-sitter-hcl.clarify] -license = "Apache-2.0" -[[tree-sitter-hcl.clarify.files]] -path = 'LICENSE' -checksum = 'c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4'