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