Commit 447c093116b4

Vincent Demeester <vincent@sbr.pm>
2026-01-27 16:54:47
feat(emacs): add terraform-ts-mode with LSP support
Add tree-sitter based Terraform editing with eglot/terraform-ls integration. Includes terraform and terraform-ls packages. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 580d5bb
Changed files (4)
dots
.config
home
dots/.config/emacs/site-lisp/terraform-ts-mode.el
@@ -0,0 +1,325 @@
+;;; terraform-ts-mode.el --- Terraform major mode using Treesitter and eglot  -*- lexical-binding: t -*-
+
+;;; Copyright (C) 2022-2027 Kai Grotelüschen
+
+;; Author:     Kai Grotelueschen <kgr@gnotes.de>
+;; Maintainer: Kai Grotelueschen <kgr@gnotes.de>
+;; Version:    0.4
+;; Keywords:   elisp, extensions
+;; Homepage:   https://github.com/kgrotel/terraform-ts-mode
+;; Package-Requires: ((emacs "29.1"))
+
+;; This program is free software; you can redistribute it and/or
+;; modify it under the terms of the GNU General Public License as
+;; published by the Free Software Foundation, either version 3 of
+;; the License, or (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with this program.  If not, see http://www.gnu.org/licenses.
+
+
+;;; Commentary:
+
+;; This is a terraform mode using treesit. There are still quite some
+;; Isues with using Treesitter for imenu and Highlight so any kind of
+;; help is greatly appreaciated
+
+;;; Code:
+
+(require 'treesit)
+(require 'eglot)
+(eval-when-compile (require 'rx))
+
+(declare-function treesit-parser-create "treesit.c")
+(declare-function treesit-query-capture "treesit.c")
+(declare-function treesit-induce-sparse-tree "treesit.c")
+(declare-function treesit-node-child "treesit.c")
+(declare-function treesit-node-start "treesit.c")
+(declare-function treesit-node-type "treesit.c")
+
+(defgroup terraform nil
+  "Support terraform code."
+  :link '(url-link "https://www.terraform.io/")
+  :group 'languages)
+
+;; module customizions
+
+(defcustom terraform-ts-mode-hook nil
+  "Hook called by `terraform-ts-mode'."
+  :type 'hook
+  :group 'terraform)
+
+(defcustom terraform-ts-indent-level 2
+  "The tab width to use when indenting."
+  :type 'integer
+  :group 'terraform)
+
+(defcustom terraform-ts-format-on-save t
+  "Format buffer on save using eglot-format"
+  :type 'boolean
+  :group 'terraform)
+
+(defcustom terraform-ts-eglot-debug nil
+  "enable debugging of eglot (mostly eglot logging) will impact performance"
+  :type 'boolean
+  :group 'terraform)
+
+;; module facses
+
+(defface terraform-resource-type-face
+  '((t :inherit font-lock-type-face))
+  "Face for resource names."
+  :group 'terraform-mode)
+
+(defface terraform-resource-name-face
+  '((t :inherit font-lock-function-name-face))
+  "Face for resource names."
+  :group 'terraform-mode)
+
+(defface terraform-builtin-face
+  '((t :inherit font-lock-builtin-face))
+  "Face for builtins."
+  :group 'terraform-mode)
+
+(defface terraform-variable-name-face
+  '((t :inherit font-lock-variable-name-face))
+  "Face for varriables."
+  :group 'terraform-mode)
+
+;; mode vars 
+
+(defvar terraform-ts--syntax-table
+  (let ((synTable (make-syntax-table)))
+    ;; Word syntax
+    (modify-syntax-entry ?_ "w" synTable)       ; underscore is always part of word (never punctiation) -> w
+    (modify-syntax-entry '(?0 . ?9) "w" synTable)
+    (modify-syntax-entry '(?a . ?z) "w" synTable)
+    (modify-syntax-entry '(?A . ?Z) "w" synTable)
+
+    ;; Punctuation 
+    (modify-syntax-entry ?- "_" synTable)       ; - can be word and punctiation -> _ class 
+    (modify-syntax-entry ?= "." synTable)
+    (modify-syntax-entry ?= "." synTable)
+
+    ;; Whitespace
+    (modify-syntax-entry ?\s " " synTable)
+    (modify-syntax-entry ?\xa0 " " synTable) ; non-breaking space
+    (modify-syntax-entry ?\t " " synTable)
+    (modify-syntax-entry ?\f " " synTable)
+    
+    ;; Brackets
+    (modify-syntax-entry ?\( "()" synTable)
+    (modify-syntax-entry ?\) ")(" synTable)
+    (modify-syntax-entry ?\[ "(]" synTable)
+    (modify-syntax-entry ?\] ")[" synTable)
+    (modify-syntax-entry ?\{ "(}" synTable)
+    (modify-syntax-entry ?\} "){" synTable)
+    
+    ;; Comments
+    (modify-syntax-entry ?# "<" synTable)        ; comment-start-class: single line comment 
+    (modify-syntax-entry ?\n ">" synTable)       ; comment-end-class: eol
+    (modify-syntax-entry ?/  ". 124" synTable)   ; punctuation but also comment: can be // or (12) or /* (1)  */ (4)
+    (modify-syntax-entry ?*  ". 23b" synTable)   ; punctuation but also comment: ca be /* (2) */ (3)
+
+    ;; Others
+    (modify-syntax-entry ?\" "\"" synTable) ; string
+    (modify-syntax-entry ?\\ "\\" synTable) ; escape
+    synTable)
+  "Syntax table for `terraform-ts-mode'.")
+
+;; Imenu
+
+;; MODE VARS
+(defvar terraform-ts--builtin-attributes
+  '("for_each" "count" "source" "type" "default" "providers" "provider")
+  "Terraform builtin attributes for tree-sitter font-locking.")
+
+(defvar terraform-ts--builtin-expressions 
+  '("local" "each" "count")
+  "Terraform builtin expressions for tree-sitter font-locking.")
+
+(defvar terraform-ts--named-expressions 
+  '("var" "module")
+  "Terraform named expressions for tree-sitter font-locking.")
+
+(defvar terraform-ts--treesit-font-lock-rules
+  (treesit-font-lock-rules
+   :language 'terraform
+   :feature 'comments
+   '((comment) @font-lock-comment-face) ;; checkOK
+   
+   :language 'terraform
+   :feature 'brackets
+   '(["(" ")" "[" "]" "{" "}"] @font-lock-bracket-face) ;; checkOK 
+   
+   :language 'terraform
+   :feature 'delimiters
+   '(["." ".*" "," "[*]" "=>"] @font-lock-delimiter-face) ;; checkOK
+
+   :language 'terraform
+   :feature 'operators
+   '(["!"] @font-lock-negation-char-face)
+   
+   :language 'terraform
+   :feature 'operators
+   '(["\*" "/" "%" "\+" "-" ">" ">=" "<" "<=" "==" "!=" "&&" "||"] @font-lock-operator-face)
+ 
+   :language 'terraform
+   :feature 'builtin
+   '((function_call (identifier) @font-lock-builtin-face)) ;; checkOK
+
+   :language 'terraform
+   :feature 'objects
+   '((object_elem key: (expression (variable_expr (identifier) @font-lock-property-name-face))))
+   
+   :language 'terraform
+   :feature 'expressions 
+   `(
+     ((expression (variable_expr (identifier) @terraform-builtin-face)
+		  (get_attr (identifier) @font-lock-property-name-face))
+      (:match ,(rx-to-string `(seq bol (or ,@terraform-ts--builtin-expressions) eol)) @terraform-builtin-face)) ; local, each and count
+     
+     ((expression (variable_expr (identifier) @terraform-builtin-face)
+		  :anchor (get_attr (identifier) @font-lock-function-call-face)
+		  (get_attr (identifier) @font-lock-property-name-face) :* )
+      (:match ,(rx-to-string `(seq bol (or ,@terraform-ts--named-expressions) eol)) @terraform-builtin-face)) ; module and var
+     
+     ((expression (variable_expr (identifier) @terraform-builtin-face)
+		  :anchor (get_attr (identifier) @terraform-resource-type-face)
+		  (get_attr (identifier) @terraform-resource-name-face) 
+		  (get_attr (identifier) @font-lock-property-name-face) :* )
+      (:match "data"  @terraform-builtin-face))
+     
+     ((expression (variable_expr (identifier) @terraform-resource-type-face)
+		  :anchor (get_attr (identifier) @terraform-resource-name-face)
+		  (get_attr (identifier) @font-lock-property-name-face) :* ))  ; that should be a resource 
+    )
+   
+   :language 'terraform
+   :feature 'interpolation
+   '((interpolation "#{" @font-lock-misc-punctuation-face)
+     (interpolation "}" @font-lock-misc-punctuation-face))
+
+   
+   :language 'terraform
+   :feature 'blocks
+   `(
+     ((attribute (identifier) @terraform-builtin-face) (:match ,(rx-to-string `(seq bol (or ,@terraform-ts--builtin-attributes) eol)) @terraform-builtin-face))
+     ((attribute (identifier) @terraform-variable-name-face))
+     )
+   
+   :language 'terraform
+   :feature 'blocks
+   '(
+     ((block (identifier) @terraform-builtin-face (string_lit (template_literal) @font-lock-type-face) (string_lit (template_literal) @font-lock-function-name-face)))
+    )
+   
+   :language 'terraform
+   :feature 'blocks
+   '(
+     ((block (identifier) @terraform-builtin-face (string_lit (template_literal) @font-lock-function-name-face) :?))
+    )
+
+   :language 'terraform
+   :feature 'conditionals
+   '(["if" "else" "endif"] @font-lock-keyword-face)
+    
+   :language 'terraform
+   :feature 'constants
+   '((bool_lit) @font-lock-constant-face) ;; checkOK
+   
+   :language 'terraform
+   :feature 'numbers
+   '((numeric_lit) @font-lock-number-face) ;; checkOK
+
+   :language 'terraform
+   :feature 'strings
+   '((string_lit (template_literal))  @font-lock-string-face)  
+  )
+  "Tree-sitter font-lock settings.")
+
+(defvar terraform-ts--indent-rules
+  `((terraform
+     ((node-is "block_end") parent-bol 0)
+     ((node-is "object_end") parent-bol 0)
+     ((node-is ")") parent-bol 0)
+     ((node-is "tuple_end") parent-bol 0)
+     ((parent-is "function_call") parent-bol ,terraform-ts-indent-level)
+     ((parent-is "object") parent-bol ,terraform-ts-indent-level)
+     ((parent-is "tuple") parent-bol ,terraform-ts-indent-level)
+     ((parent-is "block") parent-bol ,terraform-ts-indent-level))))
+
+;; Major Mode def 
+(define-derived-mode terraform-ts-mode prog-mode "Terraform"
+  "Terraform Tresitter Mode"
+  :group 'terraform
+  :syntax-table terraform-ts--syntax-table
+
+  ;; treesit - add terraform grammar
+  (add-to-list 'treesit-language-source-alist
+      '(terraform . ("https://github.com/MichaHoffmann/tree-sitter-hcl"  "main"  "dialects/terraform/src")))
+
+  ;; treesit - check grammar is readdy if not most likly in need to be installed
+  (unless (treesit-ready-p 'terraform)
+    (treesit-install-language-grammar 'terraform))
+
+  ;; treesit - init parser
+  (treesit-parser-create 'terraform)
+  
+  ;; eglot - integrate mode into terraform-ts-mode
+  (add-hook 'terraform-ts-mode-hook 'eglot-ensure)
+  (with-eval-after-load 'eglot
+    (put 'terraform-ts-mode 'eglot-language-id "terraform")
+    (add-to-list 'eglot-server-programs
+		 '(terraform-ts-mode . ("terraform-ls" "serve"))))
+
+  ;; eglot - use format on save
+  (if terraform-ts-format-on-save
+    (add-hook 'before-save-hook 'eglot-format)
+    (remove-hook 'before-save-hook 'eglot-format))
+
+  ;; eglot - disable debugging eglot - increase performance
+  (unless terraform-ts-eglot-debug
+    (fset #'jsonrpc--log-event #'ignore) ; disable eglot event logging
+    (setq eglot-events-buffer-size 0)    ; decrease event logging buffer (not needed see above)
+    (setq eglot-sync-connect nil)        ; disabling  waiting for eglot sync done / might mean that eglot is not avail at opening file
+  )
+
+  (setq-local comment-start "#")
+  (setq-local comment-use-syntax t)
+  (setq-local comment-start-skip "\\(//+\\|/\\*+\\)\\s *")
+  
+  ;; Electric
+  (setq-local electric-indent-chars (append "{}[]()" electric-indent-chars))
+
+  ;; Indent.
+  (setq-local treesit-simple-indent-rules terraform-ts--indent-rules)
+
+  ;; Navigation.
+  ;; (setq-local treesit-defun-type-regexp (rx (or "pair" "object")))
+  ;; (setq-local treesit-defun-name-function #'json-ts-mode--defun-name)
+  ;; (setq-local treesit-sentence-type-regexp "pair")
+
+  ;; Font-lock
+  (setq-local treesit-font-lock-feature-list '((comments)
+					       (keywords attributes blocks strings numbers constants objects output modules workspaces vars)
+					       (builtin brackets delimiters expressions operators interpolations conditionals)
+					       ()))
+  (setq-local treesit-font-lock-settings terraform-ts--treesit-font-lock-rules)
+
+  ;; Imenu ... todo
+  ;; (setq-local treesit-simple-imenu-settings
+  ;;	       `((nil "block" nil nil)))
+   
+  (treesit-major-mode-setup))
+  
+;;; autoload
+(add-to-list 'auto-mode-alist '("\\.tf\\(vars\\)?\\'" . terraform-ts-mode))
+ 
+(provide 'terraform-ts-mode)
+;;; terraform-ts-mode.el ends here
dots/.config/emacs/init.el
@@ -789,6 +789,10 @@ minibuffer, even without explicitly focusing it."
   :if (executable-find "nix")
   :mode ("\\.nix\\'" "\\.nix.in\\'"))
 
+(use-package terraform-ts-mode
+  :if (executable-find "terraform-ls")
+  :mode ("\\.tf\\'" "\\.tfvars\\'"))
+
 (use-package nix-drv-mode
   :if (executable-find "nix")
   :after nix-mode
home/common/dev/default.nix
@@ -9,6 +9,7 @@
     ./nix.nix
     ./python.nix
     ./rust.nix
+    ./terraform.nix
     ./base.nix
   ]
   ++ lib.optional (builtins.isString desktop) ./desktop.nix;
home/common/dev/terraform.nix
@@ -0,0 +1,7 @@
+{ pkgs, ... }:
+{
+  home.packages = with pkgs; [
+    terraform
+    terraform-ls
+  ];
+}