diff options
Diffstat (limited to 'lisp/progmodes/elixir-ts-mode.el')
-rw-r--r-- | lisp/progmodes/elixir-ts-mode.el | 767 |
1 files changed, 767 insertions, 0 deletions
diff --git a/lisp/progmodes/elixir-ts-mode.el b/lisp/progmodes/elixir-ts-mode.el new file mode 100644 index 00000000000..9804152d9ab --- /dev/null +++ b/lisp/progmodes/elixir-ts-mode.el @@ -0,0 +1,767 @@ +;;; elixir-ts-mode.el --- Major mode for Elixir with tree-sitter support -*- lexical-binding: t; -*- + +;; Copyright (C) 2022-2024 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) +(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-p "treesit.c") +(declare-function treesit-node-parent "treesit.c") +(declare-function treesit-node-start "treesit.c") +(declare-function treesit-node-end "treesit.c") +(declare-function treesit-query-compile "treesit.c") +(declare-function treesit-query-capture "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) + +;; 'define-derived-mode' doesn't expose the generated mode hook +;; variable to Custom, because we are not smart enough to provide the +;; ':options' for hook variables. Also, some packages modify hook +;; variables. The below is done because users of this mode explicitly +;; requested the hook to be customizable via Custom. +(defcustom elixir-ts-mode-hook nil + "Hook run after entering `elixir-ts-mode'." + :type 'hook + :options '(eglot-ensure) + :group 'elixir-ts + :version "30.1") + +(defface elixir-ts-comment-doc-identifier + '((t (:inherit font-lock-doc-face))) + "Face used for doc identifiers in Elixir files." + :group 'elixir-ts) + +(defface elixir-ts-comment-doc-attribute + '((t (:inherit font-lock-doc-face))) + "Face used for doc attributes in Elixir files." + :group 'elixir-ts) + +(defface elixir-ts-sigil-name + '((t (:inherit font-lock-string-face))) + "Face used for sigils in Elixir files." + :group 'elixir-ts) + +(defface elixir-ts-atom + '((t (:inherit font-lock-constant-face))) + "Face used for atoms in Elixir files." + :group 'elixir-ts) + +(defface elixir-ts-keyword-key + '((t (:inherit elixir-ts-atom))) + "Face used for keyword keys in Elixir files." + :group 'elixir-ts) + +(defface elixir-ts-attribute + '((t (:inherit font-lock-preprocessor-face))) + "Face used for attributes in Elixir files." + :group 'elixir-ts) + +(defconst elixir-ts--sexp-regexp + (rx bol + (or "call" "stab_clause" "binary_operator" "list" "tuple" "map" "pair" + "sigil" "string" "atom" "alias" "arguments" "identifier" + "boolean" "quoted_content" "bitstring") + 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 + (append elixir-ts--definition-keywords + elixir-ts--test-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 (or (treesit-node-prev-sibling node t) + ;; Don't indent if this is the first node or + ;; if the line is empty. + (save-excursion + (beginning-of-line) + (looking-at-p "[[:blank:]]*$"))) + 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 "^>>$") ,'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 "^bitstring$") 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) + ((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. + ((node-is "^keywords$") parent-bol ,offset) + ((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 ,offset) + ((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) + ((node-is "\"\"\"") parent-bol 0) + ;; Handle quoted_content indentation on the last + ;; line before the closing \"\"\", where it might + ;; see it as no-node outside a HEEx tag. + (no-node (lambda (_n _p _bol) + (treesit-node-start + (treesit-node-parent + (treesit-node-at (point) 'elixir)))) + 0))))) + +(defvar elixir-ts--font-lock-settings + (treesit-font-lock-rules + :language 'elixir + :feature 'elixir-definition + `((call target: (identifier) @target-identifier + (arguments + (call target: (identifier) @font-lock-function-name-face + (arguments))) + (:match ,elixir-ts--definition-keywords-re @target-identifier)) + (call target: (identifier) @target-identifier + (arguments (identifier) @font-lock-function-name-face) + (:match ,elixir-ts--definition-keywords-re @target-identifier)) + (call target: (identifier) @target-identifier + (arguments + (call target: (identifier) @font-lock-function-name-face + (arguments ((identifier)) @font-lock-variable-name-face))) + (:match ,elixir-ts--definition-keywords-re @target-identifier)) + (call target: (identifier) @target-identifier + (arguments + (binary_operator + left: (call target: (identifier) @font-lock-function-name-face))) + (:match ,elixir-ts--definition-keywords-re @target-identifier)) + (call target: (identifier) @target-identifier + (arguments (identifier) @font-lock-function-name-face) + (do_block) + (:match ,elixir-ts--definition-keywords-re @target-identifier)) + (call target: (identifier) @target-identifier + (arguments + (call target: (identifier) @font-lock-function-name-face + (arguments ((identifier)) @font-lock-variable-name-face))) + (do_block) + (:match ,elixir-ts--definition-keywords-re @target-identifier)) + (call target: (identifier) @target-identifier + (arguments + (binary_operator + left: (call target: (identifier) @font-lock-function-name-face + (arguments ((identifier)) @font-lock-variable-name-face)))) + (do_block) + (:match ,elixir-ts--definition-keywords-re @target-identifier)) + (unary_operator + operator: "@" + (call (arguments + (binary_operator + left: (call target: (identifier) @font-lock-function-name-face)))))) + + ;; A function definition like "def _foo" is valid, but we should + ;; not apply the comment-face unless its a non-function identifier, so + ;; the comment matches has to be after the function matches. + :language 'elixir + :feature 'elixir-comment + '((comment) @font-lock-comment-face + ((identifier) @font-lock-comment-face + (:match "^_[a-z]\\|^_$" @font-lock-comment-face))) + + :language 'elixir + :feature 'elixir-variable + `((call target: (identifier) + (arguments + (binary_operator + (call target: (identifier) + (arguments ((identifier) @font-lock-variable-use-face)))))) + (call target: (identifier) + (arguments + (call target: (identifier) + (arguments ((identifier)) @font-lock-variable-use-face)))) + (dot left: (identifier) @font-lock-variable-use-face operator: "." )) + + :language 'elixir + :feature 'elixir-doc + `((unary_operator + operator: "@" @elixir-ts-comment-doc-attribute + operand: (call + target: (identifier) @elixir-ts-comment-doc-identifier + ;; 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 + (keywords) @font-lock-doc-face + ])) + (:match ,elixir-ts--doc-keywords-re + @elixir-ts-comment-doc-identifier)) + (unary_operator + operator: "@" @elixir-ts-comment-doc-attribute + operand: (call + target: (identifier) @elixir-ts-comment-doc-identifier) + (:match ,elixir-ts--doc-keywords-re + @elixir-ts-comment-doc-identifier))) + + :language 'elixir + :feature 'elixir-string + '((interpolation + "#{" @font-lock-escape-face + "}" @font-lock-escape-face) + (string (quoted_content) @font-lock-string-face) + (quoted_keyword (quoted_content) @font-lock-string-face) + (charlist (quoted_content) @font-lock-string-face) + ["\"" "'" "\"\"\""] @font-lock-string-face) + + :language 'elixir + :feature 'elixir-sigil + `((sigil + (sigil_name) @elixir-ts-sigil-name + (quoted_content) @font-lock-string-face + ;; HEEx and Surface templates will handled by + ;; heex-ts-mode if its available. + (:match "^[^HF]$" @elixir-ts-sigil-name)) + @font-lock-string-face + (sigil + (sigil_name) @font-lock-regexp-face + (:match "^[rR]$" @font-lock-regexp-face)) + @font-lock-regexp-face + (sigil + "~" @font-lock-string-face + (sigil_name) @font-lock-string-face + quoted_start: _ @font-lock-string-face + quoted_end: _ @font-lock-string-face)) + + :language 'elixir + :feature 'elixir-operator + `(["!"] @font-lock-negation-char-face + ["%"] @font-lock-bracket-face + ["," ";"] @font-lock-operator-face + ["(" ")" "[" "]" "{" "}" "<<" ">>"] @font-lock-bracket-face) + + :language 'elixir + :feature 'elixir-data-type + '([(atom) (alias)] @font-lock-type-face + (keywords (pair key: (keyword) @elixir-ts-keyword-key)) + [(keyword) (quoted_keyword)] @elixir-ts-atom + [(boolean) (nil)] @elixir-ts-atom + (unary_operator operator: "@" @elixir-ts-attribute + operand: [ + (identifier) @elixir-ts-attribute + (call target: (identifier) + @elixir-ts-attribute) + (boolean) @elixir-ts-attribute + (nil) @elixir-ts-attribute + ]) + (operator_identifier) @font-lock-operator-face) + + :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)) + (binary_operator operator: _ @font-lock-operator-face) + (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))) + + :language 'elixir + :feature 'elixir-function-call + '((call target: (identifier) @font-lock-function-call-face) + (unary_operator operator: "&" @font-lock-operator-face + operand: (binary_operator + left: (identifier) + @font-lock-function-call-face + operator: "/" right: (integer))) + (call + target: (dot right: (identifier) @font-lock-function-call-face)) + (unary_operator operator: "&" @font-lock-variable-use-face + operand: (integer) @font-lock-variable-use-face) + (unary_operator operator: "&" @font-lock-operator-face + operand: (list))) + + :language 'elixir + :feature 'elixir-string-escape + :override t + `((escape_sequence) @font-lock-escape-face) + + :language 'elixir + :feature 'elixir-number + '([(integer) (float)] @font-lock-number-face) + + :language 'elixir + :feature 'elixir-variable + '((binary_operator left: (identifier) @font-lock-variable-use-face) + (binary_operator right: (identifier) @font-lock-variable-use-face) + (arguments ( (identifier) @font-lock-variable-use-face)) + (tuple (identifier) @font-lock-variable-use-face) + (list (identifier) @font-lock-variable-use-face) + (pair value: (identifier) @font-lock-variable-use-face) + (body (identifier) @font-lock-variable-use-face) + (unary_operator operand: (identifier) @font-lock-variable-use-face) + (interpolation (identifier) @font-lock-variable-use-face) + (do_block (identifier) @font-lock-variable-use-face) + (access_call target: (identifier) @font-lock-variable-use-face) + (access_call "[" key: (identifier) @font-lock-variable-use-face "]")) + + :language 'elixir + :feature 'elixir-builtin + :override t + `(((identifier) @font-lock-builtin-face + (:match ,elixir-ts--builtin-keywords-re + @font-lock-builtin-face)))) + + "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))))) + +(defvar heex-ts--sexp-regexp) +(defvar heex-ts--indent-rules) +(defvar heex-ts--font-lock-settings) + +(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 ((node (treesit-node-at point 'elixir))) + (if (and (equal (treesit-node-type node) "quoted_content") + (let ((prev-sibling (treesit-node-prev-sibling node t))) + (and (treesit-node-p prev-sibling) + (string-match-p + (rx bos (or "H" "F") eos) + (treesit-node-text prev-sibling))))) + 'heex + 'elixir))) + +(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))) + +(defvar elixir-ts--syntax-propertize-query + (when (treesit-available-p) + (treesit-query-compile + 'elixir + '(((["\"\"\""] @quoted-text)))))) + +(defun elixir-ts--syntax-propertize (start end) + "Apply syntax text properties between START and END for `elixir-ts-mode'." + (let ((captures + (treesit-query-capture 'elixir elixir-ts--syntax-propertize-query start end))) + (pcase-dolist (`(,name . ,node) captures) + (pcase-exhaustive name + ('quoted-text + (put-text-property (1- (treesit-node-end node)) (treesit-node-end node) + 'syntax-table (string-to-syntax "$"))))))) + +(defun elixir-ts--electric-pair-string-delimiter () + "Insert corresponding multi-line string for `electric-pair-mode'." + (when (and electric-pair-mode + (eq last-command-event ?\") + (let ((count 0)) + (while (eq (char-before (- (point) count)) last-command-event) + (cl-incf count)) + (= count 3)) + (eq (char-after) last-command-event)) + (save-excursion + (insert (make-string 2 last-command-event))) + (save-excursion + (newline 1 t)))) + +;;;###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") + + ;; Electric pair. + (add-hook 'post-self-insert-hook + #'elixir-ts--electric-pair-string-delimiter 'append t) + + (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. + (when (treesit-ready-p 'heex) + ;; Require heex-ts-mode only when we load elixir-ts-mode + ;; so that we don't get a tree-sitter compilation warning for + ;; elixir-ts-mode. + (require 'heex-ts-mode) + (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-doc elixir-definition) + ( elixir-string elixir-keyword elixir-data-type) + ( elixir-sigil elixir-builtin elixir-string-escape) + ( elixir-function-call elixir-variable elixir-operator elixir-number ))) + + + ;; 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-doc elixir-definition + heex-comment heex-keyword heex-doctype ) + ( elixir-string elixir-keyword elixir-data-type + heex-component heex-tag heex-attribute heex-string ) + ( elixir-sigil elixir-builtin elixir-string-escape) + ( elixir-function-call elixir-variable elixir-operator elixir-number )))) + + (treesit-major-mode-setup) + (setq-local syntax-propertize-function #'elixir-ts--syntax-propertize))) + +(derived-mode-add-parents 'elixir-ts-mode '(elixir-mode)) + +(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 |