all messages for Emacs-related lists mirrored at yhetil.org
 help / color / mirror / code / Atom feed
* bug#61996: 30.0.50; Submitting elixir-ts-mode and heex-ts-mode
@ 2023-03-06  7:04 Wilhelm Kirschbaum
  2023-03-06 11:59 ` Eli Zaretskii
  2023-03-06 16:41 ` Dmitry Gutov
  0 siblings, 2 replies; 19+ messages in thread
From: Wilhelm Kirschbaum @ 2023-03-06  7:04 UTC (permalink / raw)
  To: 61996; +Cc: casouri, theo

[-- Attachment #1: Type: text/plain, Size: 470 bytes --]

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.

[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: Add heex-ts-mode --]
[-- Type: text/x-patch, Size: 6937 bytes --]

From 2c31157207986aacf00d5a8405de09011cbb7d14 Mon Sep 17 00:00:00 2001
From: Wilhelm H Kirschbaum <wkirschbaum@gmail.com>
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 <wkirschbaum@gmail.com>
+;; 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


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #3: Add elixir-ts-mode --]
[-- Type: text/x-patch, Size: 24812 bytes --]

From a1e7a754aa5cd6cd69e50913e3412e5c77a6505e Mon Sep 17 00:00:00 2001
From: Wilhelm H Kirschbaum <wkirschbaum@gmail.com>
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 <wkirschbaum@gmail.com>
+;; 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


^ permalink raw reply related	[flat|nested] 19+ messages in thread

* bug#61996: 30.0.50; Submitting elixir-ts-mode and heex-ts-mode
  2023-03-06  7:04 bug#61996: 30.0.50; Submitting elixir-ts-mode and heex-ts-mode Wilhelm Kirschbaum
@ 2023-03-06 11:59 ` Eli Zaretskii
  2023-03-06 17:23   ` Wilhelm Kirschbaum
  2023-03-06 16:41 ` Dmitry Gutov
  1 sibling, 1 reply; 19+ messages in thread
From: Eli Zaretskii @ 2023-03-06 11:59 UTC (permalink / raw)
  To: Wilhelm Kirschbaum; +Cc: 61996, theo, casouri

> Cc: casouri@gmail.com, theo@thornhill.no
> From: Wilhelm Kirschbaum <wkirschbaum@gmail.com>
> Date: Mon, 06 Mar 2023 09:04:13 +0200
> 
> I would like to submit elixir-ts-mode and heex-ts-mode to emacs 
> master.

Thanks.  Please state in the comments to each mode with which grammars
is it compatible, so that users could know from which URL to download
the required grammar libraries.  The heex-ts-mode mentions that, but
elixir-ts-mode doesn't, AFAICT.

> +(defcustom heex-ts-mode-indent-offset 2
> +  "Indentation of Heex statements."
> +  :version "29.1"

I think these modes should go to the master branch, so "30.1" is more
accurate.

> +(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.





^ permalink raw reply	[flat|nested] 19+ messages in thread

* bug#61996: 30.0.50; Submitting elixir-ts-mode and heex-ts-mode
  2023-03-06  7:04 bug#61996: 30.0.50; Submitting elixir-ts-mode and heex-ts-mode Wilhelm Kirschbaum
  2023-03-06 11:59 ` Eli Zaretskii
@ 2023-03-06 16:41 ` Dmitry Gutov
  1 sibling, 0 replies; 19+ messages in thread
From: Dmitry Gutov @ 2023-03-06 16:41 UTC (permalink / raw)
  To: Wilhelm Kirschbaum, 61996; +Cc: casouri, theo

Hi!

On 06/03/2023 09:04, Wilhelm Kirschbaum wrote:
> I would like to submit elixir-ts-mode and heex-ts-mode to emacs master.

It would be great if you could accompany it with some testing suite: 
indentation code is famously prone to regressions.

You can see the examples of such tests for c-ts-mode or ruby-ts-mode 
(they use different approaches).





^ permalink raw reply	[flat|nested] 19+ messages in thread

* bug#61996: 30.0.50; Submitting elixir-ts-mode and heex-ts-mode
  2023-03-06 11:59 ` Eli Zaretskii
@ 2023-03-06 17:23   ` Wilhelm Kirschbaum
  2023-03-06 18:36     ` Eli Zaretskii
  0 siblings, 1 reply; 19+ messages in thread
From: Wilhelm Kirschbaum @ 2023-03-06 17:23 UTC (permalink / raw)
  To: Eli Zaretskii; +Cc: 61996, theo, casouri


Eli Zaretskii <eliz@gnu.org> writes:

>> Cc: casouri@gmail.com, theo@thornhill.no
>> From: Wilhelm Kirschbaum <wkirschbaum@gmail.com>
>> Date: Mon, 06 Mar 2023 09:04:13 +0200
>> 
>> I would like to submit elixir-ts-mode and heex-ts-mode to emacs 
>> master.
>
> Thanks.  Please state in the comments to each mode with which 
> grammars
> is it compatible, so that users could know from which URL to 
> download
> the required grammar libraries.  The heex-ts-mode mentions that, 
> but
> elixir-ts-mode doesn't, AFAICT.
>

Will this make sense in the Commentary section: "The
tree-sitter grammar for Elixir can be downloaded from 
https://github.com/phoenixframework/tree-sitter-heex."

>> +(defcustom heex-ts-mode-indent-offset 2
>> +  "Indentation of Heex statements."
>> +  :version "29.1"
>
> I think these modes should go to the master branch, so "30.1" is 
> more
> accurate.
>

Thanks, will change.

>> +(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.
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? 

I will update the patch with the above changes including some 
tests. 





^ permalink raw reply	[flat|nested] 19+ messages in thread

* bug#61996: 30.0.50; Submitting elixir-ts-mode and heex-ts-mode
  2023-03-06 17:23   ` Wilhelm Kirschbaum
@ 2023-03-06 18:36     ` Eli Zaretskii
  2023-03-06 19:24       ` Wilhelm Kirschbaum
  0 siblings, 1 reply; 19+ messages in thread
From: Eli Zaretskii @ 2023-03-06 18:36 UTC (permalink / raw)
  To: Wilhelm Kirschbaum; +Cc: 61996, theo, casouri

> From: Wilhelm Kirschbaum <wkirschbaum@gmail.com>
> Cc: 61996@debbugs.gnu.org, casouri@gmail.com, theo@thornhill.no
> Date: Mon, 06 Mar 2023 19:23:39 +0200
> 
> 
> Eli Zaretskii <eliz@gnu.org> writes:
> 
> > Thanks.  Please state in the comments to each mode with which
> > grammars is it compatible, so that users could know from which URL
> > to download the required grammar libraries.  The heex-ts-mode
> > mentions that, but elixir-ts-mode doesn't, AFAICT.
> 
> Will this make sense in the Commentary section: "The
> tree-sitter grammar for Elixir can be downloaded from 
> https://github.com/phoenixframework/tree-sitter-heex."

Yes, that's good enough.  But please change the wording to say that
this is the grammar with which the package was tested, not just that
it "can be downloaded" from that place.  That way, users will know
that if they use a different grammar for the same language, they might
be on their own.

> >> +(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?

> 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.





^ permalink raw reply	[flat|nested] 19+ messages in thread

* bug#61996: 30.0.50; Submitting elixir-ts-mode and heex-ts-mode
  2023-03-06 18:36     ` Eli Zaretskii
@ 2023-03-06 19:24       ` Wilhelm Kirschbaum
  2023-03-06 20:14         ` Eli Zaretskii
  2023-03-11  9:16         ` Eli Zaretskii
  0 siblings, 2 replies; 19+ messages in thread
From: Wilhelm Kirschbaum @ 2023-03-06 19:24 UTC (permalink / raw)
  To: Eli Zaretskii; +Cc: 61996, theo, casouri

[-- Attachment #1: Type: text/plain, Size: 2062 bytes --]


Eli Zaretskii <eliz@gnu.org> 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?


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: Add heex-ts-mode --]
[-- Type: text/x-patch, Size: 8600 bytes --]

From 88c941067da0e34e1e9ababeb813ba51378ae2cc Mon Sep 17 00:00:00 2001
From: Wilhelm H Kirschbaum <wkirschbaum@gmail.com>
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 <wkirschbaum@gmail.com>
+;; 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


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #3: Add elixir-ts-mode --]
[-- Type: text/x-patch, Size: 28229 bytes --]

From d13c34ed951e3e6fa473cd1bc2e955e20455022b Mon Sep 17 00:00:00 2001
From: Wilhelm H Kirschbaum <wkirschbaum@gmail.com>
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 <wkirschbaum@gmail.com>
+;; 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


^ permalink raw reply related	[flat|nested] 19+ messages in thread

* bug#61996: 30.0.50; Submitting elixir-ts-mode and heex-ts-mode
  2023-03-06 19:24       ` Wilhelm Kirschbaum
@ 2023-03-06 20:14         ` Eli Zaretskii
  2023-03-11  9:16         ` Eli Zaretskii
  1 sibling, 0 replies; 19+ messages in thread
From: Eli Zaretskii @ 2023-03-06 20:14 UTC (permalink / raw)
  To: Wilhelm Kirschbaum; +Cc: 61996, theo, casouri

> From: Wilhelm Kirschbaum <wkirschbaum@gmail.com>
> Cc: 61996@debbugs.gnu.org, casouri@gmail.com, theo@thornhill.no
> Date: Mon, 06 Mar 2023 21:24:11 +0200
> 
> Attached are the updated patches. Is this the right format, or
> should I add them inline rather?

It's okay either way.





^ permalink raw reply	[flat|nested] 19+ messages in thread

* bug#61996: 30.0.50; Submitting elixir-ts-mode and heex-ts-mode
  2023-03-06 19:24       ` Wilhelm Kirschbaum
  2023-03-06 20:14         ` Eli Zaretskii
@ 2023-03-11  9:16         ` Eli Zaretskii
  2023-03-11 14:16           ` Dmitry Gutov
  2023-03-11 18:01           ` Wilhelm Kirschbaum
  1 sibling, 2 replies; 19+ messages in thread
From: Eli Zaretskii @ 2023-03-11  9:16 UTC (permalink / raw)
  To: Wilhelm Kirschbaum; +Cc: 61996, theo, casouri

> From: Wilhelm Kirschbaum <wkirschbaum@gmail.com>
> Cc: 61996@debbugs.gnu.org, casouri@gmail.com, theo@thornhill.no
> Date: Mon, 06 Mar 2023 21:24:11 +0200
> 
> 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.

Thanks, a few minor comments below.

> >From 88c941067da0e34e1e9ababeb813ba51378ae2cc Mon Sep 17 00:00:00 2001
> From: Wilhelm H Kirschbaum <wkirschbaum@gmail.com>
> 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

Please accompany the changes with a commit log message according to
our conventions (see CONTRIBUTE for the conventions; search for
"ChangeLog" there).  In this case, just "New file" log should be
sufficient for the new files you add.

> +(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")

AFAICS, the code uses more functions from treesit.c; please add
declare-function forms for all of them , to avoid compilation warnings
n systems where Emacs was built without tree-sitter.

> +(defun heex-ts-mode--forward-sexp (&optional arg)
> +  (interactive "^p")

Why is a command an internal function?  That is unusual, as commands
are by definition public.  It looks like you thought the double-hyphen
"--" notation is a simple delimiter between the package-name part of
the symbol name and the rest?  If so, you were mistaken: the
double-hyphen means this is an internal function/variable.  Please
review all your symbol names in this patch and rename as appropriate.

Btw, there's no need to have the prefix be the full name of the
package, as in "elixir-ts-mode-".  You could use "elixir-ts-" instead.

> +;;;###autoload
> +(define-derived-mode heex-ts-mode html-mode "Heex"

html-mode? not html-ts-mode?

> >From d13c34ed951e3e6fa473cd1bc2e955e20455022b Mon Sep 17 00:00:00 2001
> From: Wilhelm H Kirschbaum <wkirschbaum@gmail.com>
> Date: Mon, 6 Mar 2023 21:18:35 +0200
> 
> ---
>  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

Likewise here: please add a commit log message describing the changes.

> +(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")

Please verify that you have declare-function for all the functions
from treesit.c this package uses, and only for those.

> +(defgroup elixir-ts nil
> +  "Major mode for editing Ruby code."
                             ^^^^
"Ruby"?

> +;; used to distinguish from comment-face in query match

Comments should be complete sentences: start with a capital letter and
end with a period (here and elsewhere in the patches).

> +(defface elixir-ts-font-comment-doc-identifier-face
> +  '((t (:inherit font-lock-doc-face)))
> +  "For use with @comment.doc tag.")

This doc string is too terse.  Imagine someone looking at it in a long
list of symbols, not necessarily all of them faces.  So something
like this is better:

  Face used for @comment.doc tags in Elixir files.

Likewise for other faces in the patch.

> +    (modify-syntax-entry ?@ "'" table)
> +    table)
> +  "Syntax table for `elixir-ts-mode.")
                                      ^
The closing ' quote is missing there.





^ permalink raw reply	[flat|nested] 19+ messages in thread

* bug#61996: 30.0.50; Submitting elixir-ts-mode and heex-ts-mode
  2023-03-11  9:16         ` Eli Zaretskii
@ 2023-03-11 14:16           ` Dmitry Gutov
  2023-03-11 18:27             ` Wilhelm Kirschbaum
  2023-03-11 18:01           ` Wilhelm Kirschbaum
  1 sibling, 1 reply; 19+ messages in thread
From: Dmitry Gutov @ 2023-03-11 14:16 UTC (permalink / raw)
  To: Eli Zaretskii, Wilhelm Kirschbaum; +Cc: 61996, theo, casouri

On 11/03/2023 11:16, Eli Zaretskii wrote:
>> +(defun heex-ts-mode--forward-sexp (&optional arg)
>> +  (interactive "^p")
> Why is a command an internal function?  That is unusual, as commands
> are by definition public.  It looks like you thought the double-hyphen
> "--" notation is a simple delimiter between the package-name part of
> the symbol name and the rest?  If so, you were mistaken: the
> double-hyphen means this is an internal function/variable.  Please
> review all your symbol names in this patch and rename as appropriate.

I'm guessing it was made interactive for debugging purposes.

But even that doesn't seem necessary: calling 'forward-sexp' through its 
regular binding will invoke forward-sexp-function basically right away.

(treesit-forward-sexp doesn't need to be interactive either.)





^ permalink raw reply	[flat|nested] 19+ messages in thread

* bug#61996: 30.0.50; Submitting elixir-ts-mode and heex-ts-mode
  2023-03-11  9:16         ` Eli Zaretskii
  2023-03-11 14:16           ` Dmitry Gutov
@ 2023-03-11 18:01           ` Wilhelm Kirschbaum
  2023-03-12  9:00             ` Eli Zaretskii
  1 sibling, 1 reply; 19+ messages in thread
From: Wilhelm Kirschbaum @ 2023-03-11 18:01 UTC (permalink / raw)
  To: Eli Zaretskii; +Cc: 61996, theo, casouri

[-- Attachment #1: Type: text/plain, Size: 5117 bytes --]


Eli Zaretskii <eliz@gnu.org> writes:

>> >From 88c941067da0e34e1e9ababeb813ba51378ae2cc Mon Sep 17 
>> >00:00:00 2001
>> From: Wilhelm H Kirschbaum <wkirschbaum@gmail.com>
>> 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
>
> Please accompany the changes with a commit log message according 
> to
> our conventions (see CONTRIBUTE for the conventions; search for
> "ChangeLog" there).  In this case, just "New file" log should be
> sufficient for the new files you add.
>
Thanks, was not aware of it. I hope it is correct in the new 
patches. 

>> +(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")
>
> AFAICS, the code uses more functions from treesit.c; please add
> declare-function forms for all of them , to avoid compilation 
> warnings
> n systems where Emacs was built without tree-sitter.
>
I made some changes and checked on a non-treesit build and
see no more warnings. 

>> +(defun heex-ts-mode--forward-sexp (&optional arg)
>> +  (interactive "^p")
>
> Why is a command an internal function?  That is unusual, as 
> commands
> are by definition public.  It looks like you thought the 
> double-hyphen
> "--" notation is a simple delimiter between the package-name 
> part of
> the symbol name and the rest?  If so, you were mistaken: the
> double-hyphen means this is an internal function/variable. 
> Please
> review all your symbol names in this patch and rename as 
> appropriate.
>
> Btw, there's no need to have the prefix be the full name of the
> package, as in "elixir-ts-mode-".  You could use "elixir-ts-" 
> instead.
>
This should be internal, I removed the interactive. 

>> +;;;###autoload
>> +(define-derived-mode heex-ts-mode html-mode "Heex"
>
> html-mode? not html-ts-mode?
>

I don't see the advantage to use html-ts-mode over html-mode at 
the
moment, but can have another look if there is a specific reason to 
do so.

>> >From d13c34ed951e3e6fa473cd1bc2e955e20455022b Mon Sep 17 
>> >00:00:00 2001
>> From: Wilhelm H Kirschbaum <wkirschbaum@gmail.com>
>> Date: Mon, 6 Mar 2023 21:18:35 +0200
>> 
>> ---
>>  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
>
> Likewise here: please add a commit log message describing the 
> changes.
>

>> +(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")
>
> Please verify that you have declare-function for all the 
> functions
> from treesit.c this package uses, and only for those.
>
I think this is fixed.

>> +(defgroup elixir-ts nil
>> +  "Major mode for editing Ruby code."
>                              ^^^^
> "Ruby"?
>

Copy paste error from ruby-ts-mode when trying to follow 
conventions. 

>> +;; used to distinguish from comment-face in query match
>
> Comments should be complete sentences: start with a capital 
> letter and
> end with a period (here and elsewhere in the patches).
>
>> +(defface elixir-ts-font-comment-doc-identifier-face
>> +  '((t (:inherit font-lock-doc-face)))
>> +  "For use with @comment.doc tag.")
>
> This doc string is too terse.  Imagine someone looking at it in 
> a long
> list of symbols, not necessarily all of them faces.  So 
> something
> like this is better:
>
>   Face used for @comment.doc tags in Elixir files.
>
> Likewise for other faces in the patch.
>
>> +    (modify-syntax-entry ?@ "'" table)
>> +    table)
>> +  "Syntax table for `elixir-ts-mode.")
>                                       ^
> The closing ' quote is missing there.

The new patches should hopefully cover all of the above issues. 
Thanks
for the patience.


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: Add heex-ts-mode --]
[-- Type: text/x-patch, Size: 10549 bytes --]

From a8adf5f1213e8e8629aecee09dcd24cb55f06e51 Mon Sep 17 00:00:00 2001
From: Wilhelm H Kirschbaum <wkirschbaum@gmail.com>
Date: Sat, 11 Mar 2023 19:42: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..6096dbabf30
--- /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 <wkirschbaum@gmail.com>
+;; 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


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #3: Add elixir-ts-mode --]
[-- Type: text/x-patch, Size: 30597 bytes --]

From fa69480e61d779c251b44b568908ef5a50e47d48 Mon Sep 17 00:00:00 2001
From: Wilhelm H Kirschbaum <wkirschbaum@gmail.com>
Date: Sat, 11 Mar 2023 19:49:20 +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              | 625 ++++++++++++++++++
 .../elixir-ts-mode-resources/indent.erts      | 147 ++++
 test/lisp/progmodes/elixir-ts-mode-tests.el   |  31 +
 7 files changed, 812 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..d6caa57b4f2
--- /dev/null
+++ b/lisp/progmodes/elixir-ts-mode.el
@@ -0,0 +1,625 @@
+;;; 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 <wkirschbaum@gmail.com>
+;; 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$")
+        ;; 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--call-parent-start parent))
+                  ((treesit-node-eq node first-child)
+                   (elixir-ts--call-parent-start parent))
+                  (t (elixir-ts--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.
+       ((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
+              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)))))
+
+(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..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


^ permalink raw reply related	[flat|nested] 19+ messages in thread

* bug#61996: 30.0.50; Submitting elixir-ts-mode and heex-ts-mode
  2023-03-11 14:16           ` Dmitry Gutov
@ 2023-03-11 18:27             ` Wilhelm Kirschbaum
  0 siblings, 0 replies; 19+ messages in thread
From: Wilhelm Kirschbaum @ 2023-03-11 18:27 UTC (permalink / raw)
  To: Dmitry Gutov; +Cc: casouri, Eli Zaretskii, theo, 61996


Dmitry Gutov <dgutov@yandex.ru> writes:

> On 11/03/2023 11:16, Eli Zaretskii wrote:
>>> +(defun heex-ts-mode--forward-sexp (&optional arg)
>>> +  (interactive "^p")
>> Why is a command an internal function?  That is unusual, as 
>> commands
>> are by definition public.  It looks like you thought the 
>> double-hyphen
>> "--" notation is a simple delimiter between the package-name 
>> part of
>> the symbol name and the rest?  If so, you were mistaken: the
>> double-hyphen means this is an internal function/variable. 
>> Please
>> review all your symbol names in this patch and rename as 
>> appropriate.
>
> I'm guessing it was made interactive for debugging purposes.
>
> But even that doesn't seem necessary: calling 'forward-sexp' 
> through
> its regular binding will invoke forward-sexp-function basically 
> right
> away.
>
> (treesit-forward-sexp doesn't need to be interactive either.)

This was a mistake and still learning some basic conventions. I 
don't see a
reason to call this function interactively.





^ permalink raw reply	[flat|nested] 19+ messages in thread

* bug#61996: 30.0.50; Submitting elixir-ts-mode and heex-ts-mode
  2023-03-11 18:01           ` Wilhelm Kirschbaum
@ 2023-03-12  9:00             ` Eli Zaretskii
  2023-03-12  9:54               ` Wilhelm Kirschbaum
  0 siblings, 1 reply; 19+ messages in thread
From: Eli Zaretskii @ 2023-03-12  9:00 UTC (permalink / raw)
  To: Wilhelm Kirschbaum; +Cc: 61996, theo, casouri

> From: Wilhelm Kirschbaum <wkirschbaum@gmail.com>
> Cc: 61996@debbugs.gnu.org, casouri@gmail.com, theo@thornhill.no
> Date: Sat, 11 Mar 2023 20:01:32 +0200
> 
> > Please accompany the changes with a commit log message according
> > to our conventions (see CONTRIBUTE for the conventions; search for
> > "ChangeLog" there).  In this case, just "New file" log should be
> > sufficient for the new files you add.
> >
> Thanks, was not aware of it. I hope it is correct in the new 
> patches. 

Yes, it is.  Thanks.

> The new patches should hopefully cover all of the above issues. 

Almost there.  Byte compiler shows warnings, which I think are real
problems in the code:

  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'






^ permalink raw reply	[flat|nested] 19+ messages in thread

* bug#61996: 30.0.50; Submitting elixir-ts-mode and heex-ts-mode
  2023-03-12  9:00             ` Eli Zaretskii
@ 2023-03-12  9:54               ` Wilhelm Kirschbaum
  2023-03-12 11:37                 ` Eli Zaretskii
  0 siblings, 1 reply; 19+ messages in thread
From: Wilhelm Kirschbaum @ 2023-03-12  9:54 UTC (permalink / raw)
  To: Eli Zaretskii; +Cc: 61996, theo, casouri

[-- Attachment #1: Type: text/plain, Size: 1211 bytes --]


Eli Zaretskii <eliz@gnu.org> writes:

>> From: Wilhelm Kirschbaum <wkirschbaum@gmail.com>
>> Cc: 61996@debbugs.gnu.org, casouri@gmail.com, theo@thornhill.no
>> Date: Sat, 11 Mar 2023 20:01:32 +0200
>> 
>> > Please accompany the changes with a commit log message 
>> > according
>> > to our conventions (see CONTRIBUTE for the conventions; 
>> > search for
>> > "ChangeLog" there).  In this case, just "New file" log should 
>> > be
>> > sufficient for the new files you add.
>> >
>> Thanks, was not aware of it. I hope it is correct in the new 
>> patches. 
>
> Yes, it is.  Thanks.
>
>> The new patches should hopefully cover all of the above issues. 
>
> Almost there.  Byte compiler shows warnings, which I think are 
> real
> problems in the code:
>
>   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.


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: Add heex-ts-mode --]
[-- Type: text/x-patch, Size: 10234 bytes --]

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

---
 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 <wkirschbaum@gmail.com>
+;; 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


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #3: Add elixir-ts-mode --]
[-- Type: text/x-patch, Size: 31585 bytes --]

From 6b7d72facfb397c8cfdc165829d79cf75ab68098 Mon Sep 17 00:00:00 2001
From: Wilhelm H Kirschbaum <wkirschbaum@gmail.com>
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 <wkirschbaum@gmail.com>
+;; 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


^ permalink raw reply related	[flat|nested] 19+ messages in thread

* bug#61996: 30.0.50; Submitting elixir-ts-mode and heex-ts-mode
  2023-03-12  9:54               ` Wilhelm Kirschbaum
@ 2023-03-12 11:37                 ` Eli Zaretskii
  2023-03-12 12:23                   ` Wilhelm Kirschbaum
  2023-03-12 15:14                   ` Wilhelm Kirschbaum
  0 siblings, 2 replies; 19+ messages in thread
From: Eli Zaretskii @ 2023-03-12 11:37 UTC (permalink / raw)
  To: Wilhelm Kirschbaum; +Cc: 61996, theo, casouri

> From: Wilhelm Kirschbaum <wkirschbaum@gmail.com>
> Cc: 61996@debbugs.gnu.org, casouri@gmail.com, theo@thornhill.no
> Date: Sun, 12 Mar 2023 11:54:33 +0200
> 
> Eli Zaretskii <eliz@gnu.org> 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.





^ permalink raw reply	[flat|nested] 19+ messages in thread

* bug#61996: 30.0.50; Submitting elixir-ts-mode and heex-ts-mode
  2023-03-12 11:37                 ` Eli Zaretskii
@ 2023-03-12 12:23                   ` Wilhelm Kirschbaum
  2023-03-12 12:32                     ` Wilhelm Kirschbaum
  2023-03-12 15:14                   ` Wilhelm Kirschbaum
  1 sibling, 1 reply; 19+ messages in thread
From: Wilhelm Kirschbaum @ 2023-03-12 12:23 UTC (permalink / raw)
  To: Eli Zaretskii; +Cc: 61996, theo, casouri

[-- Attachment #1: Type: text/plain, Size: 1083 bytes --]


Eli Zaretskii <eliz@gnu.org> writes:

>> From: Wilhelm Kirschbaum <wkirschbaum@gmail.com>
>> Cc: 61996@debbugs.gnu.org, casouri@gmail.com, theo@thornhill.no
>> Date: Sun, 12 Mar 2023 11:54:33 +0200
>> 
>> Eli Zaretskii <eliz@gnu.org> 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.


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: Add heex-ts-mode --]
[-- Type: text/x-patch, Size: 10551 bytes --]

From 2b4cee1de0b3a038f6c78da12ddddb338553878b Mon Sep 17 00:00:00 2001
From: Wilhelm H Kirschbaum <wkirschbaum@gmail.com>
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 <wkirschbaum@gmail.com>
+;; 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


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #3: Add elixir-ts-mode --]
[-- Type: text/x-patch, Size: 31585 bytes --]

From 56f4a551bc65c9a64fdcf7fb7c2e0731f724ad99 Mon Sep 17 00:00:00 2001
From: Wilhelm H Kirschbaum <wkirschbaum@gmail.com>
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 <wkirschbaum@gmail.com>
+;; 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


^ permalink raw reply related	[flat|nested] 19+ messages in thread

* bug#61996: 30.0.50; Submitting elixir-ts-mode and heex-ts-mode
  2023-03-12 12:23                   ` Wilhelm Kirschbaum
@ 2023-03-12 12:32                     ` Wilhelm Kirschbaum
  0 siblings, 0 replies; 19+ messages in thread
From: Wilhelm Kirschbaum @ 2023-03-12 12:32 UTC (permalink / raw)
  To: Eli Zaretskii; +Cc: 61996, theo, casouri


Wilhelm Kirschbaum <wkirschbaum@gmail.com> writes:

> Eli Zaretskii <eliz@gnu.org> writes:
>
>>> From: Wilhelm Kirschbaum <wkirschbaum@gmail.com>
>>> Cc: 61996@debbugs.gnu.org, casouri@gmail.com, 
>>> theo@thornhill.no
>>> Date: Sun, 12 Mar 2023 11:54:33 +0200
>>> Eli Zaretskii <eliz@gnu.org> 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.
>
> [2. Add heex-ts-mode --- text/x-patch; 
> 0001-Add-heex-ts-mode-Bug-61996.patch]...
>
> [3. Add elixir-ts-mode --- text/x-patch; 
> 0002-Add-elixir-ts-mode-Bug-61996.patch]...

Please ignore the previous patch, need to fix some tests. 





^ permalink raw reply	[flat|nested] 19+ messages in thread

* bug#61996: 30.0.50; Submitting elixir-ts-mode and heex-ts-mode
  2023-03-12 11:37                 ` Eli Zaretskii
  2023-03-12 12:23                   ` Wilhelm Kirschbaum
@ 2023-03-12 15:14                   ` Wilhelm Kirschbaum
  2023-03-12 15:46                     ` Eli Zaretskii
  1 sibling, 1 reply; 19+ messages in thread
From: Wilhelm Kirschbaum @ 2023-03-12 15:14 UTC (permalink / raw)
  To: Eli Zaretskii; +Cc: 61996, theo, casouri

[-- Attachment #1: Type: text/plain, Size: 1569 bytes --]


Eli Zaretskii <eliz@gnu.org> writes:

>> From: Wilhelm Kirschbaum <wkirschbaum@gmail.com>
>> Cc: 61996@debbugs.gnu.org, casouri@gmail.com, theo@thornhill.no
>> Date: Sun, 12 Mar 2023 11:54:33 +0200
>> 
>> Eli Zaretskii <eliz@gnu.org> 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.

Attached are the updated patches with added test cases and 
indentation
rule enhancements.

I still see this warning on a non-treesitter build:

In elixir-ts--forward-sexp:
elixir-ts-mode.el:490:8: Warning: reference to free variable
    ‘heex-ts--sexp-regexp’

But not sure why and how to fix it, because defcons 
heex-ts--sexp-regexp and
(require 'heex-ts-mode) is called.

Random concern: how will backwards compatibility work when the 
grammars
get updated?

It might just be better to add both modes in one patch if more 
changes
are required perhaps? 


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: Add heex-ts-mode --]
[-- Type: text/x-patch, Size: 10551 bytes --]

From 7544e2fe4192b7143d723bc811eeb127bbc1e1e3 Mon Sep 17 00:00:00 2001
From: Wilhelm H Kirschbaum <wkirschbaum@gmail.com>
Date: Sun, 12 Mar 2023 17:08:50 +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 <wkirschbaum@gmail.com>
+;; 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


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #3: Add elixir-ts-mode --]
[-- Type: text/x-patch, Size: 33408 bytes --]

From 6eb506f39bef5e210573e2b7dae2006a13098e2f Mon Sep 17 00:00:00 2001
From: Wilhelm H Kirschbaum <wkirschbaum@gmail.com>
Date: Sun, 12 Mar 2023 17:10:43 +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              | 634 ++++++++++++++++++
 .../elixir-ts-mode-resources/indent.erts      | 308 +++++++++
 test/lisp/progmodes/elixir-ts-mode-tests.el   |  31 +
 7 files changed, 982 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..8adf647b081
--- /dev/null
+++ b/lisp/progmodes/elixir-ts-mode.el
@@ -0,0 +1,634 @@
+;;; 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 <wkirschbaum@gmail.com>
+;; 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")
+(declare-function treesit-node-prev-sibling "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'.")
+
+(defun elixir-ts--argument-indent-offset (node _parent &rest _)
+  "Return the argument offset position for NODE."
+  (if (treesit-node-prev-sibling node t) 0 elixir-ts-indent-offset))
+
+(defun elixir-ts--argument-indent-anchor (node parent &rest _)
+  "Return the argument anchor position for NODE and PARENT."
+  (let ((first-sibling (treesit-node-child parent 0 t)))
+    (if (and first-sibling (not (treesit-node-eq first-sibling node)))
+        (treesit-node-start first-sibling)
+      (elixir-ts--parent-expression-start node parent))))
+
+(defun elixir-ts--parent-expression-start (_node parent &rest _)
+  "Return the indentation expression start for NODE and PARENT."
+  ;; If the parent is the first expression on the line return the
+  ;; parent start of node position, otherwise use the parent call
+  ;; start if available.
+  (if (eq (treesit-node-start parent)
+          (save-excursion
+            (goto-char (treesit-node-start parent))
+            (back-to-indentation)
+            (point)))
+      (treesit-node-start parent)
+    (let ((expr-parent
+           (treesit-parent-until
+            parent
+            (lambda (n)
+              (member (treesit-node-type n)
+                      '("call" "binary_operator" "keywords" "list"))))))
+      (save-excursion
+        (goto-char (treesit-node-start expr-parent))
+        (back-to-indentation)
+        (if (looking-at "|>")
+            (point)
+          (treesit-node-start expr-parent))))))
+
+(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 "^]$") ,'elixir-ts--parent-expression-start 0)
+       ((node-is "^}$") ,'elixir-ts--parent-expression-start 0)
+       ((node-is "^)$") ,'elixir-ts--parent-expression-start 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 "^access_call$")
+        ,'elixir-ts--argument-indent-anchor
+        ,'elixir-ts--argument-indent-offset)
+       ((parent-is "^tuple$")
+        ,'elixir-ts--argument-indent-anchor
+        ,'elixir-ts--argument-indent-offset)
+       ((parent-is "^list$")
+        ,'elixir-ts--argument-indent-anchor
+        ,'elixir-ts--argument-indent-offset)
+       ((parent-is "^pair$") parent ,offset)
+       ((parent-is "^map_content$") parent-bol 0)
+       ((parent-is "^map$") ,'elixir-ts--parent-expression-start ,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$")
+        ,'elixir-ts--argument-indent-anchor
+        ,'elixir-ts--argument-indent-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 0)
+       ((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--parent-expression-start node 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$") standalone-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--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..748455cc3f2
--- /dev/null
+++ b/test/lisp/progmodes/elixir-ts-mode-resources/indent.erts
@@ -0,0 +1,308 @@
+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: Map in function def
+
+=-=
+def foobar() do
+  %{
+    one: "one",
+    two: "two",
+    three: "three",
+    four: "four"
+  }
+end
+=-=-=
+
+Name: Map in tuple
+
+=-=
+def foo() do
+  {:ok,
+   %{
+     state
+     | extra_arguments: extra_arguments,
+       max_children: max_children,
+       max_restarts: max_restarts,
+       max_seconds: max_seconds,
+       strategy: strategy
+   }}
+end
+=-=-=
+
+Name: Nested maps
+
+=-=
+%{
+  foo: "bar",
+  bar: %{
+    foo: "bar"
+  }
+}
+
+def foo() do
+  %{
+    foo: "bar",
+    bar: %{
+      foo: "bar"
+    }
+  }
+end
+=-=-=
+
+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: Parameter maps
+
+=-=
+def something(%{
+      one: :one,
+      two: :two
+    }) do
+  {:ok, "done"}
+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: 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"
+=-=-=
+
+Name: Tuple with same line first node
+
+=-=
+{:one,
+ :two}
+
+{:ok,
+ fn one ->
+   one
+   |> String.upcase(one)
+ end}
+=-=-=
+
+Name: Long tuple
+
+=-=
+{"January", "February", "March", "April", "May", "June", "July", "August", "September",
+ "October", "November", "December"}
+=-=-=
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


^ permalink raw reply related	[flat|nested] 19+ messages in thread

* bug#61996: 30.0.50; Submitting elixir-ts-mode and heex-ts-mode
  2023-03-12 15:14                   ` Wilhelm Kirschbaum
@ 2023-03-12 15:46                     ` Eli Zaretskii
  2023-03-12 18:02                       ` Wilhelm Kirschbaum
  0 siblings, 1 reply; 19+ messages in thread
From: Eli Zaretskii @ 2023-03-12 15:46 UTC (permalink / raw)
  To: Wilhelm Kirschbaum; +Cc: 61996-done, theo, casouri

> From: Wilhelm Kirschbaum <wkirschbaum@gmail.com>
> Cc: 61996@debbugs.gnu.org, casouri@gmail.com, theo@thornhill.no
> Date: Sun, 12 Mar 2023 17:14:03 +0200
> 
> Attached are the updated patches with added test cases and
> indentation rule enhancements.

Thanks, installed on master.

> I still see this warning on a non-treesitter build:
> 
> In elixir-ts--forward-sexp:
> elixir-ts-mode.el:490:8: Warning: reference to free variable
>     ‘heex-ts--sexp-regexp’

Doesn't happen here, so I think we are good.

> Random concern: how will backwards compatibility work when the
> grammars get updated?

Let's discuss this when such problems actually happen.  The answer
depends on what kind of incompatibilities are introduced by changes in
the grammars.

> It might just be better to add both modes in one patch if more
> changes are required perhaps?

I'm not sure I understand the question.

In general, we like each commit to be as self-contained and
independent of the others as possible.  Not sure if this answers your
question.

Thanks, I'm closing this bug.





^ permalink raw reply	[flat|nested] 19+ messages in thread

* bug#61996: 30.0.50; Submitting elixir-ts-mode and heex-ts-mode
  2023-03-12 15:46                     ` Eli Zaretskii
@ 2023-03-12 18:02                       ` Wilhelm Kirschbaum
  0 siblings, 0 replies; 19+ messages in thread
From: Wilhelm Kirschbaum @ 2023-03-12 18:02 UTC (permalink / raw)
  To: Eli Zaretskii; +Cc: 61996-done, theo, casouri


Eli Zaretskii <eliz@gnu.org> writes:

>> It might just be better to add both modes in one patch if more
>> changes are required perhaps?
>
> I'm not sure I understand the question.
>
> In general, we like each commit to be as self-contained and
> independent of the others as possible.  Not sure if this answers 
> your
> question.
>
> Thanks, I'm closing this bug.

Thanks for the help on this.





^ permalink raw reply	[flat|nested] 19+ messages in thread

end of thread, other threads:[~2023-03-12 18:02 UTC | newest]

Thread overview: 19+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2023-03-06  7:04 bug#61996: 30.0.50; Submitting elixir-ts-mode and heex-ts-mode Wilhelm Kirschbaum
2023-03-06 11:59 ` Eli Zaretskii
2023-03-06 17:23   ` Wilhelm Kirschbaum
2023-03-06 18:36     ` Eli Zaretskii
2023-03-06 19:24       ` Wilhelm Kirschbaum
2023-03-06 20:14         ` Eli Zaretskii
2023-03-11  9:16         ` Eli Zaretskii
2023-03-11 14:16           ` Dmitry Gutov
2023-03-11 18:27             ` Wilhelm Kirschbaum
2023-03-11 18:01           ` Wilhelm Kirschbaum
2023-03-12  9:00             ` Eli Zaretskii
2023-03-12  9:54               ` Wilhelm Kirschbaum
2023-03-12 11:37                 ` Eli Zaretskii
2023-03-12 12:23                   ` Wilhelm Kirschbaum
2023-03-12 12:32                     ` Wilhelm Kirschbaum
2023-03-12 15:14                   ` Wilhelm Kirschbaum
2023-03-12 15:46                     ` Eli Zaretskii
2023-03-12 18:02                       ` Wilhelm Kirschbaum
2023-03-06 16:41 ` Dmitry Gutov

Code repositories for project(s) associated with this external index

	https://git.savannah.gnu.org/cgit/emacs.git
	https://git.savannah.gnu.org/cgit/emacs/org-mode.git

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.