aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorWilhelm H Kirschbaum2023-03-12 17:10:43 +0200
committerEli Zaretskii2023-03-12 17:41:44 +0200
commitd965d030879d9ca4ef5098cb4e2e7c56128b904b (patch)
tree8ab985a3946eac5e52417f7bb32b012db3db0af5
parent802e64922bcee40c8362b9627aa33a0de0c068d7 (diff)
downloademacs-d965d030879d9ca4ef5098cb4e2e7c56128b904b.tar.gz
emacs-d965d030879d9ca4ef5098cb4e2e7c56128b904b.zip
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.
-rwxr-xr-xadmin/notes/tree-sitter/build-module/batch.sh1
-rwxr-xr-xadmin/notes/tree-sitter/build-module/build.sh3
-rw-r--r--etc/NEWS4
-rw-r--r--lisp/progmodes/eglot.el2
-rw-r--r--lisp/progmodes/elixir-ts-mode.el634
-rw-r--r--test/lisp/progmodes/elixir-ts-mode-resources/indent.erts308
-rw-r--r--test/lisp/progmodes/elixir-ts-mode-tests.el31
7 files changed, 982 insertions, 1 deletions
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=(
8 'css' 8 'css'
9 'c-sharp' 9 'c-sharp'
10 'dockerfile' 10 'dockerfile'
11 'elixir'
11 'go' 12 'go'
12 'go-mod' 13 'go-mod'
13 'heex' 14 '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 @@ case "${lang}" in
31 "cmake") 31 "cmake")
32 org="uyha" 32 org="uyha"
33 ;; 33 ;;
34 "elixir")
35 org="elixir-lang"
36 ;;
34 "go-mod") 37 "go-mod")
35 # The parser is called "gomod". 38 # The parser is called "gomod".
36 lang="gomod" 39 lang="gomod"
diff --git a/etc/NEWS b/etc/NEWS
index 682928afa8e..662d2ad52b7 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -251,6 +251,10 @@ HTML files.
251*** New major mode heex-ts-mode'. 251*** New major mode heex-ts-mode'.
252A major mode based on the tree-sitter library for editing HEEx files. 252A major mode based on the tree-sitter library for editing HEEx files.
253 253
254*** New major mode elixir-ts-mode'.
255A major mode based on the tree-sitter library for editing Elixir
256files.
257
254--- 258---
255** The highly accessible Modus themes collection has six items. 259** The highly accessible Modus themes collection has six items.
256The 'modus-operandi' and 'modus-vivendi' are the main themes that have 260The '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 @@ chosen (interactively or automatically)."
221 ((java-mode java-ts-mode) . ("jdtls")) 221 ((java-mode java-ts-mode) . ("jdtls"))
222 (dart-mode . ("dart" "language-server" 222 (dart-mode . ("dart" "language-server"
223 "--client-id" "emacs.eglot-dart")) 223 "--client-id" "emacs.eglot-dart"))
224 (elixir-mode . ("language_server.sh")) 224 ((elixir-ts-mode elixir-mode) . ("language_server.sh"))
225 (ada-mode . ("ada_language_server")) 225 (ada-mode . ("ada_language_server"))
226 (scala-mode . ,(eglot-alternatives 226 (scala-mode . ,(eglot-alternatives
227 '("metals" "metals-emacs"))) 227 '("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 @@
1;;; elixir-ts-mode.el --- Major mode for Elixir with tree-sitter support -*- lexical-binding: t; -*-
2
3;; Copyright (C) 2022-2023 Free Software Foundation, Inc.
4
5;; Author: Wilhelm H Kirschbaum <wkirschbaum@gmail.com>
6;; Created: November 2022
7;; Keywords: elixir languages tree-sitter
8
9;; This file is part of GNU Emacs.
10
11;; GNU Emacs is free software: you can redistribute it and/or modify
12;; it under the terms of the GNU General Public License as published by
13;; the Free Software Foundation, either version 3 of the License, or
14;; (at your option) any later version.
15
16;; GNU Emacs is distributed in the hope that it will be useful,
17;; but WITHOUT ANY WARRANTY; without even the implied warranty of
18;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19;; GNU General Public License for more details.
20
21;; You should have received a copy of the GNU General Public License
22;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
23
24;;; Commentary:
25;;
26;; This package provides `elixir-ts-mode' which is a major mode for editing
27;; Elixir files and embedded HEEx templates that uses Tree Sitter to parse
28;; the language.
29;;
30;; This package is compatible with and was tested against the tree-sitter grammar
31;; for Elixir found at https://github.com/elixir-lang/tree-sitter-elixir.
32;;
33;; Features
34;;
35;; * Indent
36;;
37;; `elixir-ts-mode' tries to replicate the indentation provided by
38;; mix format, but will come with some minor differences.
39;;
40;; * IMenu
41;; * Navigation
42;; * Which-fun
43
44;;; Code:
45
46(require 'treesit)
47(require 'heex-ts-mode)
48(eval-when-compile (require 'rx))
49
50(declare-function treesit-parser-create "treesit.c")
51(declare-function treesit-node-child "treesit.c")
52(declare-function treesit-node-type "treesit.c")
53(declare-function treesit-node-child-by-field-name "treesit.c")
54(declare-function treesit-parser-language "treesit.c")
55(declare-function treesit-parser-included-ranges "treesit.c")
56(declare-function treesit-parser-list "treesit.c")
57(declare-function treesit-node-parent "treesit.c")
58(declare-function treesit-node-start "treesit.c")
59(declare-function treesit-query-compile "treesit.c")
60(declare-function treesit-node-eq "treesit.c")
61(declare-function treesit-node-prev-sibling "treesit.c")
62
63(defgroup elixir-ts nil
64 "Major mode for editing Elixir code."
65 :prefix "elixir-ts-"
66 :group 'languages)
67
68(defcustom elixir-ts-indent-offset 2
69 "Indentation of Elixir statements."
70 :version "30.1"
71 :type 'integer
72 :safe 'integerp
73 :group 'elixir-ts)
74
75(defface elixir-ts-font-comment-doc-identifier-face
76 '((t (:inherit font-lock-doc-face)))
77 "Face used for @comment.doc tags in Elixir files.")
78
79(defface elixir-ts-font-comment-doc-attribute-face
80 '((t (:inherit font-lock-doc-face)))
81 "Face used for @comment.doc.__attribute__ tags in Elixir files.")
82
83(defface elixir-ts-font-sigil-name-face
84 '((t (:inherit font-lock-string-face)))
85 "Face used for @__name__ tags in Elixir files.")
86
87(defconst elixir-ts--sexp-regexp
88 (rx bol
89 (or "call" "stab_clause" "binary_operator" "list" "tuple" "map" "pair"
90 "sigil" "string" "atom" "pair" "alias" "arguments" "atom" "identifier"
91 "boolean" "quoted_content")
92 eol))
93
94(defconst elixir-ts--test-definition-keywords
95 '("describe" "test"))
96
97(defconst elixir-ts--definition-keywords
98 '("def" "defdelegate" "defexception" "defguard" "defguardp"
99 "defimpl" "defmacro" "defmacrop" "defmodule" "defn" "defnp"
100 "defoverridable" "defp" "defprotocol" "defstruct"))
101
102(defconst elixir-ts--definition-keywords-re
103 (concat "^" (regexp-opt elixir-ts--definition-keywords) "$"))
104
105(defconst elixir-ts--kernel-keywords
106 '("alias" "case" "cond" "else" "for" "if" "import" "quote"
107 "raise" "receive" "require" "reraise" "super" "throw" "try"
108 "unless" "unquote" "unquote_splicing" "use" "with"))
109
110(defconst elixir-ts--kernel-keywords-re
111 (concat "^" (regexp-opt elixir-ts--kernel-keywords) "$"))
112
113(defconst elixir-ts--builtin-keywords
114 '("__MODULE__" "__DIR__" "__ENV__" "__CALLER__" "__STACKTRACE__"))
115
116(defconst elixir-ts--builtin-keywords-re
117 (concat "^" (regexp-opt elixir-ts--builtin-keywords) "$"))
118
119(defconst elixir-ts--doc-keywords
120 '("moduledoc" "typedoc" "doc"))
121
122(defconst elixir-ts--doc-keywords-re
123 (concat "^" (regexp-opt elixir-ts--doc-keywords) "$"))
124
125(defconst elixir-ts--reserved-keywords
126 '("when" "and" "or" "not" "in"
127 "not in" "fn" "do" "end" "catch" "rescue" "after" "else"))
128
129(defconst elixir-ts--reserved-keywords-re
130 (concat "^" (regexp-opt elixir-ts--reserved-keywords) "$"))
131
132(defconst elixir-ts--reserved-keywords-vector
133 (apply #'vector elixir-ts--reserved-keywords))
134
135(defvar elixir-ts--capture-anonymous-function-end
136 (when (treesit-available-p)
137 (treesit-query-compile 'elixir '((anonymous_function "end" @end)))))
138
139(defvar elixir-ts--capture-operator-parent
140 (when (treesit-available-p)
141 (treesit-query-compile 'elixir '((binary_operator operator: _ @val)))))
142
143(defvar elixir-ts--syntax-table
144 (let ((table (make-syntax-table)))
145 (modify-syntax-entry ?| "." table)
146 (modify-syntax-entry ?- "." table)
147 (modify-syntax-entry ?+ "." table)
148 (modify-syntax-entry ?* "." table)
149 (modify-syntax-entry ?/ "." table)
150 (modify-syntax-entry ?< "." table)
151 (modify-syntax-entry ?> "." table)
152 (modify-syntax-entry ?_ "_" table)
153 (modify-syntax-entry ?? "w" table)
154 (modify-syntax-entry ?~ "w" table)
155 (modify-syntax-entry ?! "_" table)
156 (modify-syntax-entry ?' "\"" table)
157 (modify-syntax-entry ?\" "\"" table)
158 (modify-syntax-entry ?# "<" table)
159 (modify-syntax-entry ?\n ">" table)
160 (modify-syntax-entry ?\( "()" table)
161 (modify-syntax-entry ?\) ")(" table)
162 (modify-syntax-entry ?\{ "(}" table)
163 (modify-syntax-entry ?\} "){" table)
164 (modify-syntax-entry ?\[ "(]" table)
165 (modify-syntax-entry ?\] ")[" table)
166 (modify-syntax-entry ?: "'" table)
167 (modify-syntax-entry ?@ "'" table)
168 table)
169 "Syntax table for `elixir-ts-mode'.")
170
171(defun elixir-ts--argument-indent-offset (node _parent &rest _)
172 "Return the argument offset position for NODE."
173 (if (treesit-node-prev-sibling node t) 0 elixir-ts-indent-offset))
174
175(defun elixir-ts--argument-indent-anchor (node parent &rest _)
176 "Return the argument anchor position for NODE and PARENT."
177 (let ((first-sibling (treesit-node-child parent 0 t)))
178 (if (and first-sibling (not (treesit-node-eq first-sibling node)))
179 (treesit-node-start first-sibling)
180 (elixir-ts--parent-expression-start node parent))))
181
182(defun elixir-ts--parent-expression-start (_node parent &rest _)
183 "Return the indentation expression start for NODE and PARENT."
184 ;; If the parent is the first expression on the line return the
185 ;; parent start of node position, otherwise use the parent call
186 ;; start if available.
187 (if (eq (treesit-node-start parent)
188 (save-excursion
189 (goto-char (treesit-node-start parent))
190 (back-to-indentation)
191 (point)))
192 (treesit-node-start parent)
193 (let ((expr-parent
194 (treesit-parent-until
195 parent
196 (lambda (n)
197 (member (treesit-node-type n)
198 '("call" "binary_operator" "keywords" "list"))))))
199 (save-excursion
200 (goto-char (treesit-node-start expr-parent))
201 (back-to-indentation)
202 (if (looking-at "|>")
203 (point)
204 (treesit-node-start expr-parent))))))
205
206(defvar elixir-ts--indent-rules
207 (let ((offset elixir-ts-indent-offset))
208 `((elixir
209 ((parent-is "^source$") column-0 0)
210 ((parent-is "^string$") parent-bol 0)
211 ((parent-is "^quoted_content$")
212 (lambda (_n parent bol &rest _)
213 (save-excursion
214 (back-to-indentation)
215 (if (bolp)
216 (progn
217 (goto-char (treesit-node-start parent))
218 (back-to-indentation)
219 (point))
220 (point))))
221 0)
222 ((node-is "^|>$") parent-bol 0)
223 ((node-is "^|$") parent-bol 0)
224 ((node-is "^]$") ,'elixir-ts--parent-expression-start 0)
225 ((node-is "^}$") ,'elixir-ts--parent-expression-start 0)
226 ((node-is "^)$") ,'elixir-ts--parent-expression-start 0)
227 ((node-is "^else_block$") grand-parent 0)
228 ((node-is "^catch_block$") grand-parent 0)
229 ((node-is "^rescue_block$") grand-parent 0)
230 ((node-is "^after_block$") grand-parent 0)
231 ((parent-is "^else_block$") parent ,offset)
232 ((parent-is "^catch_block$") parent ,offset)
233 ((parent-is "^rescue_block$") parent ,offset)
234 ((parent-is "^rescue_block$") parent ,offset)
235 ((parent-is "^after_block$") parent ,offset)
236 ((parent-is "^access_call$")
237 ,'elixir-ts--argument-indent-anchor
238 ,'elixir-ts--argument-indent-offset)
239 ((parent-is "^tuple$")
240 ,'elixir-ts--argument-indent-anchor
241 ,'elixir-ts--argument-indent-offset)
242 ((parent-is "^list$")
243 ,'elixir-ts--argument-indent-anchor
244 ,'elixir-ts--argument-indent-offset)
245 ((parent-is "^pair$") parent ,offset)
246 ((parent-is "^map_content$") parent-bol 0)
247 ((parent-is "^map$") ,'elixir-ts--parent-expression-start ,offset)
248 ((node-is "^stab_clause$") parent-bol ,offset)
249 ((query ,elixir-ts--capture-operator-parent) grand-parent 0)
250 ((node-is "^when$") parent 0)
251 ((node-is "^keywords$") parent-bol ,offset)
252 ((parent-is "^body$")
253 (lambda (node parent _)
254 (save-excursion
255 ;; The grammar adds a comment outside of the body, so we have to indent
256 ;; to the grand-parent if it is available.
257 (goto-char (treesit-node-start
258 (or (treesit-node-parent parent) (parent))))
259 (back-to-indentation)
260 (point)))
261 ,offset)
262 ((parent-is "^arguments$")
263 ,'elixir-ts--argument-indent-anchor
264 ,'elixir-ts--argument-indent-offset)
265 ;; Handle incomplete maps when parent is ERROR.
266 ((n-p-gp "^binary_operator$" "ERROR" nil) parent-bol 0)
267 ;; When there is an ERROR, just indent to prev-line.
268 ((parent-is "ERROR") prev-line 0)
269 ((node-is "^binary_operator$")
270 (lambda (node parent &rest _)
271 (let ((top-level
272 (treesit-parent-while
273 node
274 (lambda (node)
275 (equal (treesit-node-type node)
276 "binary_operator")))))
277 (if (treesit-node-eq top-level node)
278 (elixir-ts--parent-expression-start node parent)
279 (treesit-node-start top-level))))
280 (lambda (node parent _)
281 (cond
282 ((equal (treesit-node-type parent) "do_block")
283 ,offset)
284 ((equal (treesit-node-type parent) "binary_operator")
285 ,offset)
286 (t 0))))
287 ((parent-is "^binary_operator$")
288 (lambda (node parent bol &rest _)
289 (treesit-node-start
290 (treesit-parent-while
291 parent
292 (lambda (node)
293 (equal (treesit-node-type node) "binary_operator")))))
294 ,offset)
295 ((node-is "^pair$") first-sibling 0)
296 ((query ,elixir-ts--capture-anonymous-function-end) parent-bol 0)
297 ((node-is "^end$") standalone-parent 0)
298 ((parent-is "^do_block$") grand-parent ,offset)
299 ((parent-is "^anonymous_function$")
300 elixir-ts--treesit-anchor-grand-parent-bol ,offset)
301 ((parent-is "^else_block$") parent ,offset)
302 ((parent-is "^rescue_block$") parent ,offset)
303 ((parent-is "^catch_block$") parent ,offset)
304 ((parent-is "^keywords$") parent-bol 0)
305 ((node-is "^call$") parent-bol ,offset)
306 ((node-is "^comment$") parent-bol ,offset)))))
307
308(defvar elixir-ts--font-lock-settings
309 (treesit-font-lock-rules
310 :language 'elixir
311 :feature 'elixir-comment
312 '((comment) @font-lock-comment-face)
313
314 :language 'elixir
315 :feature 'elixir-string
316 :override t
317 '([(string) (charlist)] @font-lock-string-face)
318
319 :language 'elixir
320 :feature 'elixir-string-interpolation
321 :override t
322 '((string
323 [
324 quoted_end: _ @font-lock-string-face
325 quoted_start: _ @font-lock-string-face
326 (quoted_content) @font-lock-string-face
327 (interpolation
328 "#{" @font-lock-regexp-grouping-backslash "}"
329 @font-lock-regexp-grouping-backslash)
330 ])
331 (charlist
332 [
333 quoted_end: _ @font-lock-string-face
334 quoted_start: _ @font-lock-string-face
335 (quoted_content) @font-lock-string-face
336 (interpolation
337 "#{" @font-lock-regexp-grouping-backslash "}"
338 @font-lock-regexp-grouping-backslash)
339 ]))
340
341 :language 'elixir
342 :feature 'elixir-keyword
343 `(,elixir-ts--reserved-keywords-vector
344 @font-lock-keyword-face
345 (binary_operator
346 operator: _ @font-lock-keyword-face
347 (:match ,elixir-ts--reserved-keywords-re @font-lock-keyword-face)))
348
349 :language 'elixir
350 :feature 'elixir-doc
351 :override t
352 `((unary_operator
353 operator: "@" @elixir-ts-font-comment-doc-attribute-face
354 operand: (call
355 target: (identifier) @elixir-ts-font-comment-doc-identifier-face
356 ;; Arguments can be optional, so adding another
357 ;; entry without arguments.
358 ;; If we don't handle then we don't apply font
359 ;; and the non doc fortification query will take specify
360 ;; a more specific font which takes precedence.
361 (arguments
362 [
363 (string) @font-lock-doc-face
364 (charlist) @font-lock-doc-face
365 (sigil) @font-lock-doc-face
366 (boolean) @font-lock-doc-face
367 ]))
368 (:match ,elixir-ts--doc-keywords-re
369 @elixir-ts-font-comment-doc-identifier-face))
370 (unary_operator
371 operator: "@" @elixir-ts-font-comment-doc-attribute-face
372 operand: (call
373 target: (identifier) @elixir-ts-font-comment-doc-identifier-face)
374 (:match ,elixir-ts--doc-keywords-re
375 @elixir-ts-font-comment-doc-identifier-face)))
376
377 :language 'elixir
378 :feature 'elixir-unary-operator
379 `((unary_operator operator: "@" @font-lock-preprocessor-face
380 operand: [
381 (identifier) @font-lock-preprocessor-face
382 (call target: (identifier)
383 @font-lock-preprocessor-face)
384 (boolean) @font-lock-preprocessor-face
385 (nil) @font-lock-preprocessor-face
386 ])
387
388 (unary_operator operator: "&") @font-lock-function-name-face
389 (operator_identifier) @font-lock-operator-face)
390
391 :language 'elixir
392 :feature 'elixir-operator
393 '((binary_operator operator: _ @font-lock-operator-face)
394 (dot operator: _ @font-lock-operator-face)
395 (stab_clause operator: _ @font-lock-operator-face)
396
397 [(boolean) (nil)] @font-lock-constant-face
398 [(integer) (float)] @font-lock-number-face
399 (alias) @font-lock-type-face
400 (call target: (dot left: (atom) @font-lock-type-face))
401 (char) @font-lock-constant-face
402 [(atom) (quoted_atom)] @font-lock-type-face
403 [(keyword) (quoted_keyword)] @font-lock-builtin-face)
404
405 :language 'elixir
406 :feature 'elixir-call
407 `((call
408 target: (identifier) @font-lock-keyword-face
409 (:match ,elixir-ts--definition-keywords-re @font-lock-keyword-face))
410 (call
411 target: (identifier) @font-lock-keyword-face
412 (:match ,elixir-ts--kernel-keywords-re @font-lock-keyword-face))
413 (call
414 target: [(identifier) @font-lock-function-name-face
415 (dot right: (identifier) @font-lock-keyword-face)])
416 (call
417 target: (identifier) @font-lock-keyword-face
418 (arguments
419 [
420 (identifier) @font-lock-keyword-face
421 (binary_operator
422 left: (identifier) @font-lock-keyword-face
423 operator: "when")
424 ])
425 (:match ,elixir-ts--definition-keywords-re @font-lock-keyword-face))
426 (call
427 target: (identifier) @font-lock-keyword-face
428 (arguments
429 (binary_operator
430 operator: "|>"
431 right: (identifier)))
432 (:match ,elixir-ts--definition-keywords-re @font-lock-keyword-face)))
433
434 :language 'elixir
435 :feature 'elixir-constant
436 `((binary_operator operator: "|>" right: (identifier)
437 @font-lock-function-name-face)
438 ((identifier) @font-lock-keyword-face
439 (:match ,elixir-ts--builtin-keywords-re
440 @font-lock-keyword-face))
441 ((identifier) @font-lock-comment-face
442 (:match "^_" @font-lock-comment-face))
443 (identifier) @font-lock-function-name-face
444 ["%"] @font-lock-keyward-face
445 ["," ";"] @font-lock-keyword-face
446 ["(" ")" "[" "]" "{" "}" "<<" ">>"] @font-lock-keyword-face)
447
448 :language 'elixir
449 :feature 'elixir-sigil
450 :override t
451 `((sigil
452 (sigil_name) @elixir-ts-font-sigil-name-face
453 quoted_start: _ @font-lock-string-face
454 quoted_end: _ @font-lock-string-face
455 (:match "^[sSwWpP]$" @elixir-ts-font-sigil-name-face))
456 @font-lock-string-face
457 (sigil
458 (sigil_name) @elixir-ts-font-sigil-name-face
459 quoted_start: _ @font-lock-regex-face
460 quoted_end: _ @font-lock-regex-face
461 (:match "^[rR]$" @elixir-ts-font-sigil-name-face))
462 @font-lock-regex-face
463 (sigil
464 "~" @font-lock-string-face
465 (sigil_name) @elixir-ts-font-sigil-name-face
466 quoted_start: _ @font-lock-string-face
467 quoted_end: _ @font-lock-string-face
468 (:match "^[HF]$" @elixir-ts-font-sigil-name-face)))
469
470 :language 'elixir
471 :feature 'elixir-string-escape
472 :override t
473 `((escape_sequence) @font-lock-regexp-grouping-backslash))
474 "Tree-sitter font-lock settings.")
475
476(defvar elixir-ts--treesit-range-rules
477 (when (treesit-available-p)
478 (treesit-range-rules
479 :embed 'heex
480 :host 'elixir
481 '((sigil (sigil_name) @name (:match "^[HF]$" @name) (quoted_content) @heex)))))
482
483(defun elixir-ts--forward-sexp (&optional arg)
484 "Move forward across one balanced expression (sexp).
485With ARG, do it many times. Negative ARG means move backward."
486 (or arg (setq arg 1))
487 (funcall
488 (if (> arg 0) #'treesit-end-of-thing #'treesit-beginning-of-thing)
489 (if (eq (treesit-language-at (point)) 'heex)
490 heex-ts--sexp-regexp
491 elixir-ts--sexp-regexp)
492 (abs arg)))
493
494(defun elixir-ts--treesit-anchor-grand-parent-bol (_n parent &rest _)
495 "Return the beginning of non-space characters for the parent node of PARENT."
496 (save-excursion
497 (goto-char (treesit-node-start (treesit-node-parent parent)))
498 (back-to-indentation)
499 (point)))
500
501(defun elixir-ts--treesit-language-at-point (point)
502 "Return the language at POINT."
503 (let* ((range nil)
504 (language-in-range
505 (cl-loop
506 for parser in (treesit-parser-list)
507 do (setq range
508 (cl-loop
509 for range in (treesit-parser-included-ranges parser)
510 if (and (>= point (car range)) (<= point (cdr range)))
511 return parser))
512 if range
513 return (treesit-parser-language parser))))
514 (if (null language-in-range)
515 (when-let ((parser (car (treesit-parser-list))))
516 (treesit-parser-language parser))
517 language-in-range)))
518
519(defun elixir-ts--defun-p (node)
520 "Return non-nil when NODE is a defun."
521 (member (treesit-node-text
522 (treesit-node-child-by-field-name node "target"))
523 (append
524 elixir-ts--definition-keywords
525 elixir-ts--test-definition-keywords)))
526
527(defun elixir-ts--defun-name (node)
528 "Return the name of the defun NODE.
529Return nil if NODE is not a defun node or doesn't have a name."
530 (pcase (treesit-node-type node)
531 ("call" (let ((node-child
532 (treesit-node-child (treesit-node-child node 1) 0)))
533 (pcase (treesit-node-type node-child)
534 ("alias" (treesit-node-text node-child t))
535 ("call" (treesit-node-text
536 (treesit-node-child-by-field-name node-child "target") t))
537 ("binary_operator"
538 (treesit-node-text
539 (treesit-node-child-by-field-name
540 (treesit-node-child-by-field-name node-child "left") "target")
541 t))
542 ("identifier"
543 (treesit-node-text node-child t))
544 (_ nil))))
545 (_ nil)))
546
547;;;###autoload
548(define-derived-mode elixir-ts-mode prog-mode "Elixir"
549 "Major mode for editing Elixir, powered by tree-sitter."
550 :group 'elixir-ts
551 :syntax-table elixir-ts--syntax-table
552
553 ;; Comments
554 (setq-local comment-start "# ")
555 (setq-local comment-start-skip
556 (rx "#" (* (syntax whitespace))))
557
558 (setq-local comment-end "")
559 (setq-local comment-end-skip
560 (rx (* (syntax whitespace))
561 (group (or (syntax comment-end) "\n"))))
562
563 ;; Compile
564 (setq-local compile-command "mix")
565
566 (when (treesit-ready-p 'elixir)
567 ;; The HEEx parser has to be created first for elixir to ensure elixir
568 ;; is the first language when looking for treesit ranges.
569 (if (treesit-ready-p 'heex)
570 (treesit-parser-create 'heex))
571
572 (treesit-parser-create 'elixir)
573
574 (setq-local treesit-language-at-point-function
575 'elixir-ts--treesit-language-at-point)
576
577 ;; Font-lock.
578 (setq-local treesit-font-lock-settings elixir-ts--font-lock-settings)
579 (setq-local treesit-font-lock-feature-list
580 '(( elixir-comment elixir-constant elixir-doc )
581 ( elixir-string elixir-keyword elixir-unary-operator
582 elixir-call elixir-operator )
583 ( elixir-sigil elixir-string-escape elixir-string-interpolation)))
584
585 ;; Imenu.
586 (setq-local treesit-simple-imenu-settings
587 '((nil "\\`call\\'" elixir-ts--defun-p nil)))
588
589 ;; Indent.
590 (setq-local treesit-simple-indent-rules elixir-ts--indent-rules)
591
592 ;; Navigation
593 (setq-local forward-sexp-function #'elixir-ts--forward-sexp)
594 (setq-local treesit-defun-type-regexp
595 '("call" . elixir-ts--defun-p))
596
597 (setq-local treesit-defun-name-function #'elixir-ts--defun-name)
598
599 ;; Embedded Heex
600 (when (treesit-ready-p 'heex)
601 (setq-local treesit-range-settings elixir-ts--treesit-range-rules)
602
603 (setq-local treesit-simple-indent-rules
604 (append treesit-simple-indent-rules heex-ts--indent-rules))
605
606 (setq-local treesit-font-lock-settings
607 (append treesit-font-lock-settings
608 heex-ts--font-lock-settings))
609
610 (setq-local treesit-simple-indent-rules
611 (append treesit-simple-indent-rules
612 heex-ts--indent-rules))
613
614 (setq-local treesit-font-lock-feature-list
615 '(( elixir-comment elixir-constant elixir-doc
616 heex-comment heex-keyword heex-doctype )
617 ( elixir-string elixir-keyword elixir-unary-operator
618 elixir-call elixir-operator
619 heex-component heex-tag heex-attribute heex-string)
620 ( elixir-sigil elixir-string-escape
621 elixir-string-interpolation ))))
622
623 (treesit-major-mode-setup)))
624
625(if (treesit-ready-p 'elixir)
626 (progn
627 (add-to-list 'auto-mode-alist '("\\.elixir\\'" . elixir-ts-mode))
628 (add-to-list 'auto-mode-alist '("\\.ex\\'" . elixir-ts-mode))
629 (add-to-list 'auto-mode-alist '("\\.exs\\'" . elixir-ts-mode))
630 (add-to-list 'auto-mode-alist '("mix\\.lock" . elixir-ts-mode))))
631
632(provide 'elixir-ts-mode)
633
634;;; 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 @@
1Code:
2 (lambda ()
3 (setq indent-tabs-mode nil)
4 (elixir-ts-mode)
5 (indent-region (point-min) (point-max)))
6
7Point-Char: $
8
9Name: Basic modules
10
11=-=
12 defmodule Foobar do
13def bar() do
14"one"
15 end
16 end
17=-=
18defmodule Foobar do
19 def bar() do
20 "one"
21 end
22end
23=-=-=
24
25Name: Map
26
27=-=
28map = %{
29 "a" => 1,
30 "b" => 2
31}
32=-=-=
33
34Name: Map in function def
35
36=-=
37def foobar() do
38 %{
39 one: "one",
40 two: "two",
41 three: "three",
42 four: "four"
43 }
44end
45=-=-=
46
47Name: Map in tuple
48
49=-=
50def foo() do
51 {:ok,
52 %{
53 state
54 | extra_arguments: extra_arguments,
55 max_children: max_children,
56 max_restarts: max_restarts,
57 max_seconds: max_seconds,
58 strategy: strategy
59 }}
60end
61=-=-=
62
63Name: Nested maps
64
65=-=
66%{
67 foo: "bar",
68 bar: %{
69 foo: "bar"
70 }
71}
72
73def foo() do
74 %{
75 foo: "bar",
76 bar: %{
77 foo: "bar"
78 }
79 }
80end
81=-=-=
82
83Name: Block assignments
84
85=-=
86foo =
87 if true do
88 "yes"
89 else
90 "no"
91 end
92=-=-=
93
94Name: Function rescue
95
96=-=
97def foo do
98 "bar"
99rescue
100 e ->
101 "bar"
102end
103=-=-=
104
105Name: With statement
106=-=
107with one <- one(),
108 two <- two(),
109 {:ok, value} <- get_value(one, two) do
110 {:ok, value}
111else
112 {:error, %{"Message" => message}} ->
113 {:error, message}
114end
115=-=-=
116
117Name: Pipe statements with fn
118
119=-=
120[1, 2]
121|> Enum.map(fn num ->
122 num + 1
123end)
124=-=-=
125
126Name: Pipe statements stab clases
127
128=-=
129[1, 2]
130|> Enum.map(fn
131 x when x < 10 -> x * 2
132 x -> x * 3
133end)
134=-=-=
135
136Name: Pipe statements params
137
138=-=
139[1, 2]
140|> foobar(
141 :one,
142 :two,
143 :three,
144 :four
145)
146=-=-=
147
148Name: Parameter maps
149
150=-=
151def something(%{
152 one: :one,
153 two: :two
154 }) do
155 {:ok, "done"}
156end
157=-=-=
158
159Name: Binary operator in else block
160
161=-=
162defp foobar() do
163 if false do
164 :foo
165 else
166 :bar |> foo
167 end
168end
169=-=-=
170
171Name: Tuple indentation
172
173=-=
174tuple = {
175 :one,
176 :two
177}
178
179{
180 :one,
181 :two
182}
183=-=-=
184
185Name: Spec and method
186
187=-=
188@spec foobar(
189 t,
190 acc,
191 (one, something -> :bar | far),
192 (two -> :bar | far)
193 ) :: any()
194 when chunk: any
195def foobar(enumerable, acc, chunk_fun, after_fun) do
196 {_, {res, acc}} =
197 case after_fun.(acc) do
198 {:one, "one"} ->
199 "one"
200
201 {:two, "two"} ->
202 "two"
203 end
204end
205=-=-=
206
207Name: Spec with multi-line result
208
209=-=
210@type result ::
211 {:done, term}
212 | {:two}
213 | {:one}
214
215@type result ::
216 {
217 :done,
218 term
219 }
220 | {:two}
221 | {:one}
222
223@type boo_bar ::
224 (foo :: pos_integer, bar :: pos_integer -> any())
225
226@spec foo_bar(
227 t,
228 (foo -> any),
229 (() -> any) | (foo, foo -> boolean) | module()
230 ) :: any
231 when foo: any
232def foo(one, fun, other)
233=-=-=
234
235Name: String concatenation in call
236
237=-=
238IO.warn(
239 "one" <>
240 "two" <>
241 "bar"
242)
243
244IO.warn(
245 "foo" <>
246 "bar"
247)
248=-=-=
249
250Name: Incomplete tuple
251
252=-=
253map = {
254:foo
255
256=-=
257map = {
258 :foo
259
260=-=-=
261
262Name: Incomplete map
263
264=-=
265map = %{
266 "a" => "a",
267=-=-=
268
269Name: Incomplete list
270
271=-=
272map = [
273:foo
274
275=-=
276map = [
277 :foo
278
279=-=-=
280
281Name: String concatenation
282
283=-=
284"one" <>
285 "two" <>
286 "three" <>
287 "four"
288=-=-=
289
290Name: Tuple with same line first node
291
292=-=
293{:one,
294 :two}
295
296{:ok,
297 fn one ->
298 one
299 |> String.upcase(one)
300 end}
301=-=-=
302
303Name: Long tuple
304
305=-=
306{"January", "February", "March", "April", "May", "June", "July", "August", "September",
307 "October", "November", "December"}
308=-=-=
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 @@
1;;; c-ts-mode-tests.el --- Tests for Tree-sitter-based C mode -*- lexical-binding: t; -*-
2
3;; Copyright (C) 2023 Free Software Foundation, Inc.
4
5;; This file is part of GNU Emacs.
6
7;; GNU Emacs is free software: you can redistribute it and/or modify
8;; it under the terms of the GNU General Public License as published by
9;; the Free Software Foundation, either version 3 of the License, or
10;; (at your option) any later version.
11
12;; GNU Emacs is distributed in the hope that it will be useful,
13;; but WITHOUT ANY WARRANTY; without even the implied warranty of
14;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15;; GNU General Public License for more details.
16
17;; You should have received a copy of the GNU General Public License
18;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
19
20;;; Code:
21
22(require 'ert)
23(require 'ert-x)
24(require 'treesit)
25
26(ert-deftest elixir-ts-mode-test-indentation ()
27 (skip-unless (and (treesit-ready-p 'elixir) (treesit-ready-p 'heex)))
28 (ert-test-erts-file (ert-resource-file "indent.erts")))
29
30(provide 'elixir-ts-mode-tests)
31;;; elixir-ts-mode-tests.el ends here