;;; heex-ts-mode.el --- Major mode for Heex with tree-sitter support -*- lexical-binding: t; -*- ;; Copyright (C) 2022-2025 Free Software Foundation, Inc. ;; Author: Wilhelm H Kirschbaum ;; Created: November 2022 ;; Keywords: elixir languages tree-sitter ;; This file is part of GNU Emacs. ;; GNU Emacs 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. ;; GNU Emacs 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 GNU Emacs. If not, see . ;;; Tree-sitter language versions ;; ;; heex-ts-mode has been tested with the following grammars and version: ;; - tree-sitter-heex: v0.7.0 ;; - tree-sitter-elixir: v0.3.3 ;; ;; We try our best to make builtin modes work with latest grammar ;; versions, so a more recent grammar has a good chance to work too. ;; Send us a bug report if it doesn't. ;;; Commentary: ;; ;; This package provides `heex-ts-mode' which is a major mode for editing ;; HEEx files that uses Tree Sitter to parse the language. ;; ;; This package is compatible with and was tested against the tree-sitter grammar ;; for HEEx found at https://github.com/phoenixframework/tree-sitter-heex. ;;; Code: (require 'treesit) (eval-when-compile (require 'rx)) (treesit-declare-unavailable-functions) (add-to-list 'treesit-language-source-alist '(heex "https://github.com/phoenixframework/tree-sitter-heex" :commit "f6b83f305a755cd49cf5f6a66b2b789be93dc7b9") t) (add-to-list 'treesit-language-source-alist '(elixir "https://github.com/elixir-lang/tree-sitter-elixir" :commit "02a6f7fd4be28dd94ee4dd2ca19cb777053ea74e") t) (defgroup heex-ts nil "Major mode for editing HEEx code." :prefix "heex-ts-" :group 'languages) (defcustom heex-ts-indent-offset 2 "Indentation of HEEx statements." :version "30.1" :type 'integer :safe 'integerp :group 'heex-ts) ;; There seems to be no parent directive block for tree-sitter-heex, ;; so we ignore them for now until we learn how to query them. ;; https://github.com/phoenixframework/tree-sitter-heex/issues/28 (defvar heex-ts--indent-rules (let ((offset heex-ts-indent-offset)) `((heex ((parent-is "fragment") (lambda (_node _parent bol &rest _) ;; If HEEx is embedded indent to parent ;; otherwise indent to the bol. (if (eq (treesit-language-at (point-min)) 'heex) (point-min) (save-excursion (goto-char (treesit-node-start (treesit-node-at bol 'elixir))) (back-to-indentation) (point)) )) 0) ((node-is "end_tag") parent-bol 0) ((node-is "end_component") parent-bol 0) ((node-is "end_slot") parent-bol 0) ((node-is "/>") parent-bol 0) ((node-is ">") parent-bol 0) ((node-is "}") parent-bol 0) ((parent-is "comment") prev-adaptive-prefix 0) ((parent-is "component") parent-bol ,offset) ((parent-is "tag") parent-bol ,offset) ((parent-is "start_tag") parent-bol ,offset) ((parent-is "component") parent-bol ,offset) ((parent-is "start_component") parent-bol ,offset) ((parent-is "slot") parent-bol ,offset) ((parent-is "start_slot") parent-bol ,offset) ((parent-is "self_closing_tag") parent-bol ,offset) (no-node parent-bol ,offset))))) (defvar heex-ts--font-lock-settings (when (treesit-available-p) (treesit-font-lock-rules :language 'heex :feature 'heex-comment '((comment) @font-lock-comment-face) :language 'heex :feature 'heex-doctype '((doctype) @font-lock-doc-face) :language 'heex :feature 'heex-tag `([(tag_name) (slot_name)] @font-lock-function-name-face) :language 'heex :feature 'heex-attribute `((attribute_name) @font-lock-variable-name-face) :language 'heex :feature 'heex-keyword `((special_attribute_name) @font-lock-keyword-face) :language 'heex :feature 'heex-string `([(attribute_value) (quoted_attribute_value)] @font-lock-string-face) :language 'heex :feature 'heex-component `([ (component_name) @font-lock-function-name-face (module) @font-lock-keyword-face (function) @font-lock-keyword-face "." @font-lock-keyword-face ]))) "Tree-sitter font-lock settings.") (defvar heex-ts--font-lock-feature-list '(( heex-comment heex-keyword heex-doctype ) ( heex-component heex-tag heex-attribute heex-string ) () ()) "Tree-sitter font-lock feature list.") (defun heex-ts--defun-name (node) "Return the name of the defun NODE. Return nil if NODE is not a defun node or doesn't have a name." (pcase (treesit-node-type node) ((or "component" "slot" "tag") (string-trim (treesit-node-text (treesit-node-child (treesit-node-child node 0) 1) nil))) (_ nil))) (defvar heex-ts--thing-settings `((sexp (not (or (and named ,(rx bos (or "fragment" "comment") eos)) (and anonymous ,(rx (or "" "{" "}")))))) (list ,(rx bos (or "doctype" "tag" "component" "slot" "expression" "directive" "comment") eos)) (sentence ,(rx bos (or "tag_name" "component_name" "attribute") eos)) (text ,(rx bos (or "comment" "text") eos))) "`treesit-thing-settings' for HEEx.") (defvar heex-ts--range-rules (when (treesit-available-p) (treesit-range-rules :embed 'elixir :host 'heex '((directive [(partial_expression_value) (ending_expression_value)] @cap)) :embed 'elixir :host 'heex :local t '((directive (expression_value) @cap) (expression (expression_value) @cap))))) (defvar elixir-ts--font-lock-settings) (defvar elixir-ts--font-lock-feature-list) (defvar elixir-ts--indent-rules) (defvar elixir-ts--thing-settings) ;;;###autoload (define-derived-mode heex-ts-mode html-mode "HEEx" "Major mode for editing HEEx, powered by tree-sitter." :group 'heex-ts (when (treesit-ensure-installed 'heex) (setq treesit-primary-parser (treesit-parser-create 'heex)) ;; Comments (setq-local treesit-thing-settings `((heex ,@heex-ts--thing-settings))) ;; Navigation. (setq-local treesit-defun-type-regexp (rx bol (or "component" "tag" "slot") eol)) (setq-local treesit-defun-name-function #'heex-ts--defun-name) ;; Imenu (setq-local treesit-simple-imenu-settings '(("Component" "\\`component\\'" nil nil) ("Slot" "\\`slot\\'" nil nil) ("Tag" "\\`tag\\'" nil nil))) ;; Outline minor mode ;; `heex-ts-mode' inherits from `html-mode' that sets ;; regexp-based outline variables. So need to restore ;; the default values of outline variables to be able ;; to use `treesit-outline-predicate' derived ;; from `treesit-simple-imenu-settings' above. (kill-local-variable 'outline-heading-end-regexp) (kill-local-variable 'outline-regexp) (kill-local-variable 'outline-level) (setq-local treesit-font-lock-settings heex-ts--font-lock-settings) (setq-local treesit-simple-indent-rules heex-ts--indent-rules) (setq-local treesit-font-lock-feature-list heex-ts--font-lock-feature-list) (when (treesit-ready-p 'elixir) (require 'elixir-ts-mode) (treesit-parser-create 'elixir) (setq-local treesit-range-settings heex-ts--range-rules) (setq-local treesit-font-lock-settings (append treesit-font-lock-settings elixir-ts--font-lock-settings)) (setq-local treesit-font-lock-feature-list (treesit-merge-font-lock-feature-list treesit-font-lock-feature-list elixir-ts--font-lock-feature-list)) (setq-local treesit-simple-indent-rules (append treesit-simple-indent-rules elixir-ts--indent-rules)) (setq-local treesit-thing-settings (append treesit-thing-settings `((elixir ,@elixir-ts--thing-settings))))) (treesit-major-mode-setup) ;; Enable the 'sexp' navigation by default (setq-local forward-sexp-function #'treesit-forward-sexp treesit-sexp-thing 'sexp))) (derived-mode-add-parents 'heex-ts-mode '(heex-mode)) ;;;###autoload (defun heex-ts-mode-maybe () "Enable `heex-ts-mode' when its grammar is available. Also propose to install the grammar when `treesit-enabled-modes' is t or contains the mode name." (declare-function treesit-language-available-p "treesit.c") (if (or (treesit-language-available-p 'heex) (eq treesit-enabled-modes t) (memq 'heex-ts-mode treesit-enabled-modes)) (heex-ts-mode) (fundamental-mode))) ;;;###autoload (when (treesit-available-p) ;; Both .heex and the deprecated .leex files should work ;; with the tree-sitter-heex grammar. (add-to-list 'auto-mode-alist '("\\.[hl]?eex\\'" . heex-ts-mode-maybe)) ;; To be able to toggle between an external package and core ts-mode: (defvar treesit-major-mode-remap-alist) (add-to-list 'treesit-major-mode-remap-alist '(heex-mode . heex-ts-mode))) (provide 'heex-ts-mode) ;;; heex-ts-mode.el ends here