From mboxrd@z Thu Jan 1 00:00:00 1970 Path: news.gmane.io!.POSTED.blaine.gmane.org!not-for-mail From: Wilhelm Kirschbaum Newsgroups: gmane.emacs.bugs Subject: bug#61996: 30.0.50; Submitting elixir-ts-mode and heex-ts-mode Date: Mon, 06 Mar 2023 21:24:11 +0200 Message-ID: <877cvtfz37.fsf@gmail.com> References: <87mt4qibnk.fsf@gmail.com> <83cz5m8515.fsf@gnu.org> <874jqx3h47.fsf@gmail.com> <83lek97mm1.fsf@gnu.org> Mime-Version: 1.0 Content-Type: multipart/mixed; boundary="=-=-=" Injection-Info: ciao.gmane.io; posting-host="blaine.gmane.org:116.202.254.214"; logging-data="1952"; mail-complaints-to="usenet@ciao.gmane.io" User-Agent: mu4e 1.9.3; emacs 30.0.50 Cc: 61996@debbugs.gnu.org, theo@thornhill.no, casouri@gmail.com To: Eli Zaretskii Original-X-From: bug-gnu-emacs-bounces+geb-bug-gnu-emacs=m.gmane-mx.org@gnu.org Mon Mar 06 20:41:09 2023 Return-path: Envelope-to: geb-bug-gnu-emacs@m.gmane-mx.org Original-Received: from lists.gnu.org ([209.51.188.17]) by ciao.gmane.io with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.92) (envelope-from ) id 1pZGhp-0000MG-Cg for geb-bug-gnu-emacs@m.gmane-mx.org; Mon, 06 Mar 2023 20:41:09 +0100 Original-Received: from localhost ([::1] helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1pZGhk-000338-8E; Mon, 06 Mar 2023 14:41:04 -0500 Original-Received: from eggs.gnu.org ([2001:470:142:3::10]) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1pZGhi-0002tk-DD for bug-gnu-emacs@gnu.org; Mon, 06 Mar 2023 14:41:02 -0500 Original-Received: from debbugs.gnu.org ([209.51.188.43]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_128_GCM_SHA256:128) (Exim 4.90_1) (envelope-from ) id 1pZGhh-00033X-VI for bug-gnu-emacs@gnu.org; Mon, 06 Mar 2023 14:41:02 -0500 Original-Received: from Debian-debbugs by debbugs.gnu.org with local (Exim 4.84_2) (envelope-from ) id 1pZGhh-0001hw-RN for bug-gnu-emacs@gnu.org; Mon, 06 Mar 2023 14:41:01 -0500 X-Loop: help-debbugs@gnu.org Resent-From: Wilhelm Kirschbaum Original-Sender: "Debbugs-submit" Resent-CC: bug-gnu-emacs@gnu.org Resent-Date: Mon, 06 Mar 2023 19:41:01 +0000 Resent-Message-ID: Resent-Sender: help-debbugs@gnu.org X-GNU-PR-Message: followup 61996 X-GNU-PR-Package: emacs Original-Received: via spool by 61996-submit@debbugs.gnu.org id=B61996.16781316286517 (code B ref 61996); Mon, 06 Mar 2023 19:41:01 +0000 Original-Received: (at 61996) by debbugs.gnu.org; 6 Mar 2023 19:40:28 +0000 Original-Received: from localhost ([127.0.0.1]:43700 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1pZGh8-0001h1-BX for submit@debbugs.gnu.org; Mon, 06 Mar 2023 14:40:28 -0500 Original-Received: from mail-wm1-f46.google.com ([209.85.128.46]:36504) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1pZGh4-0001gl-Rw for 61996@debbugs.gnu.org; Mon, 06 Mar 2023 14:40:24 -0500 Original-Received: by mail-wm1-f46.google.com with SMTP id j19-20020a05600c191300b003eb3e1eb0caso8963327wmq.1 for <61996@debbugs.gnu.org>; Mon, 06 Mar 2023 11:40:22 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20210112; t=1678131617; h=mime-version:message-id:in-reply-to:date:subject:cc:to:from :user-agent:references:from:to:cc:subject:date:message-id:reply-to; bh=ZpMI9KPNJNvACgEcfzWQ4a30co6RmmEDQYvqyqWXvI4=; b=Ha6w3oClCs21WzRvgdkfNJ+jkZG8O3PM3taGztZaAkHsOQALeaUMkppA1J7+2qCi1K 0kIxubtDWchbDMN1gej8mOWSWKNfsCPjcRoNGLLecpDdWrduPb9nEvRsVfPXjkasNc/Q LEReMxVX61prfETIEhD5FyuhpGTC5YUo+tapoqY5AB2J/bv2L4MyYeq2P992disL4Vvq ir2PpjD0YZY86h91GYgKDndlgpfDthBvOtZCSBjj+BqKsAsYMl2i7BNx3uuv+6zpNK50 D/HkWhD3E3OYhKpwc2LF+viA0NOqujdm3x7UTRW4M8pUSTnWc50M+umv3nVS6C3QYN1A c11A== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20210112; t=1678131617; h=mime-version:message-id:in-reply-to:date:subject:cc:to:from :user-agent:references:x-gm-message-state:from:to:cc:subject:date :message-id:reply-to; bh=ZpMI9KPNJNvACgEcfzWQ4a30co6RmmEDQYvqyqWXvI4=; b=TLtarxoDuvOLzCZBEsS5jUnniqdiNXTabwSoRfCGABisyDnkqfKQ6MAh1KzrQRLqBB qpI2P8XO0fdbsze5Mt0OBzKDQcmmRrfyBfUC65GBcr57mgBMlj2yieMWi1VQMHhsZ9pH vlsUMaIlo0tP15hHtPf8J/yEh4Wzh9sLa0gE7gQ93k0MRYz6EBZet6fEEstRcmI2jpv7 w7x3XMNWvcW9qtuDudO9WgFN5/kUCOg5mr5FprZsLKPZ5K7o7l8HvobH6Oaji9XHoso4 eZV+P7RPvyozNUL2a87Gt6zgQNAczL3GKQ5rzUV55oqwC6gjC4Z7fUJ/qxjIF7NY+ofa 59oA== X-Gm-Message-State: AO0yUKXv40+DzyTEKLaDzIrdurWkPLiNOSvbnE+LoMy7XKXpuqgshkmi HPuhh1sKTBn+WEhAGrAXfJ0= X-Google-Smtp-Source: AK7set+Hr3RPHzeugu4E6SwpmCdI00Kauldw8wGjUT5xpyHD/Ehg/VqaGsh+2K5hr09z9tpBJSyNoA== X-Received: by 2002:a05:600c:4fc2:b0:3eb:37ce:4c3e with SMTP id o2-20020a05600c4fc200b003eb37ce4c3emr10840945wmq.16.1678131616795; Mon, 06 Mar 2023 11:40:16 -0800 (PST) Original-Received: from melissa.local (ec2-13-245-158-50.af-south-1.compute.amazonaws.com. [13.245.158.50]) by smtp.gmail.com with ESMTPSA id m1-20020a05600c4f4100b003e01493b136sm15926735wmq.43.2023.03.06.11.40.14 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Mon, 06 Mar 2023 11:40:16 -0800 (PST) In-reply-to: <83lek97mm1.fsf@gnu.org> X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list X-BeenThere: bug-gnu-emacs@gnu.org List-Id: "Bug reports for GNU Emacs, the Swiss army knife of text editors" List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: bug-gnu-emacs-bounces+geb-bug-gnu-emacs=m.gmane-mx.org@gnu.org Original-Sender: bug-gnu-emacs-bounces+geb-bug-gnu-emacs=m.gmane-mx.org@gnu.org Xref: news.gmane.io gmane.emacs.bugs:257415 Archived-At: --=-=-= Content-Type: text/plain; format=flowed Eli Zaretskii writes: >> >> +(if (treesit-ready-p 'elixir) >> >> + (progn >> >> + (add-to-list 'auto-mode-alist '("\\.elixir\\'" . >> >> elixir-ts-mode)) >> >> + (add-to-list 'auto-mode-alist '("\\.ex\\'" . >> >> elixir-ts-mode)) >> >> + (add-to-list 'auto-mode-alist '("\\.exs\\'" . >> >> elixir-ts-mode)) >> >> + (add-to-list 'auto-mode-alist '("mix\\.lock" . >> >> elixir-ts-mode)))) >> >> + >> >> +(if (treesit-ready-p 'heex) >> >> + (add-to-list 'auto-mode-alist '("\\.[hl]?eex\\'" . >> >> heex-ts-mode))) >> > ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ >> > Copy/paste error, I presume? >> > >> > Thanks. >> >> This was intentional, but perhaps a bad choice and lack of >> understanding of how the >> -ts-modes should be activated. elixir-ts-mode should ideally >> load >> the >> HEEx grammar, but should also technically be able to function >> without. > > Sorry, I don't understand: are you saying that the HEEx grammar > supports both modes? I thought you need a separate grammar for > Elixir. I also thought the Elixir files have different > file-name > extensions than the HEEx files. Was I mistaken? > No, you were not mistaken. I corrected this with the new patches. >> The Elixir language author mentioned that heex can practically >> be >> seen as part of Elixir, so requiring heex-ts-mode makes sense, >> sort >> of. >> >> heex-ts-mode and elixir-ts-mode used to be in one file, but I >> was >> asked to split them for the MELPA submission. The HEEx language >> should actually also be able to embed Elixir, but this is not >> essential and we can do without imo. Would it make sense have >> them >> in one file? > > Maybe. Otherwise, if they have a lot in common, you'd need to > duplicate stuff or have a common file used by both. Your call. For this release it will be good just to get the basics to work as is, but good to know it is an option. Attached are the updated patches. Is this the right format, or should I add them inline rather? --=-=-= Content-Type: text/x-patch Content-Disposition: attachment; filename=0001-Add-heex-ts-mode.patch Content-Description: Add heex-ts-mode >From 88c941067da0e34e1e9ababeb813ba51378ae2cc Mon Sep 17 00:00:00 2001 From: Wilhelm H Kirschbaum Date: Mon, 6 Mar 2023 21:18:04 +0200 Subject: [PATCH 1/2] Add heex-ts-mode --- lisp/progmodes/heex-ts-mode.el | 185 ++++++++++++++++++ .../heex-ts-mode-resources/indent.erts | 47 +++++ test/lisp/progmodes/heex-ts-mode-tests.el | 9 + 3 files changed, 241 insertions(+) create mode 100644 lisp/progmodes/heex-ts-mode.el create mode 100644 test/lisp/progmodes/heex-ts-mode-resources/indent.erts create mode 100644 test/lisp/progmodes/heex-ts-mode-tests.el diff --git a/lisp/progmodes/heex-ts-mode.el b/lisp/progmodes/heex-ts-mode.el new file mode 100644 index 00000000000..3feab61a1e7 --- /dev/null +++ b/lisp/progmodes/heex-ts-mode.el @@ -0,0 +1,185 @@ +;;; heex-ts-mode.el --- Major mode for Heex with tree-sitter support -*- lexical-binding: t; -*- + +;; Copyright (C) 2022-2023 Free Software Foundation, Inc. + +;; Author: Wilhelm H Kirschbaum +;; 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 `heex-ts-mode' which is a major mode for editing +;; HEEx files that uses Tree Sitter to parse the language. +;; +;; This package is compatible with and was tested against the tree-sitter grammar +;; for HEEx found at https://github.com/phoenixframework/tree-sitter-heex. + +;;; Code: + +(require 'treesit) +(eval-when-compile (require 'rx)) + +(declare-function treesit-parser-create "treesit.c") +(declare-function treesit-node-child "treesit.c") +(declare-function treesit-node-type "treesit.c") +(declare-function treesit-install-language-grammar "treesit.el") + +(defgroup heex-ts nil + "Major mode for editing HEEx code." + :prefix "heex-ts-" + :group 'langauges) + +(defcustom heex-ts-mode-indent-offset 2 + "Indentation of HEEx statements." + :version "30.1" + :type 'integer + :safe 'integerp + :group 'heex-ts) + +(defconst heex-ts-mode-sexp-regexp + (rx bol + (or "directive" "tag" "component" "slot" + "attribute" "attribute_value" "quoted_attribute_value") + eol)) + +;; There seems to be no parent directive block +;; so we ignore it for until we learn how HEEx treesit +;; represents directive blocks +;; https://github.com/phoenixframework/tree-sitter-heex/issues/28 +(defvar heex-ts-mode--indent-rules + (let ((offset heex-ts-mode-indent-offset)) + `((heex + ((parent-is "fragment") + (lambda (node parent &rest _) + ;; if Heex is embedded indent to parent + ;; otherwise indent to the bol + (if (eq (treesit-language-at (point-min)) 'heex) + (point-min) + (save-excursion + (goto-char (treesit-node-start parent)) + (back-to-indentation) + (point)) + )) 0) + ((node-is "end_tag") parent-bol 0) + ((node-is "end_component") parent-bol 0) + ((node-is "end_slot") parent-bol 0) + ((node-is "/>") parent-bol 0) + ((node-is ">") parent-bol 0) + ((parent-is "comment") prev-adaptive-prefix 0) + ((parent-is "component") parent-bol ,offset) + ((parent-is "tag") parent-bol ,offset) + ((parent-is "start_tag") parent-bol ,offset) + ((parent-is "component") parent-bol ,offset) + ((parent-is "start_component") parent-bol ,offset) + ((parent-is "slot") parent-bol ,offset) + ((parent-is "start_slot") parent-bol ,offset) + ((parent-is "self_closing_tag") parent-bol ,offset) + (no-node parent-bol ,offset))))) + +(defvar heex-ts-mode--font-lock-settings + (when (treesit-available-p) + (treesit-font-lock-rules + :language 'heex + :feature 'heex-comment + '((comment) @font-lock-comment-face) + :language 'heex + :feature 'heex-doctype + '((doctype) @font-lock-doc-face) + :language 'heex + :feature 'heex-tag + `([(tag_name) (slot_name)] @font-lock-function-name-face) + :language 'heex + :feature 'heex-attribute + `((attribute_name) @font-lock-variable-name-face) + :language 'heex + :feature 'heex-keyword + `((special_attribute_name) @font-lock-keyword-face) + :language 'heex + :feature 'heex-string + `([(attribute_value) (quoted_attribute_value)] @font-lock-constant-face) + :language 'heex + :feature 'heex-component + `([ + (component_name) @font-lock-function-name-face + (module) @font-lock-keyword-face + (function) @font-lock-keyword-face + "." @font-lock-keyword-face + ]))) + "Tree-sitter font-lock settings.") + +(defun heex-ts-mode--defun-name (node) + "Return the name of the defun NODE. +Return nil if NODE is not a defun node or doesn't have a name." + (pcase (treesit-node-type node) + ((or "component" "slot" "tag") + (string-trim + (treesit-node-text + (treesit-node-child (treesit-node-child node 0) 1) nil))) + (_ nil))) + +(defun heex-ts-mode--forward-sexp (&optional arg) + (interactive "^p") + (or arg (setq arg 1)) + (funcall + (if (> arg 0) #'treesit-end-of-thing #'treesit-beginning-of-thing) + heex-ts-mode-sexp-regexp + (abs arg))) + +;;;###autoload +(define-derived-mode heex-ts-mode html-mode "Heex" + "Major mode for editing HEEx, powered by tree-sitter." + :group 'heex-ts + + (when (treesit-ready-p 'heex) + (treesit-parser-create 'heex) + + ;; Comments + (setq-local treesit-text-type-regexp + (regexp-opt '("comment" "text"))) + + (setq-local forward-sexp-function #'heex-ts-mode--forward-sexp) + + ;; Navigation. + (setq-local treesit-defun-type-regexp + (rx bol (or "component" "tag" "slot") eol)) + (setq-local treesit-defun-name-function #'heex-ts-mode--defun-name) + + ;; Imenu + (setq-local treesit-simple-imenu-settings + '(("Component" "\\`component\\'" nil nil) + ("Slot" "\\`slot\\'" nil nil) + ("Tag" "\\`tag\\'" nil nil))) + + (setq-local treesit-font-lock-settings heex-ts-mode--font-lock-settings) + + (setq-local treesit-simple-indent-rules heex-ts-mode--indent-rules) + + (setq-local treesit-font-lock-feature-list + '(( heex-comment heex-keyword heex-doctype ) + ( heex-component heex-tag heex-attribute heex-string ) + () ())) + + (treesit-major-mode-setup))) + +(if (treesit-ready-p 'heex) + ;; Both .heex and the deprecated .leex files should work + ;; with the tree-sitter-heex grammar. + (add-to-list 'auto-mode-alist '("\\.[hl]?eex\\'" . heex-ts-mode))) + +(provide 'heex-ts-mode) +;;; heex-ts-mode.el ends here diff --git a/test/lisp/progmodes/heex-ts-mode-resources/indent.erts b/test/lisp/progmodes/heex-ts-mode-resources/indent.erts new file mode 100644 index 00000000000..500ddb2b536 --- /dev/null +++ b/test/lisp/progmodes/heex-ts-mode-resources/indent.erts @@ -0,0 +1,47 @@ +Code: + (lambda () + (setq indent-tabs-mode nil) + (heex-ts-mode) + (indent-region (point-min) (point-max))) + +Point-Char: $ + +Name: Tag + +=-= +
+ div +
+=-= +
+ div +
+=-=-= + +Name: Component + +=-= + + foobar + +=-= + + foobar + +=-=-= + +Name: Slots + +=-= + + <:bar> + foobar + + +=-= + + <:bar> + foobar + + +=-=-= diff --git a/test/lisp/progmodes/heex-ts-mode-tests.el b/test/lisp/progmodes/heex-ts-mode-tests.el new file mode 100644 index 00000000000..b59126e136a --- /dev/null +++ b/test/lisp/progmodes/heex-ts-mode-tests.el @@ -0,0 +1,9 @@ +(require 'ert) +(require 'ert-x) +(require 'treesit) + +(ert-deftest heex-ts-mode-test-indentation () + (skip-unless (treesit-ready-p 'heex)) + (ert-test-erts-file (ert-resource-file "indent.erts"))) + +(provide 'heex-ts-mode-tests) -- 2.39.2 --=-=-= Content-Type: text/x-patch Content-Disposition: attachment; filename=0002-Add-elixir-ts-mode.patch Content-Description: Add elixir-ts-mode >From d13c34ed951e3e6fa473cd1bc2e955e20455022b Mon Sep 17 00:00:00 2001 From: Wilhelm H Kirschbaum Date: Mon, 6 Mar 2023 21:18:35 +0200 Subject: [PATCH 2/2] Add elixir-ts-mode --- lisp/progmodes/elixir-ts-mode.el | 626 ++++++++++++++++++ .../elixir-ts-mode-resources/indent.erts | 147 ++++ test/lisp/progmodes/elixir-ts-mode-tests.el | 31 + 3 files changed, 804 insertions(+) create mode 100644 lisp/progmodes/elixir-ts-mode.el create mode 100644 test/lisp/progmodes/elixir-ts-mode-resources/indent.erts create mode 100644 test/lisp/progmodes/elixir-ts-mode-tests.el diff --git a/lisp/progmodes/elixir-ts-mode.el b/lisp/progmodes/elixir-ts-mode.el new file mode 100644 index 00000000000..0f3c0fee52b --- /dev/null +++ b/lisp/progmodes/elixir-ts-mode.el @@ -0,0 +1,626 @@ +;;; elixir-ts-mode.el --- Major mode for Elixir with tree-sitter support -*- lexical-binding: t; -*- + +;; Copyright (C) 2022-2023 Free Software Foundation, Inc. + +;; Author: Wilhelm H Kirschbaum +;; 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) +(require 'heex-ts-mode) +(eval-when-compile (require 'rx)) + +(declare-function treesit-parser-create "treesit.c") +(declare-function treesit-node-child "treesit.c") +(declare-function treesit-node-type "treesit.c") +(declare-function treesit-node-child-by-field-name "treesit.c") +(declare-function treesit-parser-language "treesit.c") +(declare-function treesit-parser-included-ranges "treesit.c") +(declare-function treesit-parser-list "treesit.c") +(declare-function treesit-node-parent "treesit.c") +(declare-function treesit-node-start "treesit.c") +(declare-function treesit-query-compile "treesit.c") +(declare-function treesit-install-language-grammar "treesit.el") + +(defgroup elixir-ts nil + "Major mode for editing Ruby code." + :prefix "elixir-ts-" + :group 'languages) + +(defcustom elixir-ts-mode-indent-offset 2 + "Indentation of Elixir statements." + :version "30.1" + :type 'integer + :safe 'integerp + :group 'elixir-ts) + +;; used to distinguish from comment-face in query match +(defface elixir-ts-font-comment-doc-identifier-face + '((t (:inherit font-lock-doc-face))) + "For use with @comment.doc tag.") + +;; used to distinguish from comment-face in query match +(defface elixir-ts-font-comment-doc-attribute-face + '((t (:inherit font-lock-doc-face))) + "For use with @comment.doc.__attribute__ tag.") + +;; used to distinguish from special string in query match +(defface elixir-ts-font-sigil-name-face + '((t (:inherit font-lock-string-face))) + "For use with @__name__ tag.") + +(defconst elixir-ts-mode-sexp-regexp + (rx bol + (or "call" "stab_clause" "binary_operator" "list" "tuple" "map" "pair" + "sigil" "string" "atom" "pair" "alias" "arguments" "atom" "identifier" + "boolean" "quoted_content") + eol)) + +(defconst elixir-ts-mode--test-definition-keywords + '("describe" "test")) + +(defconst elixir-ts-mode--definition-keywords + '("def" "defdelegate" "defexception" "defguard" "defguardp" + "defimpl" "defmacro" "defmacrop" "defmodule" "defn" "defnp" + "defoverridable" "defp" "defprotocol" "defstruct")) + +(defconst elixir-ts-mode--definition-keywords-re + (concat "^" (regexp-opt elixir-ts-mode--definition-keywords) "$")) + +(defconst elixir-ts-mode--kernel-keywords + '("alias" "case" "cond" "else" "for" "if" "import" "quote" + "raise" "receive" "require" "reraise" "super" "throw" "try" + "unless" "unquote" "unquote_splicing" "use" "with")) + +(defconst elixir-ts-mode--kernel-keywords-re + (concat "^" (regexp-opt elixir-ts-mode--kernel-keywords) "$")) + +(defconst elixir-ts-mode--builtin-keywords + '("__MODULE__" "__DIR__" "__ENV__" "__CALLER__" "__STACKTRACE__")) + +(defconst elixir-ts-mode--builtin-keywords-re + (concat "^" (regexp-opt elixir-ts-mode--builtin-keywords) "$")) + +(defconst elixir-ts-mode--doc-keywords + '("moduledoc" "typedoc" "doc")) + +(defconst elixir-ts-mode--doc-keywords-re + (concat "^" (regexp-opt elixir-ts-mode--doc-keywords) "$")) + +(defconst elixir-ts-mode--reserved-keywords + '("when" "and" "or" "not" "in" + "not in" "fn" "do" "end" "catch" "rescue" "after" "else")) + +(defconst elixir-ts-mode--reserved-keywords-re + (concat "^" (regexp-opt elixir-ts-mode--reserved-keywords) "$")) + +(defconst elixir-ts-mode--reserved-keywords-vector + (apply #'vector elixir-ts-mode--reserved-keywords)) + +(defvar elixir-ts-mode--capture-anonymous-function-end + (when (treesit-available-p) + (treesit-query-compile 'elixir '((anonymous_function "end" @end))))) + +(defvar elixir-ts-mode--capture-operator-parent + (when (treesit-available-p) + (treesit-query-compile 'elixir '((binary_operator operator: _ @val))))) + +(defvar elixir-ts-mode--syntax-table + (let ((table (make-syntax-table))) + (modify-syntax-entry ?| "." table) + (modify-syntax-entry ?- "." table) + (modify-syntax-entry ?+ "." table) + (modify-syntax-entry ?* "." table) + (modify-syntax-entry ?/ "." table) + (modify-syntax-entry ?< "." table) + (modify-syntax-entry ?> "." table) + (modify-syntax-entry ?_ "_" table) + (modify-syntax-entry ?? "w" table) + (modify-syntax-entry ?~ "w" table) + (modify-syntax-entry ?! "_" table) + (modify-syntax-entry ?' "\"" table) + (modify-syntax-entry ?\" "\"" table) + (modify-syntax-entry ?# "<" table) + (modify-syntax-entry ?\n ">" table) + (modify-syntax-entry ?\( "()" table) + (modify-syntax-entry ?\) ")(" table) + (modify-syntax-entry ?\{ "(}" table) + (modify-syntax-entry ?\} "){" table) + (modify-syntax-entry ?\[ "(]" table) + (modify-syntax-entry ?\] ")[" table) + (modify-syntax-entry ?: "'" table) + (modify-syntax-entry ?@ "'" table) + table) + "Syntax table for `elixir-ts-mode.") + +(defun elixir-ts-mode--call-parent-start (parent) + (let ((call-parent + (or (treesit-parent-until + parent + (lambda (node) + (equal (treesit-node-type node) "call"))) + parent))) + (save-excursion + (goto-char (treesit-node-start call-parent)) + (back-to-indentation) + ;; for pipes we ignore the call indentation + (if (looking-at "|>") + (point) + (treesit-node-start call-parent))))) + +(defvar elixir-ts-mode--indent-rules + (let ((offset elixir-ts-mode-indent-offset)) + `((elixir + ((parent-is "^source$") column-0 0) + ((parent-is "^string$") parent-bol 0) + ((parent-is "^quoted_content$") + (lambda (_n parent bol &rest _) + (save-excursion + (back-to-indentation) + (if (bolp) + (progn + (goto-char (treesit-node-start parent)) + (back-to-indentation) + (point)) + (point)))) 0) + ((node-is "^]") parent-bol 0) + ((node-is "^|>$") parent-bol 0) + ((node-is "^|$") parent-bol 0) + ((node-is "^}$") parent-bol 0) + ((node-is "^)$") + (lambda (_node parent &rest _) + (elixir-ts-mode--call-parent-start parent)) + 0) + ((node-is "^else_block$") grand-parent 0) + ((node-is "^catch_block$") grand-parent 0) + ((node-is "^rescue_block$") grand-parent 0) + ((node-is "^after_block$") grand-parent 0) + ((parent-is "^else_block$") parent ,offset) + ((parent-is "^catch_block$") parent ,offset) + ((parent-is "^rescue_block$") parent ,offset) + ((parent-is "^rescue_block$") parent ,offset) + ((parent-is "^after_block$") parent ,offset) + ((parent-is "^tuple$") parent-bol ,offset) + ((parent-is "^list$") parent-bol ,offset) + ((parent-is "^pair$") parent ,offset) + ((parent-is "^map_content$") parent-bol 0) + ((parent-is "^map$") parent-bol ,offset) + ((node-is "^stab_clause$") parent-bol ,offset) + ((query ,elixir-ts-mode--capture-operator-parent) grand-parent 0) + ((node-is "^when$") parent 0) + ((node-is "^keywords$") parent-bol ,offset) + ((parent-is "^body$") + (lambda (node parent _) + (save-excursion + ;; the grammar adds a comment outside of the body, so we have to indent + ;; to the grand-parent if it is available + (goto-char (treesit-node-start + (or (treesit-node-parent parent) (parent)))) + (back-to-indentation) + (point))) + ,offset) + ((parent-is "^arguments$") + ;; the first argument must indent ,offset from start of call + ;; otherwise indent should be the same as the first argument + (lambda (node parent bol &rest _) + (let ((first-child (treesit-node-child parent 0 t))) + (cond ((null first-child) + (elixir-ts-mode--call-parent-start parent)) + ((treesit-node-eq node first-child) + (elixir-ts-mode--call-parent-start parent)) + (t (elixir-ts-mode--call-parent-start parent))))) + (lambda (node parent rest) + ;; if first-child offset otherwise don't + (let ((first-child (treesit-node-child parent 0 t))) + (cond ((null first-child) ,offset) + ((treesit-node-eq node first-child) ,offset) + (t 0))))) + ;; handle incomplete maps when parent is ERROR + ((n-p-gp "^binary_operator$" "ERROR" nil) parent-bol 0) + ;; When there is an ERROR, just indent to prev-line + ;; Not sure why it needs one more, but adding it for now + ((parent-is "ERROR") prev-line 1) + ((node-is "^binary_operator$") + (lambda (node parent &rest _) + (let ((top-level + (treesit-parent-while + node + (lambda (node) + (equal (treesit-node-type node) + "binary_operator"))))) + (if (treesit-node-eq top-level node) + (elixir-ts-mode--call-parent-start parent) + (treesit-node-start top-level)))) + (lambda (node parent _) + (cond + ((equal (treesit-node-type parent) "do_block") + ,offset) + ((equal (treesit-node-type parent) "binary_operator") + ,offset) + (t 0)))) + ((parent-is "^binary_operator$") + (lambda (node parent bol &rest _) + (treesit-node-start + (treesit-parent-while + parent + (lambda (node) + (equal (treesit-node-type node) "binary_operator"))))) + ,offset) + ((node-is "^pair$") first-sibling 0) + ((query ,elixir-ts-mode--capture-anonymous-function-end) parent-bol 0) + ((node-is "^end$") + (lambda (_node parent &rest _) + (elixir-ts-mode--call-parent-start parent)) 0) + ((parent-is "^do_block$") grand-parent ,offset) + ((parent-is "^anonymous_function$") + elixir-ts-mode--treesit-anchor-grand-parent-bol ,offset) + ((parent-is "^else_block$") parent ,offset) + ((parent-is "^rescue_block$") parent ,offset) + ((parent-is "^catch_block$") parent ,offset) + ((parent-is "^keywords$") parent-bol 0) + ((node-is "^call$") parent-bol ,offset) + ((node-is "^comment$") parent-bol ,offset))))) + +(defvar elixir-ts-mode--font-lock-settings + (treesit-font-lock-rules + :language 'elixir + :feature 'elixir-comment + '((comment) @font-lock-comment-face) + + :language 'elixir + :feature 'elixir-string + :override t + '([(string) (charlist)] @font-lock-string-face) + + :language 'elixir + :feature 'elixir-string-interpolation + :override t + '((string + [ + quoted_end: _ @font-lock-string-face + quoted_start: _ @font-lock-string-face + (quoted_content) @font-lock-string-face + (interpolation + "#{" @font-lock-regexp-grouping-backslash "}" + @font-lock-regexp-grouping-backslash) + ]) + (charlist + [ + quoted_end: _ @font-lock-string-face + quoted_start: _ @font-lock-string-face + (quoted_content) @font-lock-string-face + (interpolation + "#{" @font-lock-regexp-grouping-backslash "}" + @font-lock-regexp-grouping-backslash) + ])) + + :language 'elixir + :feature 'elixir-keyword + ;; :override `prepend + `(,elixir-ts-mode--reserved-keywords-vector + @font-lock-keyword-face + ;; these are operators, should we mark them as keywords? + (binary_operator + operator: _ @font-lock-keyword-face + (:match ,elixir-ts-mode--reserved-keywords-re @font-lock-keyword-face))) + + :language 'elixir + :feature 'elixir-doc + :override t + `((unary_operator + operator: "@" @elixir-ts-font-comment-doc-attribute-face + operand: (call + target: (identifier) @elixir-ts-font-comment-doc-identifier-face + ;; Arguments can be optional, so adding another + ;; entry without arguments. + ;; If we don't handle then we don't apply font + ;; and the non doc fortification query will take specify + ;; a more specific font which takes precedence. + (arguments + [ + (string) @font-lock-doc-face + (charlist) @font-lock-doc-face + (sigil) @font-lock-doc-face + (boolean) @font-lock-doc-face + ])) + (:match ,elixir-ts-mode--doc-keywords-re + @elixir-ts-font-comment-doc-identifier-face)) + (unary_operator + operator: "@" @elixir-ts-font-comment-doc-attribute-face + operand: (call + target: (identifier) @elixir-ts-font-comment-doc-identifier-face) + (:match ,elixir-ts-mode--doc-keywords-re + @elixir-ts-font-comment-doc-identifier-face))) + + :language 'elixir + :feature 'elixir-unary-operator + `((unary_operator operator: "@" @font-lock-preprocessor-face + operand: [ + (identifier) @font-lock-preprocessor-face + (call target: (identifier) + @font-lock-preprocessor-face) + (boolean) @font-lock-preprocessor-face + (nil) @font-lock-preprocessor-face + ]) + + (unary_operator operator: "&") @font-lock-function-name-face + (operator_identifier) @font-lock-operator-face) + + :language 'elixir + :feature 'elixir-operator + '((binary_operator operator: _ @font-lock-operator-face) + (dot operator: _ @font-lock-operator-face) + (stab_clause operator: _ @font-lock-operator-face) + + [(boolean) (nil)] @font-lock-constant-face + [(integer) (float)] @font-lock-number-face + (alias) @font-lock-type-face + (call target: (dot left: (atom) @font-lock-type-face)) + (char) @font-lock-constant-face + [(atom) (quoted_atom)] @font-lock-type-face + [(keyword) (quoted_keyword)] @font-lock-builtin-face) + + :language 'elixir + :feature 'elixir-call + `((call + target: (identifier) @font-lock-keyword-face + (:match ,elixir-ts-mode--definition-keywords-re @font-lock-keyword-face)) + (call + target: (identifier) @font-lock-keyword-face + (:match ,elixir-ts-mode--kernel-keywords-re @font-lock-keyword-face)) + (call + target: [(identifier) @font-lock-function-name-face + (dot right: (identifier) @font-lock-keyword-face)]) + (call + target: (identifier) @font-lock-keyword-face + (arguments + [ + (identifier) @font-lock-keyword-face + (binary_operator + left: (identifier) @font-lock-keyword-face + operator: "when") + ]) + (:match ,elixir-ts-mode--definition-keywords-re @font-lock-keyword-face)) + (call + target: (identifier) @font-lock-keyword-face + (arguments + (binary_operator + operator: "|>" + right: (identifier))) + (:match ,elixir-ts-mode--definition-keywords-re @font-lock-keyword-face))) + + :language 'elixir + :feature 'elixir-constant + `((binary_operator operator: "|>" right: (identifier) + @font-lock-function-name-face) + ((identifier) @font-lock-keyword-face + (:match ,elixir-ts-mode--builtin-keywords-re + @font-lock-keyword-face)) + ((identifier) @font-lock-comment-face + (:match "^_" @font-lock-comment-face)) + (identifier) @font-lock-function-name-face + ["%"] @font-lock-keyward-face + ["," ";"] @font-lock-keyword-face + ["(" ")" "[" "]" "{" "}" "<<" ">>"] @font-lock-keyword-face) + + :language 'elixir + :feature 'elixir-sigil + :override t + `((sigil + (sigil_name) @elixir-ts-font-sigil-name-face + quoted_start: _ @font-lock-string-face + quoted_end: _ @font-lock-string-face + (:match "^[sSwWpP]$" @elixir-ts-font-sigil-name-face)) + @font-lock-string-face + (sigil + (sigil_name) @elixir-ts-font-sigil-name-face + quoted_start: _ @font-lock-regex-face + quoted_end: _ @font-lock-regex-face + (:match "^[rR]$" @elixir-ts-font-sigil-name-face)) + @font-lock-regex-face + (sigil + "~" @font-lock-string-face + (sigil_name) @elixir-ts-font-sigil-name-face + quoted_start: _ @font-lock-string-face + quoted_end: _ @font-lock-string-face + (:match "^[HF]$" @elixir-ts-font-sigil-name-face))) + + :language 'elixir + :feature 'elixir-string-escape + :override t + `((escape_sequence) @font-lock-regexp-grouping-backslash)) + "Tree-sitter font-lock settings.") + +(defun elixir-ts-mode--forward-sexp (&optional arg) + (interactive "^p") + (or arg (setq arg 1)) + (funcall + (if (> arg 0) #'treesit-end-of-thing #'treesit-beginning-of-thing) + (if (eq (treesit-language-at (point)) 'heex) + heex-ts-mode-sexp-regexp + elixir-ts-mode-sexp-regexp) + (abs arg))) + +(defun elixir-ts-mode--treesit-anchor-grand-parent-bol (_n parent &rest _) + "Return the beginning of non-space characters for the parent node of PARENT." + (save-excursion + (goto-char (treesit-node-start (treesit-node-parent parent))) + (back-to-indentation) + (point))) + +(defvar elixir-ts-mode--treesit-range-rules + (when (treesit-available-p) + (treesit-range-rules + :embed 'heex + :host 'elixir + '((sigil (sigil_name) @name (:match "^[HF]$" @name) (quoted_content) @heex))))) + +(defun elixir-ts-mode--treesit-language-at-point (point) + "Return the language at POINT." + (let* ((range nil) + (language-in-range + (cl-loop + for parser in (treesit-parser-list) + do (setq range + (cl-loop + for range in (treesit-parser-included-ranges parser) + if (and (>= point (car range)) (<= point (cdr range))) + return parser)) + if range + return (treesit-parser-language parser)))) + (if (null language-in-range) + (when-let ((parser (car (treesit-parser-list)))) + (treesit-parser-language parser)) + language-in-range))) + +(defun elixir-ts-mode--defun-p (node) + "Return non-nil when NODE is a defun." + (member (treesit-node-text + (treesit-node-child-by-field-name node "target")) + (append + elixir-ts-mode--definition-keywords + elixir-ts-mode--test-definition-keywords))) + +(defun elixir-ts-mode--defun-name (node) + "Return the name of the defun NODE. +Return nil if NODE is not a defun node or doesn't have a name." + (pcase (treesit-node-type node) + ("call" (let ((node-child + (treesit-node-child (treesit-node-child node 1) 0))) + (pcase (treesit-node-type node-child) + ("alias" (treesit-node-text node-child t)) + ("call" (treesit-node-text + (treesit-node-child-by-field-name node-child "target") t)) + ("binary_operator" + (treesit-node-text + (treesit-node-child-by-field-name + (treesit-node-child-by-field-name node-child "left") "target") t)) + ("identifier" + (treesit-node-text node-child t)) + (_ nil)))) + (_ nil))) + +;;;###autoload +(define-derived-mode elixir-ts-mode prog-mode "Elixir" + "Major mode for editing Elixir, powered by tree-sitter." + :group 'elixir-ts + :syntax-table elixir-ts-mode--syntax-table + + ;; Comments + (setq-local comment-start "# ") + (setq-local comment-start-skip + (rx "#" (* (syntax whitespace)))) + + (setq-local comment-end "") + (setq-local comment-end-skip + (rx (* (syntax whitespace)) + (group (or (syntax comment-end) "\n")))) + + ;; Compile + (setq-local compile-command "mix") + + (when (treesit-ready-p 'elixir) + ;; heex has to be created first for elixir to ensure elixir + ;; is the first language when looking for treesit ranges + (if (treesit-ready-p 'heex) + (treesit-parser-create 'heex)) + + (treesit-parser-create 'elixir) + + (setq-local treesit-language-at-point-function + 'elixir-ts-mode--treesit-language-at-point) + + ;; Font-lock. + (setq-local treesit-font-lock-settings elixir-ts-mode--font-lock-settings) + (setq-local treesit-font-lock-feature-list + '(( elixir-comment elixir-constant elixir-doc ) + ( elixir-string elixir-keyword elixir-unary-operator + elixir-call elixir-operator ) + ( elixir-sigil elixir-string-escape elixir-string-interpolation))) + + ;; Imenu. + (setq-local treesit-simple-imenu-settings + '((nil "\\`call\\'" elixir-ts-mode--defun-p nil))) + + ;; Indent. + (setq-local treesit-simple-indent-rules elixir-ts-mode--indent-rules) + + ;; Navigation + (setq-local forward-sexp-function #'elixir-ts-mode--forward-sexp) + (setq-local treesit-defun-type-regexp + '("call" . elixir-ts-mode--defun-p)) + + (setq-local treesit-defun-name-function #'elixir-ts-mode--defun-name) + + ;; Embedded Heex + (when (treesit-ready-p 'heex) + (setq-local treesit-range-settings elixir-ts-mode--treesit-range-rules) + + (setq-local treesit-simple-indent-rules + (append treesit-simple-indent-rules heex-ts-mode--indent-rules)) + + (setq-local treesit-font-lock-settings + (append treesit-font-lock-settings + heex-ts-mode--font-lock-settings)) + + (setq-local treesit-simple-indent-rules + (append treesit-simple-indent-rules + heex-ts-mode--indent-rules)) + + (setq-local treesit-font-lock-feature-list + '(( elixir-comment elixir-constant elixir-doc + heex-comment heex-keyword heex-doctype ) + ( elixir-string elixir-keyword elixir-unary-operator + elixir-call elixir-operator + heex-component heex-tag heex-attribute heex-string) + ( elixir-sigil elixir-string-escape + elixir-string-interpolation )))) + + (treesit-major-mode-setup))) + +(if (treesit-ready-p 'elixir) + (progn + (add-to-list 'auto-mode-alist '("\\.elixir\\'" . elixir-ts-mode)) + (add-to-list 'auto-mode-alist '("\\.ex\\'" . elixir-ts-mode)) + (add-to-list 'auto-mode-alist '("\\.exs\\'" . elixir-ts-mode)) + (add-to-list 'auto-mode-alist '("mix\\.lock" . elixir-ts-mode)))) + +(provide 'elixir-ts-mode) +;;; elixir-ts-mode.el ends here diff --git a/test/lisp/progmodes/elixir-ts-mode-resources/indent.erts b/test/lisp/progmodes/elixir-ts-mode-resources/indent.erts new file mode 100644 index 00000000000..dfb50319bf8 --- /dev/null +++ b/test/lisp/progmodes/elixir-ts-mode-resources/indent.erts @@ -0,0 +1,147 @@ +Code: + (lambda () + (setq indent-tabs-mode nil) + (elixir-ts-mode) + (indent-region (point-min) (point-max))) + +Point-Char: $ + +Name: Basic modules + +=-= + defmodule Foobar do +def bar() do +"one" + end + end +=-= +defmodule Foobar do + def bar() do + "one" + end +end +=-=-= + + +Name: Map + +=-= +map = %{ + "a" => 1, + "b" => 2 +} +=-=-= + +Name: Block assignments + +=-= +foo = + if true do + "yes" + else + "no" + end +=-=-= + +Name: Function rescue + +=-= +def foo do + "bar" +rescue + e -> + "bar" +end +=-=-= + + +Name: Pipe statements with fn + +=-= +[1, 2] +|> Enum.map(fn num -> + num + 1 +end) +=-=-= + +Name: Binary operator in else block + +=-= +defp foobar() do + if false do + :foo + else + :bar |> foo + end +end +=-=-= + +Name: Tuple indentation + +=-= +tuple = { + :one, + :two +} + +{ + :one, + :two +} +=-=-= + +Name: String concatenation in call + +=-= +IO.warn( + "one" <> + "two" <> + "bar" +) + +IO.warn( + "foo" <> + "bar" +) +=-=-= + +Name: Incomplete tuple + +=-= +map = { +:foo + +=-= +map = { + :foo + +=-=-= + +Name: Incomplete map + +=-= +map = %{ + "a" => "a", +=-=-= + +Name: Incomplete list + +=-= +map = [ +:foo + +=-= +map = [ + :foo + +=-=-= + + +Name: String concatenation + +=-= +"one" <> + "two" <> + "three" <> + "four" +=-=-= diff --git a/test/lisp/progmodes/elixir-ts-mode-tests.el b/test/lisp/progmodes/elixir-ts-mode-tests.el new file mode 100644 index 00000000000..8e546ad5cc6 --- /dev/null +++ b/test/lisp/progmodes/elixir-ts-mode-tests.el @@ -0,0 +1,31 @@ +;;; c-ts-mode-tests.el --- Tests for Tree-sitter-based C mode -*- lexical-binding: t; -*- + +;; Copyright (C) 2023 Free Software Foundation, Inc. + +;; This file is part of GNU Emacs. + +;; GNU Emacs is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; GNU Emacs is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with GNU Emacs. If not, see . + +;;; Code: + +(require 'ert) +(require 'ert-x) +(require 'treesit) + +(ert-deftest elixir-ts-mode-test-indentation () + (skip-unless (and (treesit-ready-p 'elixir) (treesit-ready-p 'heex))) + (ert-test-erts-file (ert-resource-file "indent.erts"))) + +(provide 'elixir-ts-mode-tests) +;;; elixir-ts-mode-tests.el ends here -- 2.39.2 --=-=-=--