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

From: Wilhelm Kirschbaum <[email-masked]>
Date: Sun, 12 Mar 2023 14:23:01 +0200

Eli Zaretskii <[email-masked]> writes:

>> From: Wilhelm Kirschbaum <[email-masked]>
>> Cc: [email-masked], [email-masked], [email-masked]
>> Date: Sun, 12 Mar 2023 11:54:33 +0200
>> 
>> Eli Zaretskii <[email-masked]> writes:
>> 
>> >   In elixir-ts--call-parent-start:
>> >   progmodes/elixir-ts-mode.el:459:38: Warning: Unused lexical 
>> >   argument `node'
>> >   progmodes/elixir-ts-mode.el:463:15: Warning: reference to 
>> >   free 
>> >   variable `parent'
>> >
>> >   In elixir-ts--forward-sexp:
>> >   progmodes/elixir-ts-mode.el:482:8: Warning: reference to 
>> >   free 
>> >   variable `heex-ts--sexp-regexp'
>> 
>> Ah, not sure how I missed them. The new patches have further 
>> tweaks and
>> should resolve the above issue.
>
> Thanks, but the first of the two patches lacks the commit log
> message.  And since you said there are further tweaks, I wasn't 
> sure
> the one from the previous version was still accurate.

Sorry about that. The workflow is still pretty foreign to me and
juggling with the old github upstream. I added the commit log.

>From 2b4cee1de0b3a038f6c78da12ddddb338553878b Mon Sep 17 00:00:00 2001
From: Wilhelm H Kirschbaum <[email-masked]>
Date: Sun, 12 Mar 2023 11:44:36 +0200
Subject: [PATCH 1/2] Add heex-ts-mode (Bug#61996)

* etc/NEWS: Mention the new mode.
* lisp/progmodes/heex-ts-mode.el: New file.
* test/lisp/progmodes/heex-ts-mode-tests.el: New file.
* test/lisp/progmodes/heex-ts-mode-resources/indent.erts: New file.
* admin/notes/tree-sitter/build-module/batch.sh:
* admin/notes/tree-sitter/build-module/build.sh: Add HEEx support.
---
 admin/notes/tree-sitter/build-module/batch.sh |   1 +
 admin/notes/tree-sitter/build-module/build.sh |   3 +
 etc/NEWS                                      |   3 +
 lisp/progmodes/heex-ts-mode.el                | 185 ++++++++++++++++++
 .../heex-ts-mode-resources/indent.erts        |  47 +++++
 test/lisp/progmodes/heex-ts-mode-tests.el     |   9 +
 6 files changed, 248 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/admin/notes/tree-sitter/build-module/batch.sh b/admin/notes/tree-sitter/build-module/batch.sh
index 58272c74549..8b0072782e8 100755
--- a/admin/notes/tree-sitter/build-module/batch.sh
+++ b/admin/notes/tree-sitter/build-module/batch.sh
@@ -10,6 +10,7 @@ languages=
     'dockerfile'
     'go'
     'go-mod'
+    'heex'
     'html'
     'javascript'
     'json'
diff --git a/admin/notes/tree-sitter/build-module/build.sh b/admin/notes/tree-sitter/build-module/build.sh
index 9dc674237ca..78ecfb5bc82 100755
--- a/admin/notes/tree-sitter/build-module/build.sh
+++ b/admin/notes/tree-sitter/build-module/build.sh
@@ -36,6 +36,9 @@ grammardir=
         lang="gomod"
         org="camdencheek"
         ;;
+    "heex")
+        org="phoenixframework"
+        ;;
     "typescript")
         sourcedir="tree-sitter-typescript/typescript/src"
         grammardir="tree-sitter-typescript/typescript"
diff --git a/etc/NEWS b/etc/NEWS
index 13d073c7fb8..ed74d0be1a1 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -230,6 +230,9 @@ This replaces 'doc-view-svg-foreground' and 'doc-view-svg-background'.
 An optional major mode based on the tree-sitter library for editing
 HTML files.
 
+*** New major mode heex-ts-mode'.
+A major mode based on the tree-sitter library for editing HEEx files.
+
 ---
 ** The highly accessible Modus themes collection has six items.
 The 'modus-operandi' and 'modus-vivendi' are the main themes that have
diff --git a/lisp/progmodes/heex-ts-mode.el b/lisp/progmodes/heex-ts-mode.el
new file mode 100644
index 00000000000..68a537b9229
--- /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-node-start "treesit.c")
+
+(defgroup heex-ts nil
+  "Major mode for editing HEEx code."
+  :prefix "heex-ts-"
+  :group 'langauges)
+
+(defcustom heex-ts-indent-offset 2
+  "Indentation of HEEx statements."
+  :version "30.1"
+  :type 'integer
+  :safe 'integerp
+  :group 'heex-ts)
+
+(defconst heex-ts--sexp-regexp
+  (rx bol
+      (or "directive" "tag" "component" "slot"
+          "attribute" "attribute_value" "quoted_attribute_value")
+      eol))
+
+;; 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 &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--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--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--forward-sexp (&optional arg)
+  "Move forward across one balanced expression (sexp).
+With ARG, do it many times.  Negative ARG means move backward."
+  (or arg (setq arg 1))
+  (funcall
+   (if (> arg 0) #'treesit-end-of-thing #'treesit-beginning-of-thing)
+   heex-ts--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--forward-sexp)
+
+    ;; 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)))
+
+    (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-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 56f4a551bc65c9a64fdcf7fb7c2e0731f724ad99 Mon Sep 17 00:00:00 2001
From: Wilhelm H Kirschbaum <[email-masked]>
Date: Sun, 12 Mar 2023 11:45:11 +0200
Subject: [PATCH 2/2] Add elixir-ts-mode (Bug#61996)

* etc/NEWS: Mention the new mode.
* lisp/progmodes/elixir-ts-mode.el: New file.
* test/lisp/progmodes/elixir-ts-mode-tests.el: New file.
* test/lisp/progmodes/elixir-ts-mode-resources/indent.erts: New file.
* admin/notes/tree-sitter/build-module/batch.sh:
* admin/notes/tree-sitter/build-module/build.sh: Add Elixir support.
* lisp/progmodes/eglot.el (eglot-server-programs): Add elixir-ts-mode.
---
 admin/notes/tree-sitter/build-module/batch.sh |   1 +
 admin/notes/tree-sitter/build-module/build.sh |   3 +
 etc/NEWS                                      |   4 +
 lisp/progmodes/eglot.el                       |   2 +-
 lisp/progmodes/elixir-ts-mode.el              | 620 ++++++++++++++++++
 .../elixir-ts-mode-resources/indent.erts      | 233 +++++++
 test/lisp/progmodes/elixir-ts-mode-tests.el   |  31 +
 7 files changed, 893 insertions(+), 1 deletion(-)
 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/admin/notes/tree-sitter/build-module/batch.sh b/admin/notes/tree-sitter/build-module/batch.sh
index 8b0072782e8..1d4076564dc 100755
--- a/admin/notes/tree-sitter/build-module/batch.sh
+++ b/admin/notes/tree-sitter/build-module/batch.sh
@@ -8,6 +8,7 @@ languages=
     'css'
     'c-sharp'
     'dockerfile'
+    'elixir'
     'go'
     'go-mod'
     'heex'
diff --git a/admin/notes/tree-sitter/build-module/build.sh b/admin/notes/tree-sitter/build-module/build.sh
index 78ecfb5bc82..0832875168b 100755
--- a/admin/notes/tree-sitter/build-module/build.sh
+++ b/admin/notes/tree-sitter/build-module/build.sh
@@ -31,6 +31,9 @@ grammardir=
     "cmake")
         org="uyha"
         ;;
+    "elixir")
+        org="elixir-lang"
+        ;;
     "go-mod")
         # The parser is called "gomod".
         lang="gomod"
diff --git a/etc/NEWS b/etc/NEWS
index ed74d0be1a1..d40fa51d80d 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -233,6 +233,10 @@ HTML files.
 *** New major mode heex-ts-mode'.
 A major mode based on the tree-sitter library for editing HEEx files.
 
+*** New major mode elixir-ts-mode'.
+A major mode based on the tree-sitter library for editing Elixir
+files.
+
 ---
 ** The highly accessible Modus themes collection has six items.
 The 'modus-operandi' and 'modus-vivendi' are the main themes that have
diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el
index 2f8d2002cd3..7b2341f3f49 100644
--- a/lisp/progmodes/eglot.el
+++ b/lisp/progmodes/eglot.el
@@ -221,7 +221,7 @@ eglot-server-programs
                                 ((java-mode java-ts-mode) . ("jdtls"))
                                 (dart-mode . ("dart" "language-server"
                                               "--client-id" "emacs.eglot-dart"))
-                                (elixir-mode . ("language_server.sh"))
+                                ((elixir-ts-mode elixir-mode) . ("language_server.sh"))
                                 (ada-mode . ("ada_language_server"))
                                 (scala-mode . ,(eglot-alternatives
                                                 '("metals" "metals-emacs")))
diff --git a/lisp/progmodes/elixir-ts-mode.el b/lisp/progmodes/elixir-ts-mode.el
new file mode 100644
index 00000000000..08d73cd55eb
--- /dev/null
+++ b/lisp/progmodes/elixir-ts-mode.el
@@ -0,0 +1,620 @@
+;;; 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-node-eq "treesit.c")
+
+(defgroup elixir-ts nil
+  "Major mode for editing Elixir code."
+  :prefix "elixir-ts-"
+  :group 'languages)
+
+(defcustom elixir-ts-indent-offset 2
+  "Indentation of Elixir statements."
+  :version "30.1"
+  :type 'integer
+  :safe 'integerp
+  :group 'elixir-ts)
+
+(defface elixir-ts-font-comment-doc-identifier-face
+  '((t (:inherit font-lock-doc-face)))
+  "Face used for @comment.doc tags in Elixir files.")
+
+(defface elixir-ts-font-comment-doc-attribute-face
+  '((t (:inherit font-lock-doc-face)))
+  "Face used for @comment.doc.__attribute__ tags in Elixir files.")
+
+(defface elixir-ts-font-sigil-name-face
+  '((t (:inherit font-lock-string-face)))
+  "Face used for @__name__ tags in Elixir files.")
+
+(defconst elixir-ts--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--test-definition-keywords
+  '("describe" "test"))
+
+(defconst elixir-ts--definition-keywords
+  '("def" "defdelegate" "defexception" "defguard" "defguardp"
+    "defimpl" "defmacro" "defmacrop" "defmodule" "defn" "defnp"
+    "defoverridable" "defp" "defprotocol" "defstruct"))
+
+(defconst elixir-ts--definition-keywords-re
+  (concat "^" (regexp-opt elixir-ts--definition-keywords) "$"))
+
+(defconst elixir-ts--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--kernel-keywords-re
+  (concat "^" (regexp-opt elixir-ts--kernel-keywords) "$"))
+
+(defconst elixir-ts--builtin-keywords
+  '("__MODULE__" "__DIR__" "__ENV__" "__CALLER__" "__STACKTRACE__"))
+
+(defconst elixir-ts--builtin-keywords-re
+  (concat "^" (regexp-opt elixir-ts--builtin-keywords) "$"))
+
+(defconst elixir-ts--doc-keywords
+  '("moduledoc" "typedoc" "doc"))
+
+(defconst elixir-ts--doc-keywords-re
+  (concat "^" (regexp-opt elixir-ts--doc-keywords) "$"))
+
+(defconst elixir-ts--reserved-keywords
+  '("when" "and" "or" "not" "in"
+    "not in" "fn" "do" "end" "catch" "rescue" "after" "else"))
+
+(defconst elixir-ts--reserved-keywords-re
+  (concat "^" (regexp-opt elixir-ts--reserved-keywords) "$"))
+
+(defconst elixir-ts--reserved-keywords-vector
+  (apply #'vector elixir-ts--reserved-keywords))
+
+(defvar elixir-ts--capture-anonymous-function-end
+  (when (treesit-available-p)
+    (treesit-query-compile 'elixir '((anonymous_function "end" @end)))))
+
+(defvar elixir-ts--capture-operator-parent
+  (when (treesit-available-p)
+    (treesit-query-compile 'elixir '((binary_operator operator: _ @val)))))
+
+(defvar elixir-ts--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'.")
+
+(defvar elixir-ts--indent-rules
+  (let ((offset elixir-ts-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--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--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$")
+        ;; If there is no previous sibling indent
+        ;; to the call parent, otherwise
+        ;; indent to the same column as the prev-sibling.
+        (lambda (node parent &rest _)
+          (let ((prev-sibling (treesit-node-prev-sibling node t)))
+          (if prev-sibling
+              (treesit-node-start prev-sibling)
+            (elixir-ts--call-parent-start parent))))
+        (lambda (node parent &rest _)
+          (if (treesit-node-prev-sibling node t) 0 ,offset)))
+       ;; 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.
+       ((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--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--capture-anonymous-function-end) parent-bol 0)
+       ((node-is "^end$")
+        (lambda (_node parent &rest _)
+          (elixir-ts--call-parent-start parent))
+        0)
+       ((parent-is "^do_block$") grand-parent ,offset)
+       ((parent-is "^anonymous_function$")
+        elixir-ts--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--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
+   `(,elixir-ts--reserved-keywords-vector
+     @font-lock-keyword-face
+     (binary_operator
+      operator: _ @font-lock-keyword-face
+      (:match ,elixir-ts--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--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--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--definition-keywords-re @font-lock-keyword-face))
+     (call
+      target: (identifier) @font-lock-keyword-face
+      (:match ,elixir-ts--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--definition-keywords-re @font-lock-keyword-face))
+     (call
+      target: (identifier) @font-lock-keyword-face
+      (arguments
+       (binary_operator
+        operator: "|>"
+        right: (identifier)))
+      (:match ,elixir-ts--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--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.")
+
+(defvar elixir-ts--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--call-parent-start (node)
+  "Return the closest parent of NODE that is of type call."
+  (let ((call-parent
+         (or (treesit-parent-until
+              node
+              (lambda (n)
+                (equal (treesit-node-type n) "call")))
+             node)))
+    (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)))))
+
+(defun elixir-ts--forward-sexp (&optional arg)
+  "Move forward across one balanced expression (sexp).
+With ARG, do it many times.  Negative ARG means move backward."
+  (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--sexp-regexp
+     elixir-ts--sexp-regexp)
+   (abs arg)))
+
+(defun elixir-ts--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)))
+
+(defun elixir-ts--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--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--definition-keywords
+           elixir-ts--test-definition-keywords)))
+
+(defun elixir-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)
+    ("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--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)
+    ;; The HEEx parser 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--treesit-language-at-point)
+
+    ;; Font-lock.
+    (setq-local treesit-font-lock-settings elixir-ts--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--defun-p nil)))
+
+    ;; Indent.
+    (setq-local treesit-simple-indent-rules elixir-ts--indent-rules)
+
+    ;; Navigation
+    (setq-local forward-sexp-function #'elixir-ts--forward-sexp)
+    (setq-local treesit-defun-type-regexp
+                '("call" . elixir-ts--defun-p))
+
+    (setq-local treesit-defun-name-function #'elixir-ts--defun-name)
+
+    ;; Embedded Heex
+    (when (treesit-ready-p 'heex)
+      (setq-local treesit-range-settings elixir-ts--treesit-range-rules)
+
+      (setq-local treesit-simple-indent-rules
+                  (append treesit-simple-indent-rules heex-ts--indent-rules))
+
+      (setq-local treesit-font-lock-settings
+                  (append treesit-font-lock-settings
+                          heex-ts--font-lock-settings))
+
+      (setq-local treesit-simple-indent-rules
+                  (append treesit-simple-indent-rules
+                          heex-ts--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..e2c97a787b5
--- /dev/null
+++ b/test/lisp/progmodes/elixir-ts-mode-resources/indent.erts
@@ -0,0 +1,233 @@
+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: With statement
+=-=
+with one <- one(),
+     two <- two(),
+     {:ok, value} <- get_value(one, two) do
+  {:ok, value}
+else
+  {:error, %{"Message" => message}} ->
+    {:error, message}
+end
+=-=-=
+
+Name: Pipe statements with fn
+
+=-=
+[1, 2]
+|> Enum.map(fn num ->
+  num + 1
+end)
+=-=-=
+
+Name: Pipe statements stab clases
+
+=-=
+[1, 2]
+|> Enum.map(fn
+  x when x < 10 -> x * 2
+  x -> x * 3
+end)
+=-=-=
+
+Name: Pipe statements params
+
+=-=
+[1, 2]
+|> foobar(
+  :one,
+  :two,
+  :three,
+  :four
+)
+=-=-=
+
+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: Spec and method
+
+=-=
+@spec foobar(
+        t,
+        acc,
+        (one, something -> :bar | far),
+        (two -> :bar | far)
+      ) :: any()
+      when chunk: any
+def foobar(enumerable, acc, chunk_fun, after_fun) do
+  {_, {res, acc}} =
+    case after_fun.(acc) do
+      {:one, "one"} ->
+        "one"
+
+      {:two, "two"} ->
+        "two"
+    end
+end
+=-=-=
+
+
+Name: Spec with multi-line result
+
+=-=
+@type result ::
+        {:done, term}
+        | {:two}
+        | {:one}
+
+@type result ::
+        {
+          :done,
+          term
+        }
+        | {:two}
+        | {:one}
+
+@type boo_bar ::
+        (foo :: pos_integer, bar :: pos_integer -> any())
+
+@spec foo_bar(
+        t,
+        (foo -> any),
+        (() -> any) | (foo, foo -> boolean) | module()
+      ) :: any
+      when foo: any
+def foo(one, fun, other)
+=-=-=
+
+
+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