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

From: Wilhelm Kirschbaum <[email-masked]>
Date: Mon, 06 Mar 2023 21:24:11 +0200

Eli Zaretskii <[email-masked]> writes:

>> >> +(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)))
>> >                                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>> > Copy/paste error, I presume?
>> >
>> > Thanks.
>> 
>> This was intentional, but perhaps a bad choice and lack of 
>> understanding of how the
>> -ts-modes should be activated. elixir-ts-mode should ideally 
>> load 
>>  the
>> HEEx grammar, but should also technically be able to function 
>> without.
>
> Sorry, I don't understand: are you saying that the HEEx grammar
> supports both modes?  I thought you need a separate grammar for
> Elixir.  I also thought the Elixir files have different 
> file-name
> extensions than the HEEx files.  Was I mistaken?
>

No, you were not mistaken. I corrected this with the new patches.

>> The Elixir language author mentioned that heex can practically 
>> be
>> seen as part of Elixir, so requiring heex-ts-mode makes sense, 
>> sort
>> of.
>> 
>> heex-ts-mode and elixir-ts-mode used to be in one file, but I 
>> was
>> asked to split them for the MELPA submission. The HEEx language
>> should actually also be able to embed Elixir, but this is not
>> essential and we can do without imo. Would it make sense have 
>> them
>> in one file?
>
> Maybe.  Otherwise, if they have a lot in common, you'd need to
> duplicate stuff or have a common file used by both.  Your call.

For this release it will be good just to get the basics to work as 
is, but
good to know it is an option. 

Attached are the updated patches. Is this the right format, or 
should I
add them inline rather?

>From 88c941067da0e34e1e9ababeb813ba51378ae2cc Mon Sep 17 00:00:00 2001
From: Wilhelm H Kirschbaum <[email-masked]>
Date: Mon, 6 Mar 2023 21:18:04 +0200
Subject: [PATCH 1/2] Add heex-ts-mode

---
 lisp/progmodes/heex-ts-mode.el                | 185 ++++++++++++++++++
 .../heex-ts-mode-resources/indent.erts        |  47 +++++
 test/lisp/progmodes/heex-ts-mode-tests.el     |   9 +
 3 files changed, 241 insertions(+)
 create mode 100644 lisp/progmodes/heex-ts-mode.el
 create mode 100644 test/lisp/progmodes/heex-ts-mode-resources/indent.erts
 create mode 100644 test/lisp/progmodes/heex-ts-mode-tests.el

