diff --git a/assets/icons/file_icons/file_types.json b/assets/icons/file_icons/file_types.json
index 2ce6ccc157408e0177b2ab9f1c2feb03a1edb83f..75719fb4966091764018837c2877a74ad9a0e56d 100644
--- a/assets/icons/file_icons/file_types.json
+++ b/assets/icons/file_icons/file_types.json
@@ -133,6 +133,8 @@
"svelte": "template",
"svg": "image",
"swift": "swift",
+ "tf": "terraform",
+ "tfvars": "terraform",
"tiff": "image",
"toml": "toml",
"ts": "typescript",
@@ -280,6 +282,9 @@
"template": {
"icon": "icons/file_icons/html.svg"
},
+ "terraform": {
+ "icon": "icons/file_icons/terraform.svg"
+ },
"terminal": {
"icon": "icons/file_icons/terminal.svg"
},
diff --git a/assets/icons/file_icons/terraform.svg b/assets/icons/file_icons/terraform.svg
new file mode 100644
index 0000000000000000000000000000000000000000..6c8efc33b665c2ca0a3df5cc4e64b3756fa40dd4
--- /dev/null
+++ b/assets/icons/file_icons/terraform.svg
@@ -0,0 +1,6 @@
+
diff --git a/crates/languages/src/lib.rs b/crates/languages/src/lib.rs
index b65cf39fe2a59a808e6e7d20aa63a3132801bde5..6bbaa8090d8b79d48885407883598d3e92cb08dc 100644
--- a/crates/languages/src/lib.rs
+++ b/crates/languages/src/lib.rs
@@ -36,6 +36,7 @@ mod ruby;
mod rust;
mod svelte;
mod tailwind;
+mod terraform;
mod toml;
mod typescript;
mod uiua;
@@ -312,8 +313,11 @@ pub fn init(
);
language("uiua", vec![Arc::new(uiua::UiuaLanguageServer {})]);
language("proto", vec![]);
- language("terraform", vec![]);
- language("terraform-vars", vec![]);
+ language("terraform", vec![Arc::new(terraform::TerraformLspAdapter)]);
+ language(
+ "terraform-vars",
+ vec![Arc::new(terraform::TerraformLspAdapter)],
+ );
language("hcl", vec![]);
language(
"prisma",
diff --git a/crates/languages/src/terraform.rs b/crates/languages/src/terraform.rs
new file mode 100644
index 0000000000000000000000000000000000000000..dba2fdcc1a48337ce51c0db1a5b6b9a235644d5b
--- /dev/null
+++ b/crates/languages/src/terraform.rs
@@ -0,0 +1,186 @@
+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, str};
+use util::{
+ async_maybe,
+ fs::remove_matching,
+ github::{latest_github_release, GitHubLspBinaryVersion},
+ ResultExt,
+};
+
+fn terraform_ls_binary_arguments() -> Vec {
+ vec!["serve".into()]
+}
+
+pub struct TerraformLspAdapter;
+
+#[async_trait]
+impl LspAdapter for TerraformLspAdapter {
+ fn name(&self) -> LanguageServerName {
+ LanguageServerName("terraform-ls".into())
+ }
+
+ fn short_name(&self) -> &'static str {
+ "terraform-ls"
+ }
+
+ 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 {
+ async_maybe!({
+ 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/docs/src/languages/terraform.md b/docs/src/languages/terraform.md
new file mode 100644
index 0000000000000000000000000000000000000000..32bfe9b4b1dd499980a2d7ba07210643b766e119
--- /dev/null
+++ b/docs/src/languages/terraform.md
@@ -0,0 +1,24 @@
+# Terraform
+
+- Tree Sitter: [tree-sitter-hcl](https://github.com/MichaHoffmann/tree-sitter-hcl)
+- Language Server: [terraform-ls](https://github.com/hashicorp/terraform-ls)
+
+### Configuration
+
+The Terraform language server can be configured in your `settings.json`, e.g.:
+
+```json
+{
+ "lsp": {
+ "terraform-ls": {
+ "initialization_options": {
+ "experimentalFeatures": {
+ "prefillRequiredFields": true
+ }
+ }
+ }
+ }
+}
+```
+
+See the [full list of server settings here](https://github.com/hashicorp/terraform-ls/blob/main/docs/SETTINGS.md).