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  ### Completion  ### Hover  ### Go to definition  ### Formatting  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.  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:
parent
8536ba54c3
commit
bbc4ed9cab
5 changed files with 227 additions and 2 deletions
|
@ -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"
|
||||
},
|
||||
|
|
6
assets/icons/file_icons/terraform.svg
Normal file
6
assets/icons/file_icons/terraform.svg
Normal 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 |
|
@ -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",
|
||||
|
|
186
crates/languages/src/terraform.rs
Normal file
186
crates/languages/src/terraform.rs
Normal 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()
|
||||
}
|
24
docs/src/languages/terraform.md
Normal file
24
docs/src/languages/terraform.md
Normal 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).
|
Loading…
Add table
Add a link
Reference in a new issue