Subject: bug#61996: 30.0.50; Submitting elixir-ts-mode and heex-ts-mode

From: Wilhelm Kirschbaum <[email-masked]>
Date: Mon, 06 Mar 2023 09:04:13 +0200

I would like to submit elixir-ts-mode and heex-ts-mode to emacs 
master.

The package elixir-ts-mode and its dependency heex-ts-mode is
currently a melpa package: https://melpa.org/#/elixir-ts-mode. 
This is a
slightly simplified version, also authored by me.

There is one change not authored by me:
https://github.com/wkirschbaum/elixir-ts-mode/commit/21ad74877ebb55f4bf0b31c2f463bbfda72590ef
which is a duplication removal.

I completed the assignment process in Jan.

>From 2c31157207986aacf00d5a8405de09011cbb7d14 Mon Sep 17 00:00:00 2001
From: Wilhelm H Kirschbaum <[email-masked]>
Date: Sun, 5 Mar 2023 16:45:39 +0200
Subject: [PATCH 1/2] Add heex-ts-mode

---
 lisp/progmodes/heex-ts-mode.el | 182 +++++++++++++++++++++++++++++++++
 1 file changed, 182 insertions(+)
 create mode 100644 lisp/progmodes/heex-ts-mode.el

diff --git a/lisp/progmodes/heex-ts-mode.el b/lisp/progmodes/heex-ts-mode.el
new file mode 100644
index 00000000000..e0e879a4b53
--- /dev/null
+++ b/lisp/progmodes/heex-ts-mode.el
@@ -0,0 +1,182 @@
+;;; heex-ts-mode.el --- Major mode for Heex with tree-sitter support -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2022-2023 Free Software Foundation, Inc.
+
+;; Author: Wilhelm H Kirschbaum <[email-masked]>
+;; 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 <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This package defines heex-ts-mode which is a major mode for editing
+;; Elixir and Heex files.
+
+;;; Code:
+
+(require 'treesit)
+(eval-when-compile (require 'rx))
+
+(declare-function treesit-parser-create "treesit.c")
+(declare-function treesit-node-child "treesit.c")
+(declare-function treesit-node-type "treesit.c")
+(declare-function treesit-install-language-grammar "treesit.el")
+
+(defgroup heex-ts nil
+  "Major mode for editing Heex code."
+  :prefix "heex-ts-"
+  :group 'langauges)
+
+(defcustom heex-ts-mode-indent-offset 2
+  "Indentation of Heex statements."
+  :version "29.1"
+  :type 'integer
+  :safe 'integerp
+  :group 'heex-ts)
+
+(defconst heex-ts-mode-sexp-regexp
+  (rx bol
+      (or "directive" "tag" "component" "slot"
+          "attribute" "attribute_value" "quoted_attribute_value")
+      eol))
+
+;; There seems to be no parent directive block
+;; so we ignore it for until we learn how heex treesit
+;; represents directive blocks
+;; https://github.com/phoenixframework/tree-sitter-heex/issues/28
+(defvar heex-ts-mode--indent-rules
+  (let ((offset heex-ts-mode-indent-offset))
+    `((heex
+       ((parent-is "fragment")
+        (lambda (node parent &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 parent))
+              (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)
+       ((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-mode--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-constant-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.")
+
+(defun heex-ts-mode--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)))
+
+(defun heex-ts-mode--forward-sexp (&optional arg)
+  (interactive "^p")
+  (or arg (setq arg 1))
+  (funcall
+   (if (> arg 0) #'treesit-end-of-thing #'treesit-beginning-of-thing)
+   heex-ts-mode-sexp-regexp
+   (abs arg)))
+
+;;;###autoload
+(define-derived-mode heex-ts-mode html-mode "Heex"
+  "Major mode for editing Heex, powered by tree-sitter."
+  :group 'heex-ts
+
+  (when (treesit-ready-p 'heex)
+    (treesit-parser-create 'heex)
+
+    ;; Comments
+    (setq-local treesit-text-type-regexp
+                (regexp-opt '("comment" "text")))
+
+    (setq-local forward-sexp-function #'heex-ts-mode--forward-sexp)
+
+    ;; Navigation.
+    (setq-local treesit-defun-type-regexp
+                (rx bol (or "component" "tag" "slot") eol))
+    (setq-local treesit-defun-name-function #'heex-ts-mode--defun-name)
+
+    ;; Imenu
+    (setq-local treesit-simple-imenu-settings
+                '(("Component" "\\`component\\'" nil nil)
+                  ("Slot" "\\`slot\\'" nil nil)
+                  ("Tag" "\\`tag\\'" nil nil)))
+
+    (setq-local treesit-font-lock-settings heex-ts-mode--font-lock-settings)
+
+    (setq-local treesit-simple-indent-rules heex-ts-mode--indent-rules)
+
+    (setq-local treesit-font-lock-feature-list
+                '(( heex-comment heex-keyword heex-doctype )
+                  ( heex-component heex-tag heex-attribute heex-string )
+                  () ()))
+
+    (treesit-major-mode-setup)))
+
+;; this is a problem when requiring from elixir-ts-mode, so moving there
+;; for now.
+;; (if (treesit-ready-p 'heex)
+;;     (add-to-list 'auto-mode-alist '("\\.[hl]?eex\\'" . heex-ts-mode)))
+
+(provide 'heex-ts-mode)
+;;; heex-ts-mode.el ends here
-- 
2.39.2

>From a1e7a754aa5cd6cd69e50913e3412e5c77a6505e Mon Sep 17 00:00:00 2001
From: Wilhelm H Kirschbaum <[email-masked]>
Date: Sun, 5 Mar 2023 16:45:54 +0200
Subject: [PATCH 2/2] Add elixir-ts-mode

---
 lisp/progmodes/elixir-ts-mode.el | 626 +++++++++++++++++++++++++++++++
 1 file changed, 626 insertions(+)
 create mode 100644 lisp/progmodes/elixir-ts-mode.el

diff --git a/lisp/progmodes/elixir-ts-mode.el b/lisp/progmodes/elixir-ts-mode.el
new file mode 100644
index 00000000000..2bf525c22f2
--- /dev/null
+++ b/lisp/progmodes/elixir-ts-mode.el
@@ -0,0 +1,626 @@
+;;; elixir-ts-mode.el --- Major mode for Elixir with tree-sitter support -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2022-2023 Free Software Foundation, Inc.
+
+;; Author: Wilhelm H Kirschbaum <[email-masked]>
+;; 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 <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This package defines elixir-ts-mode which is a major mode for editing
+;; Elixir and Heex files.
+
+;; Features
+
+;; * Indent
+
+;; elixir-ts-mode tries to replicate the indentation provided by
+;; mix format, but will come with some minor differences.
+
+;; * IMenu
+;; * Navigation
+;; * Which-fun
+
+;;; Code:
+
+(require 'treesit)
+(require 'heex-ts-mode)
+(eval-when-compile (require 'rx))
+
+(declare-function treesit-parser-create "treesit.c")
+(declare-function treesit-node-child "treesit.c")
+(declare-function treesit-node-type "treesit.c")
+(declare-function treesit-node-child-by-field-name "treesit.c")
+(declare-function treesit-parser-language "treesit.c")
+(declare-function treesit-parser-included-ranges "treesit.c")
+(declare-function treesit-parser-list "treesit.c")
+(declare-function treesit-node-parent "treesit.c")
+(declare-function treesit-node-start "treesit.c")
+(declare-function treesit-query-compile "treesit.c")
+(declare-function treesit-install-language-grammar "treesit.el")
+
+(defgroup elixir-ts nil
+  "Major mode for editing Ruby code."
+  :prefix "elixir-ts-"
+  :group 'languages)
+
+(defcustom elixir-ts-mode-indent-offset 2
+  "Indentation of Elixir statements."
+  :version "29.1"
+  :type 'integer
+  :safe 'integerp
+  :group 'elixir-ts)
+
+;; used to distinguish from comment-face in query match
+(defface elixir-ts-font-comment-doc-identifier-face
+  '((t (:inherit font-lock-doc-face)))
+  "For use with @comment.doc tag.")
+
+;; used to distinguish from comment-face in query match
+(defface elixir-ts-font-comment-doc-attribute-face
+  '((t (:inherit font-lock-doc-face)))
+  "For use with @comment.doc.__attribute__ tag.")
+
+;; used to distinguish from special string in query match
+(defface elixir-ts-font-sigil-name-face
+  '((t (:inherit font-lock-string-face)))
+  "For use with @__name__ tag.")
+
+(defconst elixir-ts-mode-sexp-regexp
+  (rx bol
+      (or "call" "stab_clause" "binary_operator" "list" "tuple" "map" "pair"
+          "sigil" "string" "atom" "pair" "alias" "arguments" "atom" "identifier"
+          "boolean" "quoted_content")
+      eol))
+
+(defconst elixir-ts-mode--test-definition-keywords
+  '("describe" "test"))
+
+(defconst elixir-ts-mode--definition-keywords
+  '("def" "defdelegate" "defexception" "defguard" "defguardp"
+    "defimpl" "defmacro" "defmacrop" "defmodule" "defn" "defnp"
+    "defoverridable" "defp" "defprotocol" "defstruct"))
+
+(defconst elixir-ts-mode--definition-keywords-re
+  (concat "^" (regexp-opt elixir-ts-mode--definition-keywords) "$"))
+
+(defconst elixir-ts-mode--kernel-keywords
+  '("alias" "case" "cond" "else" "for" "if" "import" "quote"
+    "raise" "receive" "require" "reraise" "super" "throw" "try"
+    "unless" "unquote" "unquote_splicing" "use" "with"))
+
+(defconst elixir-ts-mode--kernel-keywords-re
+  (concat "^" (regexp-opt elixir-ts-mode--kernel-keywords) "$"))
+
+(defconst elixir-ts-mode--builtin-keywords
+  '("__MODULE__" "__DIR__" "__ENV__" "__CALLER__" "__STACKTRACE__"))
+
+(defconst elixir-ts-mode--builtin-keywords-re
+  (concat "^" (regexp-opt elixir-ts-mode--builtin-keywords) "$"))
+
+(defconst elixir-ts-mode--doc-keywords
+  '("moduledoc" "typedoc" "doc"))
+
+(defconst elixir-ts-mode--doc-keywords-re
+  (concat "^" (regexp-opt elixir-ts-mode--doc-keywords) "$"))
+
+(defconst elixir-ts-mode--reserved-keywords
+  '("when" "and" "or" "not" "in"
+    "not in" "fn" "do" "end" "catch" "rescue" "after" "else"))
+
+(defconst elixir-ts-mode--reserved-keywords-re
+  (concat "^" (regexp-opt elixir-ts-mode--reserved-keywords) "$"))
+
+(defconst elixir-ts-mode--reserved-keywords-vector
+  (apply #'vector elixir-ts-mode--reserved-keywords))
+
+(defvar elixir-ts-mode--capture-anonymous-function-end
+  (when (treesit-available-p)
+    (treesit-query-compile 'elixir '((anonymous_function "end" @end)))))
+
+(defvar elixir-ts-mode--capture-operator-parent
+  (when (treesit-available-p)
+    (treesit-query-compile 'elixir '((binary_operator operator: _ @val)))))
+
+(defvar elixir-ts-mode--syntax-table
+  (let ((table (make-syntax-table)))
+    (modify-syntax-entry ?| "." table)
+    (modify-syntax-entry ?- "." table)
+    (modify-syntax-entry ?+ "." table)
+    (modify-syntax-entry ?* "." table)
+    (modify-syntax-entry ?/ "." table)
+    (modify-syntax-entry ?< "." table)
+    (modify-syntax-entry ?> "." table)
+    (modify-syntax-entry ?_ "_" table)
+    (modify-syntax-entry ?? "w" table)
+    (modify-syntax-entry ?~ "w" table)
+    (modify-syntax-entry ?! "_" table)
+    (modify-syntax-entry ?' "\"" table)
+    (modify-syntax-entry ?\" "\"" table)
+    (modify-syntax-entry ?# "<" table)
+    (modify-syntax-entry ?\n ">" table)
+    (modify-syntax-entry ?\( "()" table)
+    (modify-syntax-entry ?\) ")(" table)
+    (modify-syntax-entry ?\{ "(}" table)
+    (modify-syntax-entry ?\} "){" table)
+    (modify-syntax-entry ?\[ "(]" table)
+    (modify-syntax-entry ?\] ")[" table)
+    (modify-syntax-entry ?: "'" table)
+    (modify-syntax-entry ?@ "'" table)
+    table)
+  "Syntax table for `elixir-ts-mode.")
+
+(defun elixir-ts-mode--call-parent-start (parent)
+  (let ((call-parent
+         (or (treesit-parent-until
+              parent
+              (lambda (node)
+                (equal (treesit-node-type node) "call")))
+             parent)))
+    (save-excursion
+      (goto-char (treesit-node-start call-parent))
+      (back-to-indentation)
+      ;; for pipes we ignore the call indentation
+      (if (looking-at "|>")
+          (point)
+        (treesit-node-start call-parent)))))
+
+(defvar elixir-ts-mode--indent-rules
+  (let ((offset elixir-ts-mode-indent-offset))
+    `((elixir
+       ((parent-is "^source$") point-min 0)
+       ((parent-is "^string$") parent-bol 0)
+       ((parent-is "^quoted_content$")
+        (lambda (_n parent bol &rest _)
+          (save-excursion
+            (back-to-indentation)
+            (if (bolp)
+                (progn
+                  (goto-char (treesit-node-start parent))
+                  (back-to-indentation)
+                  (point))
+              (point)))) 0)
+       ((node-is "^]") parent-bol 0)
+       ((node-is "^|>$") parent-bol 0)
+       ((node-is "^|$") parent-bol 0)
+       ((node-is "^}$") parent-bol 0)
+       ((node-is "^)$")
+        (lambda (_node parent &rest _)
+          (elixir-ts-mode--call-parent-start parent))
+        0)
+       ((node-is "^else_block$") grand-parent 0)
+       ((node-is "^catch_block$") grand-parent 0)
+       ((node-is "^rescue_block$") grand-parent 0)
+       ((node-is "^after_block$") grand-parent 0)
+       ((parent-is "^else_block$") parent ,offset)
+       ((parent-is "^catch_block$") parent ,offset)
+       ((parent-is "^rescue_block$") parent ,offset)
+       ((parent-is "^rescue_block$") parent ,offset)
+       ((parent-is "^after_block$") parent ,offset)
+       ((parent-is "^tuple$") parent-bol ,offset)
+       ((parent-is "^list$") parent-bol ,offset)
+       ((parent-is "^pair$") parent ,offset)
+       ((parent-is "^map_content$") parent-bol 0)
+       ((parent-is "^map$") parent-bol ,offset)
+       ((node-is "^stab_clause$") parent-bol ,offset)
+       ((query ,elixir-ts-mode--capture-operator-parent) grand-parent 0)
+       ((node-is "^when$") parent 0)
+       ((node-is "^keywords$") parent-bol ,offset)
+       ((parent-is "^body$")
+        (lambda (node parent _)
+          (save-excursion
+            ;; the grammar adds a comment outside of the body, so we have to indent
+            ;; to the grand-parent if it is available
+            (goto-char (treesit-node-start
+                        (or (treesit-node-parent parent) (parent))))
+            (back-to-indentation)
+            (point)))
+        ,offset)
+       ((parent-is "^arguments$")
+        ;; the first argument must indent ,offset from start of call
+        ;; otherwise indent should be the same as the first argument
+        (lambda (node parent bol &rest _)
+          (let ((first-child (treesit-node-child parent 0 t)))
+            (cond ((null first-child)
+                   (elixir-ts-mode--call-parent-start parent))
+                  ((treesit-node-eq node first-child)
+                   (elixir-ts-mode--call-parent-start parent))
+                  (t (elixir-ts-mode--call-parent-start parent)))))
+        (lambda (node parent rest)
+          ;; if first-child offset otherwise don't
+          (let ((first-child (treesit-node-child parent 0 t)))
+            (cond ((null first-child) ,offset)
+                  ((treesit-node-eq node first-child) ,offset)
+                  (t 0)))))
+       ;; handle incomplete maps when parent is ERROR
+       ((n-p-gp "^binary_operator$" "ERROR" nil) parent-bol 0)
+       ;; When there is an ERROR, just indent to prev-line
+       ;; Not sure why it needs one more, but adding it for now
+       ((parent-is "ERROR") prev-line 1)
+       ((node-is "^binary_operator$")
+        (lambda (node parent &rest _)
+          (let ((top-level
+                 (treesit-parent-while
+                  node
+                  (lambda (node)
+                    (equal (treesit-node-type node)
+                           "binary_operator")))))
+            (if (treesit-node-eq top-level node)
+                (elixir-ts-mode--call-parent-start parent)
+              (treesit-node-start top-level))))
+        (lambda (node parent _)
+          (cond
+           ((equal (treesit-node-type parent) "do_block")
+            ,offset)
+           ((equal (treesit-node-type parent) "binary_operator")
+            ,offset)
+           (t 0))))
+       ((parent-is "^binary_operator$")
+        (lambda (node parent bol &rest _)
+          (treesit-node-start
+           (treesit-parent-while
+            parent
+            (lambda (node)
+              (equal (treesit-node-type node) "binary_operator")))))
+        ,offset)
+       ((node-is "^pair$") first-sibling 0)
+       ((query ,elixir-ts-mode--capture-anonymous-function-end) parent-bol 0)
+       ((node-is "^end$")
+        (lambda (_node parent &rest _)
+          (elixir-ts-mode--call-parent-start parent)) 0)
+       ((parent-is "^do_block$") grand-parent ,offset)
+       ((parent-is "^anonymous_function$")
+        elixir-ts-mode--treesit-anchor-grand-parent-bol ,offset)
+       ((parent-is "^else_block$") parent ,offset)
+       ((parent-is "^rescue_block$") parent ,offset)
+       ((parent-is "^catch_block$") parent ,offset)
+       ((parent-is "^keywords$") parent-bol 0)
+       ((node-is "^call$") parent-bol ,offset)
+       ((node-is "^comment$") parent-bol ,offset)))))
+
+(defvar elixir-ts-mode--font-lock-settings
+  (treesit-font-lock-rules
+   :language 'elixir
+   :feature 'elixir-comment
+   '((comment) @font-lock-comment-face)
+
+   :language 'elixir
+   :feature 'elixir-string
+   :override t
+   '([(string) (charlist)] @font-lock-string-face)
+
+   :language 'elixir
+   :feature 'elixir-string-interpolation
+   :override t
+   '((string
+      [
+       quoted_end: _ @font-lock-string-face
+       quoted_start: _ @font-lock-string-face
+       (quoted_content) @font-lock-string-face
+       (interpolation
+        "#{" @font-lock-regexp-grouping-backslash "}"
+        @font-lock-regexp-grouping-backslash)
+       ])
+     (charlist
+      [
+       quoted_end: _ @font-lock-string-face
+       quoted_start: _ @font-lock-string-face
+       (quoted_content) @font-lock-string-face
+       (interpolation
+        "#{" @font-lock-regexp-grouping-backslash "}"
+        @font-lock-regexp-grouping-backslash)
+       ]))
+
+   :language 'elixir
+   :feature 'elixir-keyword
+   ;; :override `prepend
+   `(,elixir-ts-mode--reserved-keywords-vector
+     @font-lock-keyword-face
+     ;; these are operators, should we mark them as keywords?
+     (binary_operator
+      operator: _ @font-lock-keyword-face
+      (:match ,elixir-ts-mode--reserved-keywords-re @font-lock-keyword-face)))
+
+   :language 'elixir
+   :feature 'elixir-doc
+   :override t
+   `((unary_operator
+      operator: "@" @elixir-ts-font-comment-doc-attribute-face
+      operand: (call
+                target: (identifier) @elixir-ts-font-comment-doc-identifier-face
+                ;; Arguments can be optional, so adding another
+                ;; entry without arguments.
+                ;; If we don't handle then we don't apply font
+                ;; and the non doc fortification query will take specify
+                ;; a more specific font which takes precedence.
+                (arguments
+                 [
+                  (string) @font-lock-doc-face
+                  (charlist) @font-lock-doc-face
+                  (sigil) @font-lock-doc-face
+                  (boolean) @font-lock-doc-face
+                  ]))
+      (:match ,elixir-ts-mode--doc-keywords-re
+              @elixir-ts-font-comment-doc-identifier-face))
+     (unary_operator
+      operator: "@" @elixir-ts-font-comment-doc-attribute-face
+      operand: (call
+                target: (identifier) @elixir-ts-font-comment-doc-identifier-face)
+      (:match ,elixir-ts-mode--doc-keywords-re
+              @elixir-ts-font-comment-doc-identifier-face)))
+
+   :language 'elixir
+   :feature 'elixir-unary-operator
+   `((unary_operator operator: "@" @font-lock-preprocessor-face
+                     operand: [
+                               (identifier)  @font-lock-preprocessor-face
+                               (call target: (identifier)
+                                     @font-lock-preprocessor-face)
+                               (boolean)  @font-lock-preprocessor-face
+                               (nil)  @font-lock-preprocessor-face
+                               ])
+
+     (unary_operator operator: "&") @font-lock-function-name-face
+     (operator_identifier) @font-lock-operator-face)
+
+   :language 'elixir
+   :feature 'elixir-operator
+   '((binary_operator operator: _ @font-lock-operator-face)
+     (dot operator: _ @font-lock-operator-face)
+     (stab_clause operator: _ @font-lock-operator-face)
+
+     [(boolean) (nil)] @font-lock-constant-face
+     [(integer) (float)] @font-lock-number-face
+     (alias) @font-lock-type-face
+     (call target: (dot left: (atom) @font-lock-type-face))
+     (char) @font-lock-constant-face
+     [(atom) (quoted_atom)] @font-lock-type-face
+     [(keyword) (quoted_keyword)] @font-lock-builtin-face)
+
+   :language 'elixir
+   :feature 'elixir-call
+   `((call
+      target: (identifier) @font-lock-keyword-face
+      (:match ,elixir-ts-mode--definition-keywords-re @font-lock-keyword-face))
+     (call
+      target: (identifier) @font-lock-keyword-face
+      (:match ,elixir-ts-mode--kernel-keywords-re @font-lock-keyword-face))
+     (call
+      target: [(identifier) @font-lock-function-name-face
+               (dot right: (identifier) @font-lock-keyword-face)])
+     (call
+      target: (identifier) @font-lock-keyword-face
+      (arguments
+       [
+        (identifier) @font-lock-keyword-face
+        (binary_operator
+         left: (identifier) @font-lock-keyword-face
+         operator: "when")
+        ])
+      (:match ,elixir-ts-mode--definition-keywords-re @font-lock-keyword-face))
+     (call
+      target: (identifier) @font-lock-keyword-face
+      (arguments
+       (binary_operator
+        operator: "|>"
+        right: (identifier)))
+      (:match ,elixir-ts-mode--definition-keywords-re @font-lock-keyword-face)))
+
+   :language 'elixir
+   :feature 'elixir-constant
+   `((binary_operator operator: "|>" right: (identifier)
+                      @font-lock-function-name-face)
+     ((identifier) @font-lock-keyword-face
+      (:match ,elixir-ts-mode--builtin-keywords-re
+              @font-lock-keyword-face))
+     ((identifier) @font-lock-comment-face
+      (:match "^_" @font-lock-comment-face))
+     (identifier) @font-lock-function-name-face
+     ["%"] @font-lock-keyward-face
+     ["," ";"] @font-lock-keyword-face
+     ["(" ")" "[" "]" "{" "}" "<<" ">>"] @font-lock-keyword-face)
+
+   :language 'elixir
+   :feature 'elixir-sigil
+   :override t
+   `((sigil
+      (sigil_name) @elixir-ts-font-sigil-name-face
+      quoted_start: _ @font-lock-string-face
+      quoted_end: _ @font-lock-string-face
+      (:match "^[sSwWpP]$" @elixir-ts-font-sigil-name-face))
+     @font-lock-string-face
+     (sigil
+      (sigil_name) @elixir-ts-font-sigil-name-face
+      quoted_start: _ @font-lock-regex-face
+      quoted_end: _ @font-lock-regex-face
+      (:match "^[rR]$" @elixir-ts-font-sigil-name-face))
+     @font-lock-regex-face
+     (sigil
+      "~" @font-lock-string-face
+      (sigil_name) @elixir-ts-font-sigil-name-face
+      quoted_start: _ @font-lock-string-face
+      quoted_end: _ @font-lock-string-face
+      (:match "^[HF]$" @elixir-ts-font-sigil-name-face)))
+
+   :language 'elixir
+   :feature 'elixir-string-escape
+   :override t
+   `((escape_sequence) @font-lock-regexp-grouping-backslash))
+  "Tree-sitter font-lock settings.")
+
+(defun elixir-ts-mode--forward-sexp (&optional arg)
+  (interactive "^p")
+  (or arg (setq arg 1))
+  (funcall
+   (if (> arg 0) #'treesit-end-of-thing #'treesit-beginning-of-thing)
+   (if (eq (treesit-language-at (point)) 'heex)
+       heex-ts-mode-sexp-regexp
+     elixir-ts-mode-sexp-regexp)
+   (abs arg)))
+
+(defun elixir-ts-mode--treesit-anchor-grand-parent-bol (_n parent &rest _)
+  "Return the beginning of non-space characters for the parent node of PARENT."
+  (save-excursion
+    (goto-char (treesit-node-start (treesit-node-parent parent)))
+    (back-to-indentation)
+    (point)))
+
+(defvar elixir-ts-mode--treesit-range-rules
+  (when (treesit-available-p)
+    (treesit-range-rules
+     :embed 'heex
+     :host 'elixir
+     '((sigil (sigil_name) @name (:match "^[HF]$" @name) (quoted_content) @heex)))))
+
+(defun elixir-ts-mode--treesit-language-at-point (point)
+  "Return the language at POINT."
+  (let* ((range nil)
+         (language-in-range
+          (cl-loop
+           for parser in (treesit-parser-list)
+           do (setq range
+                    (cl-loop
+                     for range in (treesit-parser-included-ranges parser)
+                     if (and (>= point (car range)) (<= point (cdr range)))
+                     return parser))
+           if range
+           return (treesit-parser-language parser))))
+    (if (null language-in-range)
+        (when-let ((parser (car (treesit-parser-list))))
+          (treesit-parser-language parser))
+      language-in-range)))
+
+(defun elixir-ts-mode--defun-p (node)
+  "Return non-nil when NODE is a defun."
+  (member (treesit-node-text
+           (treesit-node-child-by-field-name node "target"))
+          (append
+           elixir-ts-mode--definition-keywords
+           elixir-ts-mode--test-definition-keywords)))
+
+(defun elixir-ts-mode--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)
+    ("call" (let ((node-child
+                   (treesit-node-child (treesit-node-child node 1) 0)))
+              (pcase (treesit-node-type node-child)
+                ("alias" (treesit-node-text node-child t))
+                ("call" (treesit-node-text
+                         (treesit-node-child-by-field-name node-child "target") t))
+                ("binary_operator"
+                 (treesit-node-text
+                  (treesit-node-child-by-field-name
+                   (treesit-node-child-by-field-name node-child "left") "target") t))
+                ("identifier"
+                 (treesit-node-text node-child t))
+                (_ nil))))
+    (_ nil)))
+
+;;;###autoload
+(define-derived-mode elixir-ts-mode prog-mode "Elixir"
+  "Major mode for editing Elixir, powered by tree-sitter."
+  :group 'elixir-ts
+  :syntax-table elixir-ts-mode--syntax-table
+
+  ;; Comments
+  (setq-local comment-start "# ")
+  (setq-local comment-start-skip
+              (rx "#" (* (syntax whitespace))))
+
+  (setq-local comment-end "")
+  (setq-local comment-end-skip
+              (rx (* (syntax whitespace))
+                  (group (or (syntax comment-end) "\n"))))
+
+  ;; Compile
+  (setq-local compile-command "mix")
+
+  (when (treesit-ready-p 'elixir)
+    ;; heex has to be created first for elixir to ensure elixir
+    ;; is the first language when looking for treesit ranges
+    (if (treesit-ready-p 'heex)
+        (treesit-parser-create 'heex))
+
+    (treesit-parser-create 'elixir)
+
+    (setq-local treesit-language-at-point-function
+                'elixir-ts-mode--treesit-language-at-point)
+
+    ;; Font-lock.
+    (setq-local treesit-font-lock-settings elixir-ts-mode--font-lock-settings)
+    (setq-local treesit-font-lock-feature-list
+                '(( elixir-comment elixir-constant elixir-doc )
+                  ( elixir-string elixir-keyword elixir-unary-operator
+                    elixir-call elixir-operator )
+                  ( elixir-sigil elixir-string-escape elixir-string-interpolation)))
+
+    ;; Imenu.
+    (setq-local treesit-simple-imenu-settings
+                '((nil "\\`call\\'" elixir-ts-mode--defun-p nil)))
+
+    ;; Indent.
+    (setq-local treesit-simple-indent-rules elixir-ts-mode--indent-rules)
+
+    ;; Navigation
+    (setq-local forward-sexp-function #'elixir-ts-mode--forward-sexp)
+    (setq-local treesit-defun-type-regexp
+                '("call" . elixir-ts-mode--defun-p))
+
+    (setq-local treesit-defun-name-function #'elixir-ts-mode--defun-name)
+
+    ;; Embedded Heex
+    (when (treesit-ready-p 'heex)
+      (setq-local treesit-range-settings elixir-ts-mode--treesit-range-rules)
+
+      (setq-local treesit-simple-indent-rules
+                  (append treesit-simple-indent-rules heex-ts-mode--indent-rules))
+
+      (setq-local treesit-font-lock-settings
+                  (append treesit-font-lock-settings
+                          heex-ts-mode--font-lock-settings))
+
+      (setq-local treesit-simple-indent-rules
+                  (append treesit-simple-indent-rules
+                          heex-ts-mode--indent-rules))
+
+      (setq-local treesit-font-lock-feature-list
+                  '(( elixir-comment elixir-constant elixir-doc
+                      heex-comment heex-keyword heex-doctype )
+                    ( elixir-string elixir-keyword elixir-unary-operator
+                      elixir-call elixir-operator
+                      heex-component heex-tag heex-attribute heex-string)
+                    ( elixir-sigil elixir-string-escape
+                      elixir-string-interpolation ))))
+
+    (treesit-major-mode-setup)))
+
+(if (treesit-ready-p 'elixir)
+    (progn
+      (add-to-list 'auto-mode-alist '("\\.elixir\\'" . elixir-ts-mode))
+      (add-to-list 'auto-mode-alist '("\\.ex\\'" . elixir-ts-mode))
+      (add-to-list 'auto-mode-alist '("\\.exs\\'" . elixir-ts-mode))
+      (add-to-list 'auto-mode-alist '("mix\\.lock" . elixir-ts-mode))))
+
+(if (treesit-ready-p 'heex)
+    (add-to-list 'auto-mode-alist '("\\.[hl]?eex\\'" . heex-ts-mode)))
+
+(provide 'elixir-ts-mode)
+
+;;; elixir-ts-mode.el ends here
-- 
2.39.2