From 6863b9263ec31ef20406ae381ab63ce2bf493e7b Mon Sep 17 00:00:00 2001 From: Caius Durling Date: Mon, 5 Feb 2024 19:38:30 +0000 Subject: [PATCH] Add Terraform & HCL syntax highlighting (#6882) Terraform and HCL are almost the same language, but not quite so proposing them as separate languages within Zed. (Terraform is an extension of HCL, with a different formatter.) This is just adding the language definition, parsing and highlighting functionality, not any LSP or formatting beyond that for either language. I've taken a bunch of inspiration from Neovim for having the separate languages, and also lifted some of their `scm` files (with attribution comments in this codebase) as the tree-sitter repo doesn't contain them. (Neovim's code is Apache-2.0 licensed, so should be fine here with attribution from reading Zed's licenses files.) I've then amended to make sure the capture groups are named for things Zed understands. I'd love someone from Zed to confirm that's okay, or if I should clean-room implement the `scm` files. Highlighting in Terraform & HCL with a moderate amount of syntax in a file (Terraform on left, HCL on right.) Screenshot 2024-01-31 at 18 07 45 Release Notes: - (|Improved) ... ([#5098](https://github.com/zed-industries/zed/issues/5098)). --- Cargo.lock | 10 ++ Cargo.toml | 1 + assets/settings/default.json | 3 + crates/zed/Cargo.toml | 1 + crates/zed/src/languages.rs | 2 + crates/zed/src/languages/hcl/config.toml | 13 ++ crates/zed/src/languages/hcl/highlights.scm | 117 +++++++++++++ crates/zed/src/languages/hcl/indents.scm | 11 ++ crates/zed/src/languages/hcl/injections.scm | 6 + .../zed/src/languages/terraform/config.toml | 13 ++ .../src/languages/terraform/highlights.scm | 159 ++++++++++++++++++ .../zed/src/languages/terraform/indents.scm | 14 ++ .../src/languages/terraform/injections.scm | 9 + 13 files changed, 359 insertions(+) create mode 100644 crates/zed/src/languages/hcl/config.toml create mode 100644 crates/zed/src/languages/hcl/highlights.scm create mode 100644 crates/zed/src/languages/hcl/indents.scm create mode 100644 crates/zed/src/languages/hcl/injections.scm create mode 100644 crates/zed/src/languages/terraform/config.toml create mode 100644 crates/zed/src/languages/terraform/highlights.scm create mode 100644 crates/zed/src/languages/terraform/indents.scm create mode 100644 crates/zed/src/languages/terraform/injections.scm diff --git a/Cargo.lock b/Cargo.lock index 40d594ff42..ef68faac45 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8876,6 +8876,15 @@ 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" @@ -10394,6 +10403,7 @@ dependencies = [ "tree-sitter-gomod", "tree-sitter-gowork", "tree-sitter-haskell", + "tree-sitter-hcl", "tree-sitter-heex", "tree-sitter-html", "tree-sitter-json 0.20.0", diff --git a/Cargo.toml b/Cargo.toml index edfc968ffb..2676f5b658 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -157,6 +157,7 @@ tree-sitter-go = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = 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-haskell = { git = "https://github.com/tree-sitter/tree-sitter-haskell", rev = "cf98de23e4285b8e6bcb57b050ef2326e2cc284b" } +tree-sitter-hcl = {git = "https://github.com/MichaHoffmann/tree-sitter-hcl", rev = "v1.1.0"} tree-sitter-heex = { git = "https://github.com/phoenixframework/tree-sitter-heex", rev = "2e1348c3cf2c9323e87c2744796cf3f3868aa82a" } tree-sitter-html = "0.19.0" tree-sitter-json = { git = "https://github.com/tree-sitter/tree-sitter-json", rev = "40a81c01a40ac48744e0c8ccabbaba1920441199" } diff --git a/assets/settings/default.json b/assets/settings/default.json index 9387c58f1d..3cb6082991 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -498,6 +498,9 @@ "JavaScript": { "tab_size": 2 }, + "Terraform": { + "tab_size": 2 + }, "TypeScript": { "tab_size": 2 }, diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index f3ba0825c7..46025a59bb 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -124,6 +124,7 @@ tree-sitter-go.workspace = true tree-sitter-gomod.workspace = true tree-sitter-gowork.workspace = true tree-sitter-haskell.workspace = true +tree-sitter-hcl.workspace = true tree-sitter-heex.workspace = true tree-sitter-html.workspace = true tree-sitter-json.workspace = true diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index 248ff3c7fd..1250aa92c7 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -322,6 +322,8 @@ pub fn init( vec![Arc::new(uiua::UiuaLanguageServer {})], ); language("proto", tree_sitter_proto::language(), vec![]); + language("terraform", tree_sitter_hcl::language(), vec![]); + language("hcl", tree_sitter_hcl::language(), vec![]); if let Ok(children) = std::fs::read_dir(&*PLUGINS_DIR) { for child in children { diff --git a/crates/zed/src/languages/hcl/config.toml b/crates/zed/src/languages/hcl/config.toml new file mode 100644 index 0000000000..c7c2ebf6ba --- /dev/null +++ b/crates/zed/src/languages/hcl/config.toml @@ -0,0 +1,13 @@ +name = "HCL" +path_suffixes = ["hcl"] +line_comments = ["# ", "// "] +block_comment = ["/*", "*/"] +autoclose_before = ",}])" +brackets = [ + { start = "{", end = "}", close = true, newline = true }, + { start = "[", end = "]", close = true, newline = true }, + { start = "(", end = ")", close = true, newline = true }, + { start = "\"", end = "\"", close = true, newline = false, not_in = ["comment", "string"] }, + { start = "'", end = "'", close = true, newline = false, not_in = ["comment", "string"] }, + { start = "/*", end = " */", close = true, newline = false, not_in = ["comment", "string"] }, +] diff --git a/crates/zed/src/languages/hcl/highlights.scm b/crates/zed/src/languages/hcl/highlights.scm new file mode 100644 index 0000000000..a1713b3e3f --- /dev/null +++ b/crates/zed/src/languages/hcl/highlights.scm @@ -0,0 +1,117 @@ +; https://github.com/nvim-treesitter/nvim-treesitter/blob/cb79d2446196d25607eb1d982c96939abdf67b8e/queries/hcl/highlights.scm +; highlights.scm +[ + "!" + "\*" + "/" + "%" + "\+" + "-" + ">" + ">=" + "<" + "<=" + "==" + "!=" + "&&" + "||" +] @operator + +[ + "{" + "}" + "[" + "]" + "(" + ")" +] @punctuation.bracket + +[ + "." + ".*" + "," + "[*]" +] @punctuation.delimiter + +[ + (ellipsis) + "\?" + "=>" +] @punctuation.special + +[ + ":" + "=" +] @punctuation + +[ + "for" + "endfor" + "in" + "if" + "else" + "endif" +] @keyword + +[ + (quoted_template_start) ; " + (quoted_template_end) ; " + (template_literal) ; non-interpolation/directive content +] @string + +[ + (heredoc_identifier) ; END + (heredoc_start) ; << or <<- +] @punctuation.delimiter + +[ + (template_interpolation_start) ; ${ + (template_interpolation_end) ; } + (template_directive_start) ; %{ + (template_directive_end) ; } + (strip_marker) ; ~ +] @punctuation.special + +(numeric_lit) @number + +(bool_lit) @boolean + +(null_lit) @constant + +(comment) @comment + +(identifier) @variable + +(body + (block + (identifier) @keyword)) + +(body + (block + (body + (block + (identifier) @type)))) + +(function_call + (identifier) @function) + +(attribute + (identifier) @variable) + +; { key: val } +; +; highlight identifier keys as though they were block attributes +(object_elem + key: + (expression + (variable_expr + (identifier) @variable))) + +; var.foo, data.bar +; +; first element in get_attr is a variable.builtin or a reference to a variable.builtin +(expression + (variable_expr + (identifier) @variable) + (get_attr + (identifier) @variable)) diff --git a/crates/zed/src/languages/hcl/indents.scm b/crates/zed/src/languages/hcl/indents.scm new file mode 100644 index 0000000000..74edb66bdf --- /dev/null +++ b/crates/zed/src/languages/hcl/indents.scm @@ -0,0 +1,11 @@ +; https://github.com/nvim-treesitter/nvim-treesitter/blob/ce4adf11cfe36fc5b0e5bcdce0c7c6e8fbc9798a/queries/hcl/indents.scm +[ + (block) + (object) + (tuple) + (function_call) +] @indent + +(_ "[" "]" @end) @indent +(_ "(" ")" @end) @indent +(_ "{" "}" @end) @indent diff --git a/crates/zed/src/languages/hcl/injections.scm b/crates/zed/src/languages/hcl/injections.scm new file mode 100644 index 0000000000..8617e9fc2e --- /dev/null +++ b/crates/zed/src/languages/hcl/injections.scm @@ -0,0 +1,6 @@ +; https://github.com/nvim-treesitter/nvim-treesitter/blob/ce4adf11cfe36fc5b0e5bcdce0c7c6e8fbc9798a/queries/hcl/injections.scm + +(heredoc_template + (template_literal) @content + (heredoc_identifier) @language + (#downcase! @language)) diff --git a/crates/zed/src/languages/terraform/config.toml b/crates/zed/src/languages/terraform/config.toml new file mode 100644 index 0000000000..9ea5896001 --- /dev/null +++ b/crates/zed/src/languages/terraform/config.toml @@ -0,0 +1,13 @@ +name = "Terraform" +path_suffixes = ["tf", "tfvars"] +line_comments = ["# ", "// "] +block_comment = ["/*", "*/"] +autoclose_before = ",}])" +brackets = [ + { start = "{", end = "}", close = true, newline = true }, + { start = "[", end = "]", close = true, newline = true }, + { start = "(", end = ")", close = true, newline = true }, + { start = "\"", end = "\"", close = true, newline = false, not_in = ["comment", "string"] }, + { start = "'", end = "'", close = true, newline = false, not_in = ["comment", "string"] }, + { start = "/*", end = " */", close = true, newline = false, not_in = ["comment", "string"] }, +] diff --git a/crates/zed/src/languages/terraform/highlights.scm b/crates/zed/src/languages/terraform/highlights.scm new file mode 100644 index 0000000000..f123c3232d --- /dev/null +++ b/crates/zed/src/languages/terraform/highlights.scm @@ -0,0 +1,159 @@ +; https://github.com/nvim-treesitter/nvim-treesitter/blob/cb79d2446196d25607eb1d982c96939abdf67b8e/queries/hcl/highlights.scm +; highlights.scm +[ + "!" + "\*" + "/" + "%" + "\+" + "-" + ">" + ">=" + "<" + "<=" + "==" + "!=" + "&&" + "||" +] @operator + +[ + "{" + "}" + "[" + "]" + "(" + ")" +] @punctuation.bracket + +[ + "." + ".*" + "," + "[*]" +] @punctuation.delimiter + +[ + (ellipsis) + "\?" + "=>" +] @punctuation.special + +[ + ":" + "=" +] @punctuation + +[ + "for" + "endfor" + "in" + "if" + "else" + "endif" +] @keyword + +[ + (quoted_template_start) ; " + (quoted_template_end) ; " + (template_literal) ; non-interpolation/directive content +] @string + +[ + (heredoc_identifier) ; END + (heredoc_start) ; << or <<- +] @punctuation.delimiter + +[ + (template_interpolation_start) ; ${ + (template_interpolation_end) ; } + (template_directive_start) ; %{ + (template_directive_end) ; } + (strip_marker) ; ~ +] @punctuation.special + +(numeric_lit) @number + +(bool_lit) @boolean + +(null_lit) @constant + +(comment) @comment + +(identifier) @variable + +(body + (block + (identifier) @keyword)) + +(body + (block + (body + (block + (identifier) @type)))) + +(function_call + (identifier) @function) + +(attribute + (identifier) @variable) + +; { key: val } +; +; highlight identifier keys as though they were block attributes +(object_elem + key: + (expression + (variable_expr + (identifier) @variable))) + +; var.foo, data.bar +; +; first element in get_attr is a variable.builtin or a reference to a variable.builtin +(expression + (variable_expr + (identifier) @variable) + (get_attr + (identifier) @variable)) + +; https://github.com/nvim-treesitter/nvim-treesitter/blob/cb79d2446196d25607eb1d982c96939abdf67b8e/queries/terraform/highlights.scm +; Terraform specific references +; +; +; local/module/data/var/output +(expression + (variable_expr + (identifier) @variable + (#any-of? @variable "data" "var" "local" "module" "output")) + (get_attr + (identifier) @variable)) + +; path.root/cwd/module +(expression + (variable_expr + (identifier) @type + (#eq? @type "path")) + (get_attr + (identifier) @variable + (#any-of? @variable "root" "cwd" "module"))) + +; terraform.workspace +(expression + (variable_expr + (identifier) @type + (#eq? @type "terraform")) + (get_attr + (identifier) @variable + (#any-of? @variable "workspace"))) + +; Terraform specific keywords +; FIXME: ideally only for identifiers under a `variable` block to minimize false positives +((identifier) @type + (#any-of? @type "bool" "string" "number" "object" "tuple" "list" "map" "set" "any")) + +(object_elem + val: + (expression + (variable_expr + (identifier) @type + (#any-of? @type "bool" "string" "number" "object" "tuple" "list" "map" "set" "any")))) diff --git a/crates/zed/src/languages/terraform/indents.scm b/crates/zed/src/languages/terraform/indents.scm new file mode 100644 index 0000000000..95ad93df1d --- /dev/null +++ b/crates/zed/src/languages/terraform/indents.scm @@ -0,0 +1,14 @@ +; https://github.com/nvim-treesitter/nvim-treesitter/blob/ce4adf11cfe36fc5b0e5bcdce0c7c6e8fbc9798a/queries/hcl/indents.scm +[ + (block) + (object) + (tuple) + (function_call) +] @indent + +(_ "[" "]" @end) @indent +(_ "(" ")" @end) @indent +(_ "{" "}" @end) @indent + +; https://github.com/nvim-treesitter/nvim-treesitter/blob/ce4adf11cfe36fc5b0e5bcdce0c7c6e8fbc9798a/queries/terraform/indents.scm +; inherits: hcl diff --git a/crates/zed/src/languages/terraform/injections.scm b/crates/zed/src/languages/terraform/injections.scm new file mode 100644 index 0000000000..b41ee95d40 --- /dev/null +++ b/crates/zed/src/languages/terraform/injections.scm @@ -0,0 +1,9 @@ +; https://github.com/nvim-treesitter/nvim-treesitter/blob/ce4adf11cfe36fc5b0e5bcdce0c7c6e8fbc9798a/queries/hcl/injections.scm + +(heredoc_template + (template_literal) @content + (heredoc_identifier) @language + (#downcase! @language)) + +; https://github.com/nvim-treesitter/nvim-treesitter/blob/ce4adf11cfe36fc5b0e5bcdce0c7c6e8fbc9798a/queries/terraform/injections.scm +; inherits: hcl