Add language server for Terraform (#7657)

* Depends on: https://github.com/zed-industries/zed/pull/7449
* Closes: https://github.com/zed-industries/zed/issues/5098

---

This PR adds support for downloading and running the Terraform language
server for `*.tf` and `*.tfvars` files. I've verified that the code
works for `aarch64` and `x86_64` macOS. Downloading new language server
versions on release also works as expected.

Furthermore this PR adds:
- A short docs page for Terraform
- An icon for `*.tf` and `*.tfvars` files

## UX

### File Icons

![CleanShot 2024-02-10 at 23 10
13@2x](https://github.com/zed-industries/zed/assets/45985/6f7cd4f0-e94c-4cfb-b3e9-64b0e33c8a43)

### Completion

![CleanShot 2024-02-13 at 20 54
15@2x](https://github.com/zed-industries/zed/assets/45985/18fafa3b-cb50-4f51-b071-ca9eee3521a6)

### Hover

![CleanShot 2024-02-13 at 20 53
40@2x](https://github.com/zed-industries/zed/assets/45985/4d215315-e019-4d3d-b23c-2691db1803e3)

### Go to definition

![2024-02-13 20 56
28](https://github.com/zed-industries/zed/assets/45985/c21d562f-eb0b-4df9-9175-c53b9923344e)

### Formatting

![2024-02-13 20 59
06](https://github.com/zed-industries/zed/assets/45985/0cdf4ec5-e231-4c8a-a257-cae30a8edc8b)

and more!

## Known issue(s)

@fdionisi discovered that sometimes completion results are inserted with
the wrong indentation. Or rather, if you look closely, they are inserted
with the correct indentation and then something shifts the closing `}`.
I don't think this is related to LSP support and can be addressed in a
separate PR.

![2024-02-13 20 58
16](https://github.com/zed-industries/zed/assets/45985/94a118dd-95f5-4e38-8f83-75fec7a0dddf)

Release Notes:

- Add language server support for Terraform
([#5098](https://github.com/zed-industries/zed/issues/5098)).

---------

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
This commit is contained in:
Daniel Banck 2024-02-27 02:08:49 +01:00 committed by GitHub
parent 8536ba54c3
commit bbc4ed9cab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 227 additions and 2 deletions

View file

@ -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"
},

View file

@ -0,0 +1,6 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.5066 8.01531L19.2375 12.1073V20.2894L12.5066 16.1992V8.01531Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.0294 12.1073V20.2894L27.1563 16.1992V8.01531L20.0294 12.1073Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.58781 3.66V11.5787L11.7147 15.5381V7.61937L4.58781 3.66Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.5066 25.04L19.2375 29V21.1348V21.0818L12.5066 17.1219V25.04Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 620 B

View file

@ -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",

View file

@ -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<OsString> {
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<Box<dyn 'static + Send + Any>> {
// 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<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!("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<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
.map(|mut binary| {
binary.arguments = vec!["version".into()];
binary
})
}
fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
// TODO: file issue for server supported code actions
// TODO: reenable default actions / delete override
Some(vec![])
}
fn language_ids(&self) -> HashMap<String, String> {
HashMap::from_iter([
("Terraform".into(), "terraform".into()),
("Terraform Vars".into(), "terraform-vars".into()),
])
}
}
fn build_download_url(version: String) -> Result<String> {
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<LanguageServerBinary> {
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()
}

View file

@ -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).