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