flake-update-20260201
1;;; terraform-ts-mode.el --- Terraform major mode using Treesitter and eglot -*- lexical-binding: t -*-
2
3;;; Copyright (C) 2022-2027 Kai Grotelüschen
4
5;; Author: Kai Grotelueschen <kgr@gnotes.de>
6;; Maintainer: Kai Grotelueschen <kgr@gnotes.de>
7;; Version: 0.4
8;; Keywords: elisp, extensions
9;; Homepage: https://github.com/kgrotel/terraform-ts-mode
10;; Package-Requires: ((emacs "29.1"))
11
12;; This program is free software; you can redistribute it and/or
13;; modify it under the terms of the GNU General Public License as
14;; published by the Free Software Foundation, either version 3 of
15;; the License, or (at your option) any later version.
16
17;; This program is distributed in the hope that it will be useful,
18;; but WITHOUT ANY WARRANTY; without even the implied warranty of
19;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20;; GNU General Public License for more details.
21;;
22;; You should have received a copy of the GNU General Public License
23;; along with this program. If not, see http://www.gnu.org/licenses.
24
25
26;;; Commentary:
27
28;; This is a terraform mode using treesit. There are still quite some
29;; Isues with using Treesitter for imenu and Highlight so any kind of
30;; help is greatly appreaciated
31
32;;; Code:
33
34(require 'treesit)
35(require 'eglot)
36(eval-when-compile (require 'rx))
37
38(declare-function treesit-parser-create "treesit.c")
39(declare-function treesit-query-capture "treesit.c")
40(declare-function treesit-induce-sparse-tree "treesit.c")
41(declare-function treesit-node-child "treesit.c")
42(declare-function treesit-node-start "treesit.c")
43(declare-function treesit-node-type "treesit.c")
44
45(defgroup terraform nil
46 "Support terraform code."
47 :link '(url-link "https://www.terraform.io/")
48 :group 'languages)
49
50;; module customizions
51
52(defcustom terraform-ts-mode-hook nil
53 "Hook called by `terraform-ts-mode'."
54 :type 'hook
55 :group 'terraform)
56
57(defcustom terraform-ts-indent-level 2
58 "The tab width to use when indenting."
59 :type 'integer
60 :group 'terraform)
61
62(defcustom terraform-ts-format-on-save t
63 "Format buffer on save using eglot-format"
64 :type 'boolean
65 :group 'terraform)
66
67(defcustom terraform-ts-eglot-debug nil
68 "enable debugging of eglot (mostly eglot logging) will impact performance"
69 :type 'boolean
70 :group 'terraform)
71
72;; module facses
73
74(defface terraform-resource-type-face
75 '((t :inherit font-lock-type-face))
76 "Face for resource names."
77 :group 'terraform-mode)
78
79(defface terraform-resource-name-face
80 '((t :inherit font-lock-function-name-face))
81 "Face for resource names."
82 :group 'terraform-mode)
83
84(defface terraform-builtin-face
85 '((t :inherit font-lock-builtin-face))
86 "Face for builtins."
87 :group 'terraform-mode)
88
89(defface terraform-variable-name-face
90 '((t :inherit font-lock-variable-name-face))
91 "Face for varriables."
92 :group 'terraform-mode)
93
94;; mode vars
95
96(defvar terraform-ts--syntax-table
97 (let ((synTable (make-syntax-table)))
98 ;; Word syntax
99 (modify-syntax-entry ?_ "w" synTable) ; underscore is always part of word (never punctiation) -> w
100 (modify-syntax-entry '(?0 . ?9) "w" synTable)
101 (modify-syntax-entry '(?a . ?z) "w" synTable)
102 (modify-syntax-entry '(?A . ?Z) "w" synTable)
103
104 ;; Punctuation
105 (modify-syntax-entry ?- "_" synTable) ; - can be word and punctiation -> _ class
106 (modify-syntax-entry ?= "." synTable)
107 (modify-syntax-entry ?= "." synTable)
108
109 ;; Whitespace
110 (modify-syntax-entry ?\s " " synTable)
111 (modify-syntax-entry ?\xa0 " " synTable) ; non-breaking space
112 (modify-syntax-entry ?\t " " synTable)
113 (modify-syntax-entry ?\f " " synTable)
114
115 ;; Brackets
116 (modify-syntax-entry ?\( "()" synTable)
117 (modify-syntax-entry ?\) ")(" synTable)
118 (modify-syntax-entry ?\[ "(]" synTable)
119 (modify-syntax-entry ?\] ")[" synTable)
120 (modify-syntax-entry ?\{ "(}" synTable)
121 (modify-syntax-entry ?\} "){" synTable)
122
123 ;; Comments
124 (modify-syntax-entry ?# "<" synTable) ; comment-start-class: single line comment
125 (modify-syntax-entry ?\n ">" synTable) ; comment-end-class: eol
126 (modify-syntax-entry ?/ ". 124" synTable) ; punctuation but also comment: can be // or (12) or /* (1) */ (4)
127 (modify-syntax-entry ?* ". 23b" synTable) ; punctuation but also comment: ca be /* (2) */ (3)
128
129 ;; Others
130 (modify-syntax-entry ?\" "\"" synTable) ; string
131 (modify-syntax-entry ?\\ "\\" synTable) ; escape
132 synTable)
133 "Syntax table for `terraform-ts-mode'.")
134
135;; Imenu
136
137;; MODE VARS
138(defvar terraform-ts--builtin-attributes
139 '("for_each" "count" "source" "type" "default" "providers" "provider")
140 "Terraform builtin attributes for tree-sitter font-locking.")
141
142(defvar terraform-ts--builtin-expressions
143 '("local" "each" "count")
144 "Terraform builtin expressions for tree-sitter font-locking.")
145
146(defvar terraform-ts--named-expressions
147 '("var" "module")
148 "Terraform named expressions for tree-sitter font-locking.")
149
150(defvar terraform-ts--treesit-font-lock-rules
151 (treesit-font-lock-rules
152 :language 'terraform
153 :feature 'comments
154 '((comment) @font-lock-comment-face) ;; checkOK
155
156 :language 'terraform
157 :feature 'brackets
158 '(["(" ")" "[" "]" "{" "}"] @font-lock-bracket-face) ;; checkOK
159
160 :language 'terraform
161 :feature 'delimiters
162 '(["." ".*" "," "[*]" "=>"] @font-lock-delimiter-face) ;; checkOK
163
164 :language 'terraform
165 :feature 'operators
166 '(["!"] @font-lock-negation-char-face)
167
168 :language 'terraform
169 :feature 'operators
170 '(["\*" "/" "%" "\+" "-" ">" ">=" "<" "<=" "==" "!=" "&&" "||"] @font-lock-operator-face)
171
172 :language 'terraform
173 :feature 'builtin
174 '((function_call (identifier) @font-lock-builtin-face)) ;; checkOK
175
176 :language 'terraform
177 :feature 'objects
178 '((object_elem key: (expression (variable_expr (identifier) @font-lock-property-name-face))))
179
180 :language 'terraform
181 :feature 'expressions
182 `(
183 ((expression (variable_expr (identifier) @terraform-builtin-face)
184 (get_attr (identifier) @font-lock-property-name-face))
185 (:match ,(rx-to-string `(seq bol (or ,@terraform-ts--builtin-expressions) eol)) @terraform-builtin-face)) ; local, each and count
186
187 ((expression (variable_expr (identifier) @terraform-builtin-face)
188 :anchor (get_attr (identifier) @font-lock-function-call-face)
189 (get_attr (identifier) @font-lock-property-name-face) :* )
190 (:match ,(rx-to-string `(seq bol (or ,@terraform-ts--named-expressions) eol)) @terraform-builtin-face)) ; module and var
191
192 ((expression (variable_expr (identifier) @terraform-builtin-face)
193 :anchor (get_attr (identifier) @terraform-resource-type-face)
194 (get_attr (identifier) @terraform-resource-name-face)
195 (get_attr (identifier) @font-lock-property-name-face) :* )
196 (:match "data" @terraform-builtin-face))
197
198 ((expression (variable_expr (identifier) @terraform-resource-type-face)
199 :anchor (get_attr (identifier) @terraform-resource-name-face)
200 (get_attr (identifier) @font-lock-property-name-face) :* )) ; that should be a resource
201 )
202
203 :language 'terraform
204 :feature 'interpolation
205 '((interpolation "#{" @font-lock-misc-punctuation-face)
206 (interpolation "}" @font-lock-misc-punctuation-face))
207
208
209 :language 'terraform
210 :feature 'blocks
211 `(
212 ((attribute (identifier) @terraform-builtin-face) (:match ,(rx-to-string `(seq bol (or ,@terraform-ts--builtin-attributes) eol)) @terraform-builtin-face))
213 ((attribute (identifier) @terraform-variable-name-face))
214 )
215
216 :language 'terraform
217 :feature 'blocks
218 '(
219 ((block (identifier) @terraform-builtin-face (string_lit (template_literal) @font-lock-type-face) (string_lit (template_literal) @font-lock-function-name-face)))
220 )
221
222 :language 'terraform
223 :feature 'blocks
224 '(
225 ((block (identifier) @terraform-builtin-face (string_lit (template_literal) @font-lock-function-name-face) :?))
226 )
227
228 :language 'terraform
229 :feature 'conditionals
230 '(["if" "else" "endif"] @font-lock-keyword-face)
231
232 :language 'terraform
233 :feature 'constants
234 '((bool_lit) @font-lock-constant-face) ;; checkOK
235
236 :language 'terraform
237 :feature 'numbers
238 '((numeric_lit) @font-lock-number-face) ;; checkOK
239
240 :language 'terraform
241 :feature 'strings
242 '((string_lit (template_literal)) @font-lock-string-face)
243 )
244 "Tree-sitter font-lock settings.")
245
246(defvar terraform-ts--indent-rules
247 `((terraform
248 ((node-is "block_end") parent-bol 0)
249 ((node-is "object_end") parent-bol 0)
250 ((node-is ")") parent-bol 0)
251 ((node-is "tuple_end") parent-bol 0)
252 ((parent-is "function_call") parent-bol ,terraform-ts-indent-level)
253 ((parent-is "object") parent-bol ,terraform-ts-indent-level)
254 ((parent-is "tuple") parent-bol ,terraform-ts-indent-level)
255 ((parent-is "block") parent-bol ,terraform-ts-indent-level))))
256
257;; Major Mode def
258(define-derived-mode terraform-ts-mode prog-mode "Terraform"
259 "Terraform Tresitter Mode"
260 :group 'terraform
261 :syntax-table terraform-ts--syntax-table
262
263 ;; treesit - add terraform grammar
264 (add-to-list 'treesit-language-source-alist
265 '(terraform . ("https://github.com/MichaHoffmann/tree-sitter-hcl" "main" "dialects/terraform/src")))
266
267 ;; treesit - check grammar is readdy if not most likly in need to be installed
268 (unless (treesit-ready-p 'terraform)
269 (treesit-install-language-grammar 'terraform))
270
271 ;; treesit - init parser
272 (treesit-parser-create 'terraform)
273
274 ;; eglot - integrate mode into terraform-ts-mode
275 (add-hook 'terraform-ts-mode-hook 'eglot-ensure)
276 (with-eval-after-load 'eglot
277 (put 'terraform-ts-mode 'eglot-language-id "terraform")
278 (add-to-list 'eglot-server-programs
279 '(terraform-ts-mode . ("terraform-ls" "serve"))))
280
281 ;; eglot - use format on save
282 (if terraform-ts-format-on-save
283 (add-hook 'before-save-hook 'eglot-format)
284 (remove-hook 'before-save-hook 'eglot-format))
285
286 ;; eglot - disable debugging eglot - increase performance
287 (unless terraform-ts-eglot-debug
288 (fset #'jsonrpc--log-event #'ignore) ; disable eglot event logging
289 (setq eglot-events-buffer-size 0) ; decrease event logging buffer (not needed see above)
290 (setq eglot-sync-connect nil) ; disabling waiting for eglot sync done / might mean that eglot is not avail at opening file
291 )
292
293 (setq-local comment-start "#")
294 (setq-local comment-use-syntax t)
295 (setq-local comment-start-skip "\\(//+\\|/\\*+\\)\\s *")
296
297 ;; Electric
298 (setq-local electric-indent-chars (append "{}[]()" electric-indent-chars))
299
300 ;; Indent.
301 (setq-local treesit-simple-indent-rules terraform-ts--indent-rules)
302
303 ;; Navigation.
304 ;; (setq-local treesit-defun-type-regexp (rx (or "pair" "object")))
305 ;; (setq-local treesit-defun-name-function #'json-ts-mode--defun-name)
306 ;; (setq-local treesit-sentence-type-regexp "pair")
307
308 ;; Font-lock
309 (setq-local treesit-font-lock-feature-list '((comments)
310 (keywords attributes blocks strings numbers constants objects output modules workspaces vars)
311 (builtin brackets delimiters expressions operators interpolations conditionals)
312 ()))
313 (setq-local treesit-font-lock-settings terraform-ts--treesit-font-lock-rules)
314
315 ;; Imenu ... todo
316 ;; (setq-local treesit-simple-imenu-settings
317 ;; `((nil "block" nil nil)))
318
319 (treesit-major-mode-setup))
320
321;;; autoload
322(add-to-list 'auto-mode-alist '("\\.tf\\(vars\\)?\\'" . terraform-ts-mode))
323
324(provide 'terraform-ts-mode)
325;;; terraform-ts-mode.el ends here