diff --git a/lisp/progmodes/heex-ts-mode.el b/lisp/progmodes/heex-ts-mode.el
new file mode 100644
index 00000000000..3feab61a1e7
--- /dev/null
+++ b/lisp/progmodes/heex-ts-mode.el
@@ -0,0 +1,185 @@
+;;; 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 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))
+
+(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 "30.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)))
+
+(if (treesit-ready-p 'heex)
+    ;; 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)))
+
+(provide 'heex-ts-mode)
+;;; heex-ts-mode.el ends here
diff --git a/test/lisp/progmodes/heex-ts-mode-resources/indent.erts b/test/lisp/progmodes/heex-ts-mode-resources/indent.erts
new file mode 100644
index 00000000000..500ddb2b536
--- /dev/null
+++ b/test/lisp/progmodes/heex-ts-mode-resources/indent.erts
@@ -0,0 +1,47 @@
+Code:
+  (lambda ()
+    (setq indent-tabs-mode nil)
+    (heex-ts-mode)
+    (indent-region (point-min) (point-max)))
+
+Point-Char: $
+
+Name: Tag
+
+=-=
+   <div>
+ div
+    </div>
+=-=
+<div>
+  div
+</div>
+=-=-=
+
+Name: Component
+
+=-=
+   <Foo>
+     foobar
+      </Foo>
+=-=
+<Foo>
+  foobar
+</Foo>
+=-=-=
+
+Name: Slots
+
+=-=
+   <Foo>
+   <:bar>
+     foobar
+      </:bar>
+      </Foo>
+=-=
+<Foo>
+  <:bar>
+    foobar
+  </:bar>
+</Foo>
+=-=-=
diff --git a/test/lisp/progmodes/heex-ts-mode-tests.el b/test/lisp/progmodes/heex-ts-mode-tests.el
new file mode 100644
index 00000000000..b59126e136a
--- /dev/null
+++ b/test/lisp/progmodes/heex-ts-mode-tests.el
@@ -0,0 +1,9 @@
+(require 'ert)
+(require 'ert-x)
+(require 'treesit)
+
+(ert-deftest heex-ts-mode-test-indentation ()
+  (skip-unless (treesit-ready-p 'heex))
+  (ert-test-erts-file (ert-resource-file "indent.erts")))
+
+(provide 'heex-ts-mode-tests)
-- 
2.39.2

>From d13c34ed951e3e6fa473cd1bc2e955e20455022b Mon Sep 17 00:00:00 2001
From: Wilhelm H Kirschbaum <[email-masked]>
Date: Mon, 6 Mar 2023 21:18:35 +0200
Subject: [PATCH 2/2] Add elixir-ts-mode

---
 lisp/progmodes/elixir-ts-mode.el              | 626 ++++++++++++++++++
 .../elixir-ts-mode-resources/indent.erts      | 147 ++++
 test/lisp/progmodes/elixir-ts-mode-tests.el   |  31 +
 3 files changed, 804 insertions(+)
 create mode 100644 lisp/progmodes/elixir-ts-mode.el
 create mode 100644 test/lisp/progmodes/elixir-ts-mode-resources/indent.erts
 create mode 100644 test/lisp/progmodes/elixir-ts-mode-tests.el

diff --git a/lisp/progmodes/elixir-ts-mode.el b/lisp/progmodes/elixir-ts-mode.el
new file mode 100644
index 00000000000..0f3c0fee52b
--- /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 provides `elixir-ts-mode' which is a major mode for editing
+;; Elixir files and embedded HEEx templates that uses Tree Sitter to parse
+;; the language.
+;;
+;; This package is compatible with and was tested against the tree-sitter grammar
+;; for Elixir found at https://github.com/elixir-lang/tree-sitter-elixir.
+;;
+;; 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 "30.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$") column-0 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))))
+
+(provide 'elixir-ts-mode)
+;;; elixir-ts-mode.el ends here
diff --git a/test/lisp/progmodes/elixir-ts-mode-resources/indent.erts b/test/lisp/progmodes/elixir-ts-mode-resources/indent.erts
new file mode 100644
index 00000000000..dfb50319bf8
--- /dev/null
+++ b/test/lisp/progmodes/elixir-ts-mode-resources/indent.erts
@@ -0,0 +1,147 @@
+Code:
+  (lambda ()
+    (setq indent-tabs-mode nil)
+    (elixir-ts-mode)
+    (indent-region (point-min) (point-max)))
+
+Point-Char: $
+
+Name: Basic modules
+
+=-=
+  defmodule Foobar do
+def bar() do
+"one"
+      end
+    end
+=-=
+defmodule Foobar do
+  def bar() do
+    "one"
+  end
+end
+=-=-=
+
+
+Name: Map
+
+=-=
+map = %{
+  "a" => 1,
+  "b" => 2
+}
+=-=-=
+
+Name: Block assignments
+
+=-=
+foo =
+  if true do
+    "yes"
+  else
+    "no"
+  end
+=-=-=
+
+Name: Function rescue
+
+=-=
+def foo do
+  "bar"
+rescue
+  e ->
+    "bar"
+end
+=-=-=
+
+
+Name: Pipe statements with fn
+
+=-=
+[1, 2]
+|> Enum.map(fn num ->
+  num + 1
+end)
+=-=-=
+
+Name: Binary operator in else block
+
+=-=
+defp foobar() do
+  if false do
+    :foo
+  else
+    :bar |> foo
+  end
+end
+=-=-=
+
+Name: Tuple indentation
+
+=-=
+tuple = {
+  :one,
+  :two
+}
+
+{
+  :one,
+  :two
+}
+=-=-=
+
+Name: String concatenation in call
+
+=-=
+IO.warn(
+  "one" <>
+    "two" <>
+    "bar"
+)
+
+IO.warn(
+  "foo" <>
+    "bar"
+)
+=-=-=
+
+Name: Incomplete tuple
+
+=-=
+map = {
+:foo
+
+=-=
+map = {
+  :foo
+
+=-=-=
+
+Name: Incomplete map
+
+=-=
+map = %{
+  "a" => "a",
+=-=-=
+
+Name: Incomplete list
+
+=-=
+map = [
+:foo
+
+=-=
+map = [
+  :foo
+
+=-=-=
+
+
+Name: String concatenation
+
+=-=
+"one" <>
+  "two" <>
+  "three" <>
+  "four"
+=-=-=
diff --git a/test/lisp/progmodes/elixir-ts-mode-tests.el b/test/lisp/progmodes/elixir-ts-mode-tests.el
new file mode 100644
index 00000000000..8e546ad5cc6
--- /dev/null
+++ b/test/lisp/progmodes/elixir-ts-mode-tests.el
@@ -0,0 +1,31 @@
+;;; c-ts-mode-tests.el --- Tests for Tree-sitter-based C mode         -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2023 Free Software Foundation, Inc.
+
+;; 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/>.
+
+;;; Code:
+
+(require 'ert)
+(require 'ert-x)
+(require 'treesit)
+
+(ert-deftest elixir-ts-mode-test-indentation ()
+  (skip-unless (and (treesit-ready-p 'elixir) (treesit-ready-p 'heex)))
+  (ert-test-erts-file (ert-resource-file "indent.erts")))
+
+(provide 'elixir-ts-mode-tests)
+;;; elixir-ts-mode-tests.el ends here
-- 
2.39.2