
Changes and made from conversations from the Elixir slack channel, the github issue https://github.com/wkirschbaum/elixir-ts-mode/issues/35 and bug#67246. * lisp/progmodes/elixir-ts-mode.el (elixir-ts--font-lock-settings): Update features. (elixir-ts-mode): Update treesit-font-lock-feature-list. (elixir-ts-font-comment-doc-identifier-face): Rename to elixir-ts-comment-doc-identifier. (elixir-ts-font-comment-doc-attribute-face): Rename to elixir-ts-comment-doc-attribute. (elixir-ts-font-sigil-name-face): Rename to elixir-ts-sigil-name. (elixir-ts-atom-key-face) (elixir-ts-keyword-key-face) (elixir-ts-attribute-face): Add new custom face.
757 lines
28 KiB
EmacsLisp
757 lines
28 KiB
EmacsLisp
;;; 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)
|
|
(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-function-name
|
|
`((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))
|
|
(: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))
|
|
(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)))
|
|
(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-name-face
|
|
operand: (integer) @font-lock-variable-name-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-name-face)
|
|
(binary_operator right: (identifier) @font-lock-variable-name-face)
|
|
(arguments ( (identifier) @font-lock-variable-name-face))
|
|
(tuple (identifier) @font-lock-variable-name-face)
|
|
(list (identifier) @font-lock-variable-name-face)
|
|
(pair value: (identifier) @font-lock-variable-name-face)
|
|
(body (identifier) @font-lock-variable-name-face)
|
|
(unary_operator operand: (identifier) @font-lock-variable-name-face)
|
|
(interpolation (identifier) @font-lock-variable-name-face)
|
|
(do_block (identifier) @font-lock-variable-name-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-function-name)
|
|
( elixir-string elixir-keyword elixir-data-type)
|
|
( elixir-sigil elixir-variable elixir-builtin
|
|
elixir-string-escape)
|
|
( elixir-function-call 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-function-name
|
|
heex-comment heex-keyword heex-doctype )
|
|
( elixir-string elixir-keyword elixir-data-type
|
|
heex-component heex-tag heex-attribute heex-string )
|
|
( elixir-sigil elixir-variable elixir-builtin
|
|
elixir-string-escape)
|
|
( elixir-function-call elixir-operator elixir-number ))))
|
|
|
|
(treesit-major-mode-setup)
|
|
(setq-local syntax-propertize-function #'elixir-ts--syntax-propertize)))
|
|
|
|
(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
|