From unknown Sun Jun 22 22:41:43 2025 Content-Disposition: inline Content-Transfer-Encoding: quoted-printable MIME-Version: 1.0 X-Mailer: MIME-tools 5.509 (Entity 5.509) Content-Type: text/plain; charset=utf-8 From: bug#61996 <61996@debbugs.gnu.org> To: bug#61996 <61996@debbugs.gnu.org> Subject: Status: 30.0.50; Submitting elixir-ts-mode and heex-ts-mode Reply-To: bug#61996 <61996@debbugs.gnu.org> Date: Mon, 23 Jun 2025 05:41:43 +0000 retitle 61996 30.0.50; Submitting elixir-ts-mode and heex-ts-mode reassign 61996 emacs submitter 61996 Wilhelm Kirschbaum severity 61996 normal thanks From debbugs-submit-bounces@debbugs.gnu.org Mon Mar 06 02:26:07 2023 Received: (at submit) by debbugs.gnu.org; 6 Mar 2023 07:26:07 +0000 Received: from localhost ([127.0.0.1]:41216 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1pZ5EU-0002Nl-47 for submit@debbugs.gnu.org; Mon, 06 Mar 2023 02:26:07 -0500 Received: from lists.gnu.org ([209.51.188.17]:50666) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1pZ5ER-0002Nd-8O for submit@debbugs.gnu.org; Mon, 06 Mar 2023 02:26:04 -0500 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 1pZ5EP-0006cB-HN for bug-gnu-emacs@gnu.org; Mon, 06 Mar 2023 02:26:01 -0500 Received: from mail-wm1-x333.google.com ([2a00:1450:4864:20::333]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_128_GCM_SHA256:128) (Exim 4.90_1) (envelope-from ) id 1pZ5EL-0008Oe-Qm for bug-gnu-emacs@gnu.org; Mon, 06 Mar 2023 02:26:01 -0500 Received: by mail-wm1-x333.google.com with SMTP id o38-20020a05600c512600b003e8320d1c11so5091087wms.1 for ; Sun, 05 Mar 2023 23:25:57 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20210112; t=1678087555; h=mime-version:message-id:cc:date:subject:to:from:user-agent:from:to :cc:subject:date:message-id:reply-to; bh=IU7zD00wk5uNqQdPXwyz6XxCHIxZlIyS4n0yMIzxohs=; b=RqK3bM/rWXBi+td4Hw5+Kw2tuPmCAkh9lwxajW4EOjLX+cCJjXawnUjpKQnBznVSk7 JGmG8VlCrwefTwXFIrNLW4HukG6sd0Urp29DGEOt5n2BCkR6FeWVOMTR27IX88BNRdsA oPCEcVmA7lTltBgdUipHOl3Dk2aNWA3ZAkZrf1uXJkiUTOAAdaa0nua3FHOPf6S4AlZm VfLA6CLCO1opEnezdt+EpU+g/QzPR2+DyNK3hSmgh0iD4rtap8AeO4ofaQrpX8jBhGar EkxSwISf+1V18hOIw8T90e4AlWe0gFulzNseKz4j+0J6YpVxL/cuNYSwooaXfDtySS9q hcwA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20210112; t=1678087555; h=mime-version:message-id:cc:date:subject:to:from:user-agent :x-gm-message-state:from:to:cc:subject:date:message-id:reply-to; bh=IU7zD00wk5uNqQdPXwyz6XxCHIxZlIyS4n0yMIzxohs=; b=ykMILMLFs9LC/RYS3qMvwdpdGHAkwWnYZSZlqpMcRXFb04XuBQd3VqXqwSTzSo81Po SQo7FI+7ZiPlg1DqJMIbSqUgopRimA2YbIVIUPvxzmSues702qV3/UBGL2DaJhJfnIn5 E/DD8WcjhkwRsr+X0WzQo8E+LQ9WVrZAJzIFLHtfPb++kX/1Fu60smu1jpf/lCWv+P1I +sWMrJrFX8U9k45P53OKfL3tODWAmn2GYQFNuPe8EWPj9gOC9qsduyjZjbmX4BI3/cI+ KD+cENduTPFo/W2d3t2SgM+5Qr3bsyjuuM54wBELkSczQgI0PIrJEH/yV5ZWdieeHdYM taZA== X-Gm-Message-State: AO0yUKV8RU2Ejz2xKjeCukJRY3AJAl8c3SIdG4Dt9Hk8Wj/voOcjF0aR HoUBNKrE+bqg4l6gq+FGYVs= X-Google-Smtp-Source: AK7set9rGf6g6b3fX309/AXyo/8qWplfsvFZLjRJSOWPH0ixN3x1icEQh6UFiSmBOu8iJwEtxZV7DQ== X-Received: by 2002:a05:600c:35c8:b0:3eb:3692:6450 with SMTP id r8-20020a05600c35c800b003eb36926450mr8967312wmq.18.1678087555448; Sun, 05 Mar 2023 23:25:55 -0800 (PST) Received: from melissa.local ([2c0f:ef18:1431:0:b09:9616:db04:c248]) by smtp.gmail.com with ESMTPSA id z17-20020a5d44d1000000b002c58ca558b6sm8986500wrr.88.2023.03.05.23.25.53 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Sun, 05 Mar 2023 23:25:54 -0800 (PST) User-agent: mu4e 1.9.3; emacs 30.0.50 From: Wilhelm Kirschbaum To: bug-gnu-emacs@gnu.org Subject: 30.0.50; Submitting elixir-ts-mode and heex-ts-mode Date: Mon, 06 Mar 2023 09:04:13 +0200 Message-ID: <87mt4qibnk.fsf@gmail.com> MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="=-=-=" Received-SPF: pass client-ip=2a00:1450:4864:20::333; envelope-from=wkirschbaum@gmail.com; helo=mail-wm1-x333.google.com X-Spam_score_int: -20 X-Spam_score: -2.1 X-Spam_bar: -- X-Spam_report: (-2.1 / 5.0 requ) BAYES_00=-1.9, DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1, FREEMAIL_FROM=0.001, RCVD_IN_DNSWL_NONE=-0.0001, SPF_HELO_NONE=0.001, SPF_PASS=-0.001 autolearn=ham autolearn_force=no X-Spam_action: no action X-Spam-Score: -1.3 (-) X-Debbugs-Envelope-To: submit Cc: casouri@gmail.com, theo@thornhill.no X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: debbugs-submit-bounces@debbugs.gnu.org Sender: "Debbugs-submit" X-Spam-Score: -2.3 (--) --=-=-= Content-Type: text/plain; format=flowed I would like to submit elixir-ts-mode and heex-ts-mode to emacs master. The package elixir-ts-mode and its dependency heex-ts-mode is currently a melpa package: https://melpa.org/#/elixir-ts-mode. This is a slightly simplified version, also authored by me. There is one change not authored by me: https://github.com/wkirschbaum/elixir-ts-mode/commit/21ad74877ebb55f4bf0b31c2f463bbfda72590ef which is a duplication removal. I completed the assignment process in Jan. --=-=-= Content-Type: text/x-patch Content-Disposition: attachment; filename=0001-Add-heex-ts-mode.patch Content-Description: Add heex-ts-mode >From 2c31157207986aacf00d5a8405de09011cbb7d14 Mon Sep 17 00:00:00 2001 From: Wilhelm H Kirschbaum Date: Sun, 5 Mar 2023 16:45:39 +0200 Subject: [PATCH 1/2] Add heex-ts-mode --- lisp/progmodes/heex-ts-mode.el | 182 +++++++++++++++++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 lisp/progmodes/heex-ts-mode.el diff --git a/lisp/progmodes/heex-ts-mode.el b/lisp/progmodes/heex-ts-mode.el new file mode 100644 index 00000000000..e0e879a4b53 --- /dev/null +++ b/lisp/progmodes/heex-ts-mode.el @@ -0,0 +1,182 @@ +;;; 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 defines heex-ts-mode which is a major mode for editing +;; Elixir and Heex files. + +;;; 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 "29.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))) + +;; this is a problem when requiring from elixir-ts-mode, so moving there +;; for now. +;; (if (treesit-ready-p 'heex) +;; (add-to-list 'auto-mode-alist '("\\.[hl]?eex\\'" . heex-ts-mode))) + +(provide 'heex-ts-mode) +;;; heex-ts-mode.el ends here -- 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 a1e7a754aa5cd6cd69e50913e3412e5c77a6505e Mon Sep 17 00:00:00 2001 From: Wilhelm H Kirschbaum Date: Sun, 5 Mar 2023 16:45:54 +0200 Subject: [PATCH 2/2] Add elixir-ts-mode --- lisp/progmodes/elixir-ts-mode.el | 626 +++++++++++++++++++++++++++++++ 1 file changed, 626 insertions(+) create mode 100644 lisp/progmodes/elixir-ts-mode.el diff --git a/lisp/progmodes/elixir-ts-mode.el b/lisp/progmodes/elixir-ts-mode.el new file mode 100644 index 00000000000..2bf525c22f2 --- /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 defines elixir-ts-mode which is a major mode for editing +;; Elixir and Heex files. + +;; 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 "29.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$") point-min 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)))) + +(if (treesit-ready-p 'heex) + (add-to-list 'auto-mode-alist '("\\.[hl]?eex\\'" . heex-ts-mode))) + +(provide 'elixir-ts-mode) + +;;; elixir-ts-mode.el ends here -- 2.39.2 --=-=-=-- From debbugs-submit-bounces@debbugs.gnu.org Mon Mar 06 06:59:20 2023 Received: (at 61996) by debbugs.gnu.org; 6 Mar 2023 11:59:20 +0000 Received: from localhost ([127.0.0.1]:41533 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1pZ9Ut-000129-St for submit@debbugs.gnu.org; Mon, 06 Mar 2023 06:59:20 -0500 Received: from eggs.gnu.org ([209.51.188.92]:39996) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1pZ9Ur-00011x-TJ for 61996@debbugs.gnu.org; Mon, 06 Mar 2023 06:59:18 -0500 Received: from fencepost.gnu.org ([2001:470:142:3::e]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1pZ9Ul-00041f-Nm; Mon, 06 Mar 2023 06:59:11 -0500 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=gnu.org; s=fencepost-gnu-org; h=References:Subject:In-Reply-To:To:From:Date: mime-version; bh=UaSquXPagLrFZUM/0jD6f9va2PMRCA8sIMq1eWYXJvA=; b=WHFNoJdw3w1C r8kt6xgg3QBoguVMV3+1kcd2Ok85wimD4d/TtdfRA58DPg84TFnZZ43em08OXebp4V/c6wfNdoZh/ w4QDFhkqBVJh18eLOq15ofcyfnDvjmTBLBOgKOPfxnqv00b6veHXUXgpZHlfeHWqNv2vaVzcrkmMk O+XOv4S6MAb2IVSPi+wBxpFKumeLr6aFtbvA7xn4GLWa381XNccqKq5WiNQNDKHohV4C46SI6HwOh p4zKp5gZUK8NDHCGTdyI5HRCv/D/H1sAEjQlv5rBi6BUtHnIUFdVghhfAmPYOcgZz2OUdSHlAVX1W WAL1Rq99qKS1cFAzCDUOPg==; Received: from [87.69.77.57] (helo=home-c4e4a596f7) by fencepost.gnu.org with esmtpsa (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1pZ9Uj-0000QU-UD; Mon, 06 Mar 2023 06:59:11 -0500 Date: Mon, 06 Mar 2023 13:59:02 +0200 Message-Id: <83cz5m8515.fsf@gnu.org> From: Eli Zaretskii To: Wilhelm Kirschbaum In-Reply-To: <87mt4qibnk.fsf@gmail.com> (message from Wilhelm Kirschbaum on Mon, 06 Mar 2023 09:04:13 +0200) Subject: Re: bug#61996: 30.0.50; Submitting elixir-ts-mode and heex-ts-mode References: <87mt4qibnk.fsf@gmail.com> X-Spam-Score: -2.3 (--) X-Debbugs-Envelope-To: 61996 Cc: 61996@debbugs.gnu.org, theo@thornhill.no, casouri@gmail.com X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: debbugs-submit-bounces@debbugs.gnu.org Sender: "Debbugs-submit" X-Spam-Score: -3.3 (---) > Cc: casouri@gmail.com, theo@thornhill.no > From: Wilhelm Kirschbaum > Date: Mon, 06 Mar 2023 09:04:13 +0200 > > I would like to submit elixir-ts-mode and heex-ts-mode to emacs > master. Thanks. Please state in the comments to each mode with which grammars is it compatible, so that users could know from which URL to download the required grammar libraries. The heex-ts-mode mentions that, but elixir-ts-mode doesn't, AFAICT. > +(defcustom heex-ts-mode-indent-offset 2 > + "Indentation of Heex statements." > + :version "29.1" I think these modes should go to the master branch, so "30.1" is more accurate. > +(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. From debbugs-submit-bounces@debbugs.gnu.org Mon Mar 06 11:42:06 2023 Received: (at 61996) by debbugs.gnu.org; 6 Mar 2023 16:42:06 +0000 Received: from localhost ([127.0.0.1]:43450 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1pZDuY-0005Tq-2a for submit@debbugs.gnu.org; Mon, 06 Mar 2023 11:42:06 -0500 Received: from mail-wr1-f52.google.com ([209.85.221.52]:42878) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1pZDuW-0005TM-TT for 61996@debbugs.gnu.org; Mon, 06 Mar 2023 11:42:05 -0500 Received: by mail-wr1-f52.google.com with SMTP id j2so9478703wrh.9 for <61996@debbugs.gnu.org>; Mon, 06 Mar 2023 08:42:04 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20210112; t=1678120919; h=content-transfer-encoding:in-reply-to:from:references:cc:to :content-language:subject:user-agent:mime-version:date:message-id :sender:from:to:cc:subject:date:message-id:reply-to; bh=I6+Z2rp4zrYyrV/cnDouKfNMHtCORGZfclYDlPEmEo0=; b=JShs53hgyEvf4a0qFjCKky6xt6drsdVQL/4HGeYq72oCBH7nKkjFV+f8EKIT9uFJYU o55T9kKkXKac+nMM5umIQcqdsQ4GR0JaR++b1HjLGgyIsSS1uGCZu9btyahYd3iq9BDm X5iyb6Q2v5e3SZvsns6cLbAYbwRrrJCyo+aFChsbK3Qj2Gvs+iuPK6UIEGmEJQsFmOqD NEPEzO0Ci53muTLh1VKLcwY3wRtu7zCoeNLR6ZDF1NCj9f/eib1cLQMAjweN/v1bAsDN G5ukKRqlhA7DVe7bd/UP7FNCDRk/IYEG7E7ha6uczHFcF07EUQd8uCLle+umzdasazw2 UPSQ== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20210112; t=1678120919; h=content-transfer-encoding:in-reply-to:from:references:cc:to :content-language:subject:user-agent:mime-version:date:message-id :sender:x-gm-message-state:from:to:cc:subject:date:message-id :reply-to; bh=I6+Z2rp4zrYyrV/cnDouKfNMHtCORGZfclYDlPEmEo0=; b=iodLHT2wBMebao6IGpsnPfLuS7+1D8reucDd4ZKtwtgvyHnw2lZoPpPQUjcrV3C32l QlHoq2ak5WG0xvTDYSfnAY2vUGWRdrJXHr2M3g0wmTGuvP5QGW3GrKWsfhFysR2zHe84 x+Mb4C6rgfveTNJRHp5Qj37Umyeql5npb9kvcr8dTARpVZcyHpOhzH5O0OEnbqYb1d9f XS/82DikRJf8Ju9diECAyo2HB2Aan1EpMhvpBS4t3a/l6BFNFpPNzDhWC2ha1m6Uq8eR 8kbMOe7dj+nbPAB0GJnypxSNbdz9kPKCsd0xVtOA6J67sz1Qz+n7HbZhD596yDLPlghS ROEw== X-Gm-Message-State: AO0yUKU5JQHi8hUGsrF/R+uFDAZo78peUV8G1xed4qaMgfPKVpBLQE+e 7h0+br8znr0a+vOJEAl/r9M= X-Google-Smtp-Source: AK7set9Z9SCEydJYzLPsdUfXwpMYlMybrNQGaj75WQfRnr2OTIKzQdKm0pds3rVkv6nbLjvYMtLkAA== X-Received: by 2002:a5d:6a0a:0:b0:2c7:fde:f7e0 with SMTP id m10-20020a5d6a0a000000b002c70fdef7e0mr7040401wru.65.1678120918789; Mon, 06 Mar 2023 08:41:58 -0800 (PST) Received: from [192.168.0.2] ([46.251.119.176]) by smtp.googlemail.com with ESMTPSA id s17-20020a5d4ed1000000b002c5584d0e3dsm10098787wrv.24.2023.03.06.08.41.57 (version=TLS1_3 cipher=TLS_AES_128_GCM_SHA256 bits=128/128); Mon, 06 Mar 2023 08:41:58 -0800 (PST) Message-ID: <8296ddf3-ba04-0e68-a208-45b0b0dc4df8@yandex.ru> Date: Mon, 6 Mar 2023 18:41:56 +0200 MIME-Version: 1.0 User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Thunderbird/102.7.1 Subject: Re: bug#61996: 30.0.50; Submitting elixir-ts-mode and heex-ts-mode Content-Language: en-US To: Wilhelm Kirschbaum , 61996@debbugs.gnu.org References: <87mt4qibnk.fsf@gmail.com> From: Dmitry Gutov In-Reply-To: <87mt4qibnk.fsf@gmail.com> Content-Type: text/plain; charset=UTF-8; format=flowed Content-Transfer-Encoding: 7bit X-Spam-Score: -0.9 (/) X-Debbugs-Envelope-To: 61996 Cc: casouri@gmail.com, theo@thornhill.no X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: debbugs-submit-bounces@debbugs.gnu.org Sender: "Debbugs-submit" X-Spam-Score: -1.9 (-) Hi! On 06/03/2023 09:04, Wilhelm Kirschbaum wrote: > I would like to submit elixir-ts-mode and heex-ts-mode to emacs master. It would be great if you could accompany it with some testing suite: indentation code is famously prone to regressions. You can see the examples of such tests for c-ts-mode or ruby-ts-mode (they use different approaches). From debbugs-submit-bounces@debbugs.gnu.org Mon Mar 06 12:49:25 2023 Received: (at 61996) by debbugs.gnu.org; 6 Mar 2023 17:49:25 +0000 Received: from localhost ([127.0.0.1]:43580 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1pZExh-0007Pm-1H for submit@debbugs.gnu.org; Mon, 06 Mar 2023 12:49:25 -0500 Received: from mail-wr1-f52.google.com ([209.85.221.52]:33594) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1pZExe-0007PX-PB for 61996@debbugs.gnu.org; Mon, 06 Mar 2023 12:49:23 -0500 Received: by mail-wr1-f52.google.com with SMTP id v16so9770762wrn.0 for <61996@debbugs.gnu.org>; Mon, 06 Mar 2023 09:49:22 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20210112; t=1678124957; 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=zaX5yF7zeGKNVbkR6HFpbNOoM0z1lYGwqiIDQ9Y36fY=; b=I6RTrTyAyAu7FSNRQf9l2bW6LDiaCyS8sPeUpBSzjVh9bZWYfCbgN2f7t2nmDBhjEo UzQ7xUyiJ8mixcxYCDFrDfs/Xdjr8VcyiRohRjbk53hLKHXz27YOiLenARqClz7kA9cr IAxC+Yk/V7Wb1US6XEkLerYmzbYSuTdJDYCh4EdzGwOwulOMePMa3HGLdzSxHTVtyH0f 00Sjv9WHtX5vW8V0Z4S0sGVR95dn9fm+UCawtUC2GzSHv/eRnL9IoJI/C6JttbnHfM/G PCoGIsD06ZvgP5lnD15NKhrldPYtz/CyMiZW6gkujaF7Tly0oHOGSvZtO4YrI7JInhM1 2AVw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20210112; t=1678124957; 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=zaX5yF7zeGKNVbkR6HFpbNOoM0z1lYGwqiIDQ9Y36fY=; b=XdsUlt4t573A6ApQ6ht17w12AdNglLjooQVUiY3sAInexlTnt9Vhf/cG9Nftozjtac y8jcDnb2jcZZ0Sj/knmJ3TeaDtOdRfga2d8O7sSZjAJE7kXYilKmk/O3ygG6v/rtrZ9a 5e3LfjKMafMIv9sIiposqfxxKx8p2WTJnh/pvDCYW606d+yC0IHfQpbqzibVZsygdQIx MFVHAogtOCKgDlkIMYhkInF9QAkj5hZHoPZcwqWVZVVJOqW1jZukHm3K+nU/ilkIeCkx 3Sq3JQWydR5Ow0H+LYa5yx6alLcXeiTUsZXm5C9Z5d7fn6jOXYRQ/288bhuZGqzMmm/7 zpKA== X-Gm-Message-State: AO0yUKXYS3so8X9XpmkLeBc1eFPPtfM88h5HgD8iflZSvKc5z80hl37v n5dhzDGugNThMJy1i2BSGpM= X-Google-Smtp-Source: AK7set/qFlxTNWM1il+S9oo9ZMClZO7ZnAxYxTG1oQIKNOBRhKysxTDZPWNBTvj2sDlHnNVP9Q0CJw== X-Received: by 2002:adf:f2cb:0:b0:2c9:867e:7b6f with SMTP id d11-20020adff2cb000000b002c9867e7b6fmr6412366wrp.3.1678124956704; Mon, 06 Mar 2023 09:49:16 -0800 (PST) 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 n16-20020a5d67d0000000b002c758fe9689sm10350066wrw.52.2023.03.06.09.49.14 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Mon, 06 Mar 2023 09:49:16 -0800 (PST) References: <87mt4qibnk.fsf@gmail.com> <83cz5m8515.fsf@gnu.org> User-agent: mu4e 1.9.3; emacs 30.0.50 From: Wilhelm Kirschbaum To: Eli Zaretskii Subject: Re: bug#61996: 30.0.50; Submitting elixir-ts-mode and heex-ts-mode Date: Mon, 06 Mar 2023 19:23:39 +0200 In-reply-to: <83cz5m8515.fsf@gnu.org> Message-ID: <874jqx3h47.fsf@gmail.com> MIME-Version: 1.0 Content-Type: text/plain; format=flowed X-Spam-Score: 1.0 (+) X-Debbugs-Envelope-To: 61996 Cc: 61996@debbugs.gnu.org, theo@thornhill.no, casouri@gmail.com X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: debbugs-submit-bounces@debbugs.gnu.org Sender: "Debbugs-submit" X-Spam-Score: -1.0 (-) Eli Zaretskii writes: >> Cc: casouri@gmail.com, theo@thornhill.no >> From: Wilhelm Kirschbaum >> Date: Mon, 06 Mar 2023 09:04:13 +0200 >> >> I would like to submit elixir-ts-mode and heex-ts-mode to emacs >> master. > > Thanks. Please state in the comments to each mode with which > grammars > is it compatible, so that users could know from which URL to > download > the required grammar libraries. The heex-ts-mode mentions that, > but > elixir-ts-mode doesn't, AFAICT. > Will this make sense in the Commentary section: "The tree-sitter grammar for Elixir can be downloaded from https://github.com/phoenixframework/tree-sitter-heex." >> +(defcustom heex-ts-mode-indent-offset 2 >> + "Indentation of Heex statements." >> + :version "29.1" > > I think these modes should go to the master branch, so "30.1" is > more > accurate. > Thanks, will change. >> +(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. 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? I will update the patch with the above changes including some tests. From debbugs-submit-bounces@debbugs.gnu.org Mon Mar 06 13:37:09 2023 Received: (at 61996) by debbugs.gnu.org; 6 Mar 2023 18:37:09 +0000 Received: from localhost ([127.0.0.1]:43616 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1pZFhs-00005G-Oe for submit@debbugs.gnu.org; Mon, 06 Mar 2023 13:37:09 -0500 Received: from eggs.gnu.org ([209.51.188.92]:37506) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1pZFhq-0008WO-Un for 61996@debbugs.gnu.org; Mon, 06 Mar 2023 13:37:08 -0500 Received: from fencepost.gnu.org ([2001:470:142:3::e]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1pZFhl-0003jh-4g; Mon, 06 Mar 2023 13:37:01 -0500 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=gnu.org; s=fencepost-gnu-org; h=References:Subject:In-Reply-To:To:From:Date: mime-version; bh=CaOB5414kqMxiHbpP3dxNzbZ9h/76o5skZenIuA/nhA=; b=iNnfGRI0rDlu IV3EHll+47TMR/s94ob2bDUOHav9zsCnyPZKpZxeip10uCV5MIu2XDnWG2E3k98axzREPstW+nkIL GsdkMTyUquzruXOEyN6QvC+4wO80QyrmPmoP9i3q1gdrfOCLuaM57aYOUvPgNeEK5/H762NWjIDh6 6zbuz1qGIWZFauUyAs59krEapOguxgTi7YncV2AYBqon7u6mRmcnRh/s8iMhBkrJUrND2oMfEnk0j 5AQf8PnKrtwDQSpwxAKZQv2qupABEL/GhNOmJvCECYcz5zgUL6ehrlG7UDOimP69+gKcB//G92W0k aONJuEjaXg89Dn6K5Qybbw==; Received: from [87.69.77.57] (helo=home-c4e4a596f7) by fencepost.gnu.org with esmtpsa (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1pZFhk-0001nr-HR; Mon, 06 Mar 2023 13:37:00 -0500 Date: Mon, 06 Mar 2023 20:36:54 +0200 Message-Id: <83lek97mm1.fsf@gnu.org> From: Eli Zaretskii To: Wilhelm Kirschbaum In-Reply-To: <874jqx3h47.fsf@gmail.com> (message from Wilhelm Kirschbaum on Mon, 06 Mar 2023 19:23:39 +0200) Subject: Re: bug#61996: 30.0.50; Submitting elixir-ts-mode and heex-ts-mode References: <87mt4qibnk.fsf@gmail.com> <83cz5m8515.fsf@gnu.org> <874jqx3h47.fsf@gmail.com> X-Spam-Score: -2.3 (--) X-Debbugs-Envelope-To: 61996 Cc: 61996@debbugs.gnu.org, theo@thornhill.no, casouri@gmail.com X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: debbugs-submit-bounces@debbugs.gnu.org Sender: "Debbugs-submit" X-Spam-Score: -3.3 (---) > From: Wilhelm Kirschbaum > Cc: 61996@debbugs.gnu.org, casouri@gmail.com, theo@thornhill.no > Date: Mon, 06 Mar 2023 19:23:39 +0200 > > > Eli Zaretskii writes: > > > Thanks. Please state in the comments to each mode with which > > grammars is it compatible, so that users could know from which URL > > to download the required grammar libraries. The heex-ts-mode > > mentions that, but elixir-ts-mode doesn't, AFAICT. > > Will this make sense in the Commentary section: "The > tree-sitter grammar for Elixir can be downloaded from > https://github.com/phoenixframework/tree-sitter-heex." Yes, that's good enough. But please change the wording to say that this is the grammar with which the package was tested, not just that it "can be downloaded" from that place. That way, users will know that if they use a different grammar for the same language, they might be on their own. > >> +(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? > 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. From debbugs-submit-bounces@debbugs.gnu.org Mon Mar 06 14:40:28 2023 Received: (at 61996) by debbugs.gnu.org; 6 Mar 2023 19:40:28 +0000 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 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 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) 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) References: <87mt4qibnk.fsf@gmail.com> <83cz5m8515.fsf@gnu.org> <874jqx3h47.fsf@gmail.com> <83lek97mm1.fsf@gnu.org> User-agent: mu4e 1.9.3; emacs 30.0.50 From: Wilhelm Kirschbaum To: Eli Zaretskii Subject: Re: bug#61996: 30.0.50; Submitting elixir-ts-mode and heex-ts-mode Date: Mon, 06 Mar 2023 21:24:11 +0200 In-reply-to: <83lek97mm1.fsf@gnu.org> Message-ID: <877cvtfz37.fsf@gmail.com> MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="=-=-=" X-Spam-Score: 0.0 (/) X-Debbugs-Envelope-To: 61996 Cc: 61996@debbugs.gnu.org, theo@thornhill.no, casouri@gmail.com X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: debbugs-submit-bounces@debbugs.gnu.org Sender: "Debbugs-submit" X-Spam-Score: -1.0 (-) --=-=-= 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 --=-=-=-- From debbugs-submit-bounces@debbugs.gnu.org Mon Mar 06 15:14:51 2023 Received: (at 61996) by debbugs.gnu.org; 6 Mar 2023 20:14:51 +0000 Received: from localhost ([127.0.0.1]:43773 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1pZHER-0002gs-DI for submit@debbugs.gnu.org; Mon, 06 Mar 2023 15:14:51 -0500 Received: from eggs.gnu.org ([209.51.188.92]:49484) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1pZHEP-0002gd-Gf for 61996@debbugs.gnu.org; Mon, 06 Mar 2023 15:14:49 -0500 Received: from fencepost.gnu.org ([2001:470:142:3::e]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1pZHEJ-0000i5-PE; Mon, 06 Mar 2023 15:14:43 -0500 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=gnu.org; s=fencepost-gnu-org; h=References:Subject:In-Reply-To:To:From:Date: mime-version; bh=AYrLEpBlOMFYLz2qE6Qsd7Kd5X55PX6i7eZ3bCAQxjE=; b=MNHN5gA8y5DT O2PiIZ7jhv5MtIjiiwAFh6I04d0kbtOXjZ90R9A3mF+xSxhVy3zLSJNdmb9P4w3fe4RKFTjxsG6rg e8mapV46+I+sfkDkihgRlyuvZh95osUXTzSqjQ1Ajn0XUTugPN/0+W2kbOwLOyyCvXdHlym1n4ow9 S1VvqM7A+b98FhQBd4iMSH8u2+ipKZE31DSD/nKPlBRflvY5g1G08N3o0iEzMyHkcZTKgUmzTAPPO g3eOMSizD7bn6ims3ars5eZrEbuj4itkyJSnonTvMwscNQZjjwgUzSNoNl3bJBL0Bc9MS+eyc3Vlm rH463gSLYmPixQTnJU//Pw==; Received: from [87.69.77.57] (helo=home-c4e4a596f7) by fencepost.gnu.org with esmtpsa (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1pZHEJ-0006c7-6l; Mon, 06 Mar 2023 15:14:43 -0500 Date: Mon, 06 Mar 2023 22:14:37 +0200 Message-Id: <83ilfd7i36.fsf@gnu.org> From: Eli Zaretskii To: Wilhelm Kirschbaum In-Reply-To: <877cvtfz37.fsf@gmail.com> (message from Wilhelm Kirschbaum on Mon, 06 Mar 2023 21:24:11 +0200) Subject: Re: bug#61996: 30.0.50; Submitting elixir-ts-mode and heex-ts-mode References: <87mt4qibnk.fsf@gmail.com> <83cz5m8515.fsf@gnu.org> <874jqx3h47.fsf@gmail.com> <83lek97mm1.fsf@gnu.org> <877cvtfz37.fsf@gmail.com> X-Spam-Score: -2.3 (--) X-Debbugs-Envelope-To: 61996 Cc: 61996@debbugs.gnu.org, theo@thornhill.no, casouri@gmail.com X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: debbugs-submit-bounces@debbugs.gnu.org Sender: "Debbugs-submit" X-Spam-Score: -3.3 (---) > From: Wilhelm Kirschbaum > Cc: 61996@debbugs.gnu.org, casouri@gmail.com, theo@thornhill.no > Date: Mon, 06 Mar 2023 21:24:11 +0200 > > Attached are the updated patches. Is this the right format, or > should I add them inline rather? It's okay either way. From debbugs-submit-bounces@debbugs.gnu.org Sat Mar 11 04:16:32 2023 Received: (at 61996) by debbugs.gnu.org; 11 Mar 2023 09:16:32 +0000 Received: from localhost ([127.0.0.1]:56570 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1pavL6-0007JR-9z for submit@debbugs.gnu.org; Sat, 11 Mar 2023 04:16:32 -0500 Received: from eggs.gnu.org ([209.51.188.92]:40232) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1pavL4-0007J2-7u for 61996@debbugs.gnu.org; Sat, 11 Mar 2023 04:16:30 -0500 Received: from fencepost.gnu.org ([2001:470:142:3::e]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1pavKy-0001MG-L0; Sat, 11 Mar 2023 04:16:24 -0500 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=gnu.org; s=fencepost-gnu-org; h=References:Subject:In-Reply-To:To:From:Date: mime-version; bh=SCLEDybZh5y2IAdyQTUAxuzMy++ogfBmZLi8d1Kvqrc=; b=f5FuRgy7SQaV 7EoeHWnm26s/LbiHESreyv18fvfOgT2SoMRsjS6eQCzgZJikdw/CpA0CzSuvxT/6sX+okABJy3bXA HvSpAKQBx1jK6g+48F5NwHL6tGw83s+YW87sywzs8E4jnVLfcZaS5RH0B3+WxbAxRo/9heBK1VFxJ UE2BOdON1t1YLWgjhZQxXhE8QeXWwmWy4ig6iY+X45boS8gwoHUhMVITlyWdbPXkfn6DYWCpzkgPu Kvigb3Gisi3u4do63xaVA5ZZmvU0HAYzRXzjZvxBel288DtKiAOOzkYPt6EZBeTqhORScChKzhTSV 8AneteVMJ0mn6VWEaqHS8Q==; Received: from [87.69.77.57] (helo=home-c4e4a596f7) by fencepost.gnu.org with esmtpsa (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1pavKy-0008GP-44; Sat, 11 Mar 2023 04:16:24 -0500 Date: Sat, 11 Mar 2023 11:16:08 +0200 Message-Id: <83356by7fr.fsf@gnu.org> From: Eli Zaretskii To: Wilhelm Kirschbaum In-Reply-To: <877cvtfz37.fsf@gmail.com> (message from Wilhelm Kirschbaum on Mon, 06 Mar 2023 21:24:11 +0200) Subject: Re: bug#61996: 30.0.50; Submitting elixir-ts-mode and heex-ts-mode References: <87mt4qibnk.fsf@gmail.com> <83cz5m8515.fsf@gnu.org> <874jqx3h47.fsf@gmail.com> <83lek97mm1.fsf@gnu.org> <877cvtfz37.fsf@gmail.com> X-Spam-Score: -2.3 (--) X-Debbugs-Envelope-To: 61996 Cc: 61996@debbugs.gnu.org, theo@thornhill.no, casouri@gmail.com X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: debbugs-submit-bounces@debbugs.gnu.org Sender: "Debbugs-submit" X-Spam-Score: -3.3 (---) > From: Wilhelm Kirschbaum > Cc: 61996@debbugs.gnu.org, casouri@gmail.com, theo@thornhill.no > Date: Mon, 06 Mar 2023 21:24:11 +0200 > > 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. Thanks, a few minor comments below. > >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 Please accompany the changes with a commit log message according to our conventions (see CONTRIBUTE for the conventions; search for "ChangeLog" there). In this case, just "New file" log should be sufficient for the new files you add. > +(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") AFAICS, the code uses more functions from treesit.c; please add declare-function forms for all of them , to avoid compilation warnings n systems where Emacs was built without tree-sitter. > +(defun heex-ts-mode--forward-sexp (&optional arg) > + (interactive "^p") Why is a command an internal function? That is unusual, as commands are by definition public. It looks like you thought the double-hyphen "--" notation is a simple delimiter between the package-name part of the symbol name and the rest? If so, you were mistaken: the double-hyphen means this is an internal function/variable. Please review all your symbol names in this patch and rename as appropriate. Btw, there's no need to have the prefix be the full name of the package, as in "elixir-ts-mode-". You could use "elixir-ts-" instead. > +;;;###autoload > +(define-derived-mode heex-ts-mode html-mode "Heex" html-mode? not html-ts-mode? > >From d13c34ed951e3e6fa473cd1bc2e955e20455022b Mon Sep 17 00:00:00 2001 > From: Wilhelm H Kirschbaum > Date: Mon, 6 Mar 2023 21:18:35 +0200 > > --- > 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 Likewise here: please add a commit log message describing the changes. > +(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") Please verify that you have declare-function for all the functions from treesit.c this package uses, and only for those. > +(defgroup elixir-ts nil > + "Major mode for editing Ruby code." ^^^^ "Ruby"? > +;; used to distinguish from comment-face in query match Comments should be complete sentences: start with a capital letter and end with a period (here and elsewhere in the patches). > +(defface elixir-ts-font-comment-doc-identifier-face > + '((t (:inherit font-lock-doc-face))) > + "For use with @comment.doc tag.") This doc string is too terse. Imagine someone looking at it in a long list of symbols, not necessarily all of them faces. So something like this is better: Face used for @comment.doc tags in Elixir files. Likewise for other faces in the patch. > + (modify-syntax-entry ?@ "'" table) > + table) > + "Syntax table for `elixir-ts-mode.") ^ The closing ' quote is missing there. From debbugs-submit-bounces@debbugs.gnu.org Sat Mar 11 09:16:34 2023 Received: (at 61996) by debbugs.gnu.org; 11 Mar 2023 14:16:34 +0000 Received: from localhost ([127.0.0.1]:56925 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1pb01S-0003er-CE for submit@debbugs.gnu.org; Sat, 11 Mar 2023 09:16:34 -0500 Received: from mail-wr1-f46.google.com ([209.85.221.46]:44808) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1pb01R-0003eb-1p for 61996@debbugs.gnu.org; Sat, 11 Mar 2023 09:16:33 -0500 Received: by mail-wr1-f46.google.com with SMTP id p4so1350757wre.11 for <61996@debbugs.gnu.org>; Sat, 11 Mar 2023 06:16:33 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20210112; t=1678544187; h=content-transfer-encoding:in-reply-to:from:references:cc:to :content-language:subject:user-agent:mime-version:date:message-id :sender:from:to:cc:subject:date:message-id:reply-to; bh=2yvrcv15iF9DgcGTVGSclz8hf6cilPE3T2KWhF+C4Ko=; b=oNRUoGE/bpkwn2p4gleEu+NwcilhqDbS4urRqnqmMz3jDL71mUIjH7dk2awok+5pZC URGS9y0B1BdoWLEWW72dyQKcQEIvuTcTC0p+hMLBMckHzJt1YxEfWqk4dM+Rh0JkahMz L7BwEofIOiaT5R4j0X9M3oBJgRJrrduNgP8y6+1AuueFTu99eSufvDPGEAWoO2/XrdQb fGrX5Z+PoWrZp51E6QlUzrlsf8JV5kEZyNBLZ90IoDRYF7OJBC6b2TcMvzfk5Xu5HoXh dUeCDqAzXC3t293r4RFSDaDI//8lUigZPtN/yKlomZGv5vFN8hdUyh92BloracSOAlfp Lwyg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20210112; t=1678544187; h=content-transfer-encoding:in-reply-to:from:references:cc:to :content-language:subject:user-agent:mime-version:date:message-id :sender:x-gm-message-state:from:to:cc:subject:date:message-id :reply-to; bh=2yvrcv15iF9DgcGTVGSclz8hf6cilPE3T2KWhF+C4Ko=; b=hWglGJ6ax3yEqEz9zZzbIU8qijs0naacqfG1BNkTBVKlxqNDicqY34a1KuqwATP11q qO/5SrsmUx7xcDvaJdy48+JHK/i9kpIdJzNMeZryLUX+lLtSTUkgpAxdTqcvXvAC3FK0 TXTS4LOYw70vA8eICW3SbFS5rIKXeiSW25US2Ahz+wYWIJ9YVJnn0K0dtsHjTZGZL9My ZHoK/FZetmQj5W47pL5rum/b5WrAU1CsQl4vdtxiMbIEBFlka8kAWtb/C0/BcPkawDTD 0iHBIlRNubQYukngpBLbQnu9VxE5oB4V6FWYjc6z4atzU8SHqDElBy0kDyAXfpbSqeG2 maIw== X-Gm-Message-State: AO0yUKWEa0immE4JtHkbrrWsId0U0z8dHW4tDaYaG/R8qZq5qfKTSjZB gYKnNiQfRjDFxNw2N1IzmAU= X-Google-Smtp-Source: AK7set/T/wOI7oDzb+WTkEhOVdmO78JP2CHqR90i3OKJrfErg4HbMnQC4W7JMWqg1sRGOADYF7/N7g== X-Received: by 2002:adf:f304:0:b0:2c7:1210:feaa with SMTP id i4-20020adff304000000b002c71210feaamr18426131wro.26.1678544186884; Sat, 11 Mar 2023 06:16:26 -0800 (PST) Received: from [192.168.0.2] ([85.132.229.92]) by smtp.googlemail.com with ESMTPSA id z17-20020a5d4c91000000b002c5a1bd5280sm2579397wrs.95.2023.03.11.06.16.25 (version=TLS1_3 cipher=TLS_AES_128_GCM_SHA256 bits=128/128); Sat, 11 Mar 2023 06:16:26 -0800 (PST) Message-ID: <77b6fddf-ee84-9a3e-e08e-768bc05359b7@yandex.ru> Date: Sat, 11 Mar 2023 16:16:24 +0200 MIME-Version: 1.0 User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Thunderbird/102.7.1 Subject: Re: bug#61996: 30.0.50; Submitting elixir-ts-mode and heex-ts-mode Content-Language: en-US To: Eli Zaretskii , Wilhelm Kirschbaum References: <87mt4qibnk.fsf@gmail.com> <83cz5m8515.fsf@gnu.org> <874jqx3h47.fsf@gmail.com> <83lek97mm1.fsf@gnu.org> <877cvtfz37.fsf@gmail.com> <83356by7fr.fsf@gnu.org> From: Dmitry Gutov In-Reply-To: <83356by7fr.fsf@gnu.org> Content-Type: text/plain; charset=UTF-8; format=flowed Content-Transfer-Encoding: 7bit X-Spam-Score: -0.9 (/) X-Debbugs-Envelope-To: 61996 Cc: 61996@debbugs.gnu.org, theo@thornhill.no, casouri@gmail.com X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: debbugs-submit-bounces@debbugs.gnu.org Sender: "Debbugs-submit" X-Spam-Score: -1.9 (-) On 11/03/2023 11:16, Eli Zaretskii wrote: >> +(defun heex-ts-mode--forward-sexp (&optional arg) >> + (interactive "^p") > Why is a command an internal function? That is unusual, as commands > are by definition public. It looks like you thought the double-hyphen > "--" notation is a simple delimiter between the package-name part of > the symbol name and the rest? If so, you were mistaken: the > double-hyphen means this is an internal function/variable. Please > review all your symbol names in this patch and rename as appropriate. I'm guessing it was made interactive for debugging purposes. But even that doesn't seem necessary: calling 'forward-sexp' through its regular binding will invoke forward-sexp-function basically right away. (treesit-forward-sexp doesn't need to be interactive either.) From debbugs-submit-bounces@debbugs.gnu.org Sat Mar 11 13:24:06 2023 Received: (at 61996) by debbugs.gnu.org; 11 Mar 2023 18:24:06 +0000 Received: from localhost ([127.0.0.1]:58641 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1pb3sy-0004Xm-6a for submit@debbugs.gnu.org; Sat, 11 Mar 2023 13:24:06 -0500 Received: from mail-wr1-f43.google.com ([209.85.221.43]:39763) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1pb3su-0004XG-VG for 61996@debbugs.gnu.org; Sat, 11 Mar 2023 13:24:03 -0500 Received: by mail-wr1-f43.google.com with SMTP id g3so7870858wri.6 for <61996@debbugs.gnu.org>; Sat, 11 Mar 2023 10:24:00 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20210112; t=1678559035; 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=IqLfaxT1e3CDY3+oJiXkRIbymJoeemR8cqz9OaiA+D4=; b=VQ5HjUkvnbp46nN9qSAPbp6zdNbW1DuHK2JOu2HKtqTNXHmMMXBiai9axUtoUfl4Uy 2lxjI7nPYV2qq/R7tno4zUSiJLgTKcmRKmRgUsf5xLSpXz2Ltk+Ab7Et1iIvoAOj9Atq z7xPyYVBQ8l9wFSmZl2fDL9/xxR2CTkiwn5213hLjJ1OjGVMM2Iis0FPilxOIunanBRS FnuKzkd0ubfTqWh7mrCBsiyhrA8MqdRq1TC1WBTHR45RPlKJbnLms5sZKKpiwmu53KXb 2Nki9KItgH8+3iLx+bfRamuO4QBJb3dPTGLlP10yrxmh2iX0KQtBrHpfMDnQH8AUp0p3 GUTA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20210112; t=1678559035; 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=IqLfaxT1e3CDY3+oJiXkRIbymJoeemR8cqz9OaiA+D4=; b=N90kYbTb1eBd0aE8+lCSJzy7iCc9hdeRC2NgbH3y6F6u6haYOyj6OehnVCtgyPzBms KXbDzDS2JfngtTxcvlc5PWklFALUDTjslXsimDiWXb5SD8wu+3wnVynr/sIdGr8cZsqS 5px8LKGV9fOYRBzmxzm3OqQ+T5OKt+1ljR1BJsp2gP2DHNTcoyIlmwUZ4zHRLY+0tLM5 8+0KaUVC4OehDT+Lmh1MSOpWCLFpnisV5A/QJs5Y0gioXHjABuWDfIYJnzfBVo5WGiB0 5VJl0CjNA5pnY4RqMLqXb48/HBnPLjgtDCLVmBuLl7lj6VCP+kWNLb8dqpCecrL29wxP +WFA== X-Gm-Message-State: AO0yUKWVRgw8oDJ5or/q9P+d1Vmn1CL1HV+YuwQhP9tBMveQxTsQSLi0 J4c5MlFgJ50DQq+t2c3ZBnA= X-Google-Smtp-Source: AK7set8XMbeaXSRPCBtA9O0jHKsEODOrOYXvroN1pd0ix5ZWUdlO0TGdrTQlTkYzhFitOpvUeNdn8w== X-Received: by 2002:a5d:4652:0:b0:2c3:d032:5b29 with SMTP id j18-20020a5d4652000000b002c3d0325b29mr5134842wrs.12.1678559034917; Sat, 11 Mar 2023 10:23:54 -0800 (PST) Received: from melissa.local ([2c0f:ef18:1431:0:b09:9616:db04:c248]) by smtp.gmail.com with ESMTPSA id c14-20020adfe70e000000b002cdbb26bb48sm3099554wrm.33.2023.03.11.10.23.52 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Sat, 11 Mar 2023 10:23:54 -0800 (PST) References: <87mt4qibnk.fsf@gmail.com> <83cz5m8515.fsf@gnu.org> <874jqx3h47.fsf@gmail.com> <83lek97mm1.fsf@gnu.org> <877cvtfz37.fsf@gmail.com> <83356by7fr.fsf@gnu.org> User-agent: mu4e 1.9.3; emacs 30.0.50 From: Wilhelm Kirschbaum To: Eli Zaretskii Subject: Re: bug#61996: 30.0.50; Submitting elixir-ts-mode and heex-ts-mode Date: Sat, 11 Mar 2023 20:01:32 +0200 In-reply-to: <83356by7fr.fsf@gnu.org> Message-ID: <87h6urkuyx.fsf@gmail.com> MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="=-=-=" X-Spam-Score: 0.0 (/) X-Debbugs-Envelope-To: 61996 Cc: 61996@debbugs.gnu.org, theo@thornhill.no, casouri@gmail.com X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: debbugs-submit-bounces@debbugs.gnu.org Sender: "Debbugs-submit" X-Spam-Score: -1.0 (-) --=-=-= Content-Type: text/plain; format=flowed Eli Zaretskii writes: >> >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 > > Please accompany the changes with a commit log message according > to > our conventions (see CONTRIBUTE for the conventions; search for > "ChangeLog" there). In this case, just "New file" log should be > sufficient for the new files you add. > Thanks, was not aware of it. I hope it is correct in the new patches. >> +(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") > > AFAICS, the code uses more functions from treesit.c; please add > declare-function forms for all of them , to avoid compilation > warnings > n systems where Emacs was built without tree-sitter. > I made some changes and checked on a non-treesit build and see no more warnings. >> +(defun heex-ts-mode--forward-sexp (&optional arg) >> + (interactive "^p") > > Why is a command an internal function? That is unusual, as > commands > are by definition public. It looks like you thought the > double-hyphen > "--" notation is a simple delimiter between the package-name > part of > the symbol name and the rest? If so, you were mistaken: the > double-hyphen means this is an internal function/variable. > Please > review all your symbol names in this patch and rename as > appropriate. > > Btw, there's no need to have the prefix be the full name of the > package, as in "elixir-ts-mode-". You could use "elixir-ts-" > instead. > This should be internal, I removed the interactive. >> +;;;###autoload >> +(define-derived-mode heex-ts-mode html-mode "Heex" > > html-mode? not html-ts-mode? > I don't see the advantage to use html-ts-mode over html-mode at the moment, but can have another look if there is a specific reason to do so. >> >From d13c34ed951e3e6fa473cd1bc2e955e20455022b Mon Sep 17 >> >00:00:00 2001 >> From: Wilhelm H Kirschbaum >> Date: Mon, 6 Mar 2023 21:18:35 +0200 >> >> --- >> 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 > > Likewise here: please add a commit log message describing the > changes. > >> +(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") > > Please verify that you have declare-function for all the > functions > from treesit.c this package uses, and only for those. > I think this is fixed. >> +(defgroup elixir-ts nil >> + "Major mode for editing Ruby code." > ^^^^ > "Ruby"? > Copy paste error from ruby-ts-mode when trying to follow conventions. >> +;; used to distinguish from comment-face in query match > > Comments should be complete sentences: start with a capital > letter and > end with a period (here and elsewhere in the patches). > >> +(defface elixir-ts-font-comment-doc-identifier-face >> + '((t (:inherit font-lock-doc-face))) >> + "For use with @comment.doc tag.") > > This doc string is too terse. Imagine someone looking at it in > a long > list of symbols, not necessarily all of them faces. So > something > like this is better: > > Face used for @comment.doc tags in Elixir files. > > Likewise for other faces in the patch. > >> + (modify-syntax-entry ?@ "'" table) >> + table) >> + "Syntax table for `elixir-ts-mode.") > ^ > The closing ' quote is missing there. The new patches should hopefully cover all of the above issues. Thanks for the patience. --=-=-= Content-Type: text/x-patch Content-Disposition: attachment; filename=0001-Add-heex-ts-mode-Bug-61996.patch Content-Description: Add heex-ts-mode >From a8adf5f1213e8e8629aecee09dcd24cb55f06e51 Mon Sep 17 00:00:00 2001 From: Wilhelm H Kirschbaum Date: Sat, 11 Mar 2023 19:42:36 +0200 Subject: [PATCH 1/2] Add heex-ts-mode (Bug#61996) * etc/NEWS: Mention the new mode. * lisp/progmodes/heex-ts-mode.el: New file. * test/lisp/progmodes/heex-ts-mode-tests.el: New file. * test/lisp/progmodes/heex-ts-mode-resources/indent.erts: New file. * admin/notes/tree-sitter/build-module/batch.sh: * admin/notes/tree-sitter/build-module/build.sh: Add HEEx support. --- admin/notes/tree-sitter/build-module/batch.sh | 1 + admin/notes/tree-sitter/build-module/build.sh | 3 + etc/NEWS | 3 + lisp/progmodes/heex-ts-mode.el | 185 ++++++++++++++++++ .../heex-ts-mode-resources/indent.erts | 47 +++++ test/lisp/progmodes/heex-ts-mode-tests.el | 9 + 6 files changed, 248 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/admin/notes/tree-sitter/build-module/batch.sh b/admin/notes/tree-sitter/build-module/batch.sh index 58272c74549..8b0072782e8 100755 --- a/admin/notes/tree-sitter/build-module/batch.sh +++ b/admin/notes/tree-sitter/build-module/batch.sh @@ -10,6 +10,7 @@ languages= 'dockerfile' 'go' 'go-mod' + 'heex' 'html' 'javascript' 'json' diff --git a/admin/notes/tree-sitter/build-module/build.sh b/admin/notes/tree-sitter/build-module/build.sh index 9dc674237ca..78ecfb5bc82 100755 --- a/admin/notes/tree-sitter/build-module/build.sh +++ b/admin/notes/tree-sitter/build-module/build.sh @@ -36,6 +36,9 @@ grammardir= lang="gomod" org="camdencheek" ;; + "heex") + org="phoenixframework" + ;; "typescript") sourcedir="tree-sitter-typescript/typescript/src" grammardir="tree-sitter-typescript/typescript" diff --git a/etc/NEWS b/etc/NEWS index 13d073c7fb8..ed74d0be1a1 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -230,6 +230,9 @@ This replaces 'doc-view-svg-foreground' and 'doc-view-svg-background'. An optional major mode based on the tree-sitter library for editing HTML files. +*** New major mode heex-ts-mode'. +A major mode based on the tree-sitter library for editing HEEx files. + --- ** The highly accessible Modus themes collection has six items. The 'modus-operandi' and 'modus-vivendi' are the main themes that have diff --git a/lisp/progmodes/heex-ts-mode.el b/lisp/progmodes/heex-ts-mode.el new file mode 100644 index 00000000000..6096dbabf30 --- /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-node-start "treesit.c") + +(defgroup heex-ts nil + "Major mode for editing HEEx code." + :prefix "heex-ts-" + :group 'langauges) + +(defcustom heex-ts-indent-offset 2 + "Indentation of HEEx statements." + :version "30.1" + :type 'integer + :safe 'integerp + :group 'heex-ts) + +(defconst heex-ts-sexp-regexp + (rx bol + (or "directive" "tag" "component" "slot" + "attribute" "attribute_value" "quoted_attribute_value") + eol)) + +;; There seems to be no parent directive block for tree-sitter-heex, +;; so we ignore them for now until we learn how to query them. +;; https://github.com/phoenixframework/tree-sitter-heex/issues/28 +(defvar heex-ts--indent-rules + (let ((offset heex-ts-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--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--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--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) + heex-ts-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--forward-sexp) + + ;; Navigation. + (setq-local treesit-defun-type-regexp + (rx bol (or "component" "tag" "slot") eol)) + (setq-local treesit-defun-name-function #'heex-ts--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--font-lock-settings) + + (setq-local treesit-simple-indent-rules heex-ts--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-Bug-61996.patch Content-Description: Add elixir-ts-mode >From fa69480e61d779c251b44b568908ef5a50e47d48 Mon Sep 17 00:00:00 2001 From: Wilhelm H Kirschbaum Date: Sat, 11 Mar 2023 19:49:20 +0200 Subject: [PATCH 2/2] 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. --- admin/notes/tree-sitter/build-module/batch.sh | 1 + admin/notes/tree-sitter/build-module/build.sh | 3 + etc/NEWS | 4 + lisp/progmodes/eglot.el | 2 +- lisp/progmodes/elixir-ts-mode.el | 625 ++++++++++++++++++ .../elixir-ts-mode-resources/indent.erts | 147 ++++ test/lisp/progmodes/elixir-ts-mode-tests.el | 31 + 7 files changed, 812 insertions(+), 1 deletion(-) 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/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= 'css' 'c-sharp' 'dockerfile' + 'elixir' 'go' 'go-mod' '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 @@ grammardir= "cmake") org="uyha" ;; + "elixir") + org="elixir-lang" + ;; "go-mod") # The parser is called "gomod". lang="gomod" diff --git a/etc/NEWS b/etc/NEWS index ed74d0be1a1..d40fa51d80d 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -233,6 +233,10 @@ HTML files. *** New major mode heex-ts-mode'. A major mode based on the tree-sitter library for editing HEEx files. +*** New major mode elixir-ts-mode'. +A major mode based on the tree-sitter library for editing Elixir +files. + --- ** The highly accessible Modus themes collection has six items. The '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 @@ eglot-server-programs ((java-mode java-ts-mode) . ("jdtls")) (dart-mode . ("dart" "language-server" "--client-id" "emacs.eglot-dart")) - (elixir-mode . ("language_server.sh")) + ((elixir-ts-mode elixir-mode) . ("language_server.sh")) (ada-mode . ("ada_language_server")) (scala-mode . ,(eglot-alternatives '("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..d6caa57b4f2 --- /dev/null +++ b/lisp/progmodes/elixir-ts-mode.el @@ -0,0 +1,625 @@ +;;; 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-node-eq "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) + +(defface elixir-ts-font-comment-doc-identifier-face + '((t (:inherit font-lock-doc-face))) + "Face used for @comment.doc tags in Elixir files.") + +(defface elixir-ts-font-comment-doc-attribute-face + '((t (:inherit font-lock-doc-face))) + "Face used for @comment.doc.__attribute__ tags in Elixir files.") + +(defface elixir-ts-font-sigil-name-face + '((t (:inherit font-lock-string-face))) + "Face used for @__name__ tags in Elixir files.") + +(defconst elixir-ts--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--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 elixir-ts--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'.") + +(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 "^|$") parent-bol 0) + ((node-is "^}$") parent-bol 0) + ((node-is "^)$") + (lambda (_node parent &rest _) + (elixir-ts--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--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--call-parent-start parent)) + ((treesit-node-eq node first-child) + (elixir-ts--call-parent-start parent)) + (t (elixir-ts--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. + ((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--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--capture-anonymous-function-end) parent-bol 0) + ((node-is "^end$") + (lambda (_node parent &rest _) + (elixir-ts--call-parent-start 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))))) + +(defvar elixir-ts--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 + `(,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))) + + :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--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--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--definition-keywords-re @font-lock-keyword-face)) + (call + target: (identifier) @font-lock-keyword-face + (:match ,elixir-ts--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--definition-keywords-re @font-lock-keyword-face)) + (call + target: (identifier) @font-lock-keyword-face + (arguments + (binary_operator + operator: "|>" + right: (identifier))) + (:match ,elixir-ts--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--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.") + +(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))))) + +(defun elixir-ts--call-parent-start (node) + "Return the closest parent of NODE that is of type call." + (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))))) + +(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* ((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--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))) + +;;;###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") + + (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. + (if (treesit-ready-p 'heex) + (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-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--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-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 --=-=-=-- From debbugs-submit-bounces@debbugs.gnu.org Sat Mar 11 13:33:40 2023 Received: (at 61996) by debbugs.gnu.org; 11 Mar 2023 18:33:40 +0000 Received: from localhost ([127.0.0.1]:58646 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1pb42G-0004mj-CE for submit@debbugs.gnu.org; Sat, 11 Mar 2023 13:33:40 -0500 Received: from mail-wm1-f54.google.com ([209.85.128.54]:55107) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1pb42D-0004mU-Pl for 61996@debbugs.gnu.org; Sat, 11 Mar 2023 13:33:38 -0500 Received: by mail-wm1-f54.google.com with SMTP id p26so5419227wmc.4 for <61996@debbugs.gnu.org>; Sat, 11 Mar 2023 10:33:37 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20210112; t=1678559611; 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=gN2a8W/p9eDb8mqFxX2tgr2uXqHlpj4ixwGRwSPXpT8=; b=b7gMbvjk4mhiUFGtYObBdHmRI87PiwwclOAIgKpXMYgWSfYozX7VaNYqzAF6qqza5v ZmBxXZHcqlJQWqwS1wfheQCTYwffjT1IZ4cv5TaoRtW839gAYqUc2TbVAbF8RF4U9uKL BUB4UV1NHTtbF+2UaSv0xproO05rspacfT6b4dYwNvYbmXm1Y+/NKqRncNhgG+y9v9Ij K6uZnsqk3Cr6CPGiOsfYLmy7QkimBU7/2dQpSair/EDEwJgo320lUOgk8rOYV7gV9ZxJ 2kWgpDlRXk9RBHAxOH9B7wXj8pMThw0Rx9ZbFe3QMNyli3v8ZXx+DRkc1D8TysN8qjZd rfPg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20210112; t=1678559611; 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=gN2a8W/p9eDb8mqFxX2tgr2uXqHlpj4ixwGRwSPXpT8=; b=qCfSmE68hBOrBoyLRSx0I+XU1gj0K3iHxtShdC0JxaA1rPwkjsph4DOOX7ijlkszsG CMz75Ho9ae/u2kzGdnTVS+e4eOpKwGMsZgqktEUuc8I5G3al18akdzlEjg1i650svVpw cYCGKUQl4w8ML/WL2SGuyRSlww7Pme5H7reLnJIKp+/iZubEwzg3zqQpAxE9og78ATU/ 3M9LFbIHnSDIrjUeaAxyhqmaTWmMTBolRZlvMpol4dIzOfXmjcTnD7YK8Mk7k2MDpuR+ CAZtRIKbN19fOW5qCYNLUTFOlPCXXRDlpW54IjwMtEcLYA5s+SfjETdWTGsq3SHO3F9k CaKg== X-Gm-Message-State: AO0yUKWZvOPf30RN1up+JhjmRZivVe7HWSYSG04+9068HZzWKM2ImXrF dJ7r/rXSjAA/4Q+9LqKf7vI= X-Google-Smtp-Source: AK7set/I2LKzWdGx2SfLJCNXCaICpOQuCIrKXWTEWrJb0k4K2+Q7BbyIxH9bTZtF9zcUyczEM5+2Ww== X-Received: by 2002:a05:600c:310e:b0:3e9:f15b:935b with SMTP id g14-20020a05600c310e00b003e9f15b935bmr6741401wmo.32.1678559611661; Sat, 11 Mar 2023 10:33:31 -0800 (PST) Received: from melissa.local ([2c0f:ef18:1431:0:b09:9616:db04:c248]) by smtp.gmail.com with ESMTPSA id i2-20020a05600c290200b003ed1fa34bd3sm400765wmd.13.2023.03.11.10.33.29 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Sat, 11 Mar 2023 10:33:31 -0800 (PST) References: <87mt4qibnk.fsf@gmail.com> <83cz5m8515.fsf@gnu.org> <874jqx3h47.fsf@gmail.com> <83lek97mm1.fsf@gnu.org> <877cvtfz37.fsf@gmail.com> <83356by7fr.fsf@gnu.org> <77b6fddf-ee84-9a3e-e08e-768bc05359b7@yandex.ru> User-agent: mu4e 1.9.3; emacs 30.0.50 From: Wilhelm Kirschbaum To: Dmitry Gutov Subject: Re: bug#61996: 30.0.50; Submitting elixir-ts-mode and heex-ts-mode Date: Sat, 11 Mar 2023 20:27:35 +0200 In-reply-to: <77b6fddf-ee84-9a3e-e08e-768bc05359b7@yandex.ru> Message-ID: <87cz5fkuiw.fsf@gmail.com> MIME-Version: 1.0 Content-Type: text/plain; format=flowed X-Spam-Score: 0.0 (/) X-Debbugs-Envelope-To: 61996 Cc: casouri@gmail.com, Eli Zaretskii , theo@thornhill.no, 61996@debbugs.gnu.org X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: debbugs-submit-bounces@debbugs.gnu.org Sender: "Debbugs-submit" X-Spam-Score: -1.0 (-) Dmitry Gutov writes: > On 11/03/2023 11:16, Eli Zaretskii wrote: >>> +(defun heex-ts-mode--forward-sexp (&optional arg) >>> + (interactive "^p") >> Why is a command an internal function? That is unusual, as >> commands >> are by definition public. It looks like you thought the >> double-hyphen >> "--" notation is a simple delimiter between the package-name >> part of >> the symbol name and the rest? If so, you were mistaken: the >> double-hyphen means this is an internal function/variable. >> Please >> review all your symbol names in this patch and rename as >> appropriate. > > I'm guessing it was made interactive for debugging purposes. > > But even that doesn't seem necessary: calling 'forward-sexp' > through > its regular binding will invoke forward-sexp-function basically > right > away. > > (treesit-forward-sexp doesn't need to be interactive either.) This was a mistake and still learning some basic conventions. I don't see a reason to call this function interactively. From debbugs-submit-bounces@debbugs.gnu.org Sun Mar 12 05:01:13 2023 Received: (at 61996) by debbugs.gnu.org; 12 Mar 2023 09:01:13 +0000 Received: from localhost ([127.0.0.1]:59153 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1pbHZo-0003wP-Mo for submit@debbugs.gnu.org; Sun, 12 Mar 2023 05:01:12 -0400 Received: from eggs.gnu.org ([209.51.188.92]:46772) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1pbHZl-0003w9-DO for 61996@debbugs.gnu.org; Sun, 12 Mar 2023 05:01:11 -0400 Received: from fencepost.gnu.org ([2001:470:142:3::e]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1pbHZf-0001T6-5b; Sun, 12 Mar 2023 05:01:03 -0400 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=gnu.org; s=fencepost-gnu-org; h=References:Subject:In-Reply-To:To:From:Date: mime-version; bh=vo5kINVIRV+OzU8rDgnrOXkt6ZheIiFXKDmdlJNsJLE=; b=M4ThBgP05D8C sGJvsNaKTQw93/MS0kYczJq0N24HbKFh58hOp0C3ZNP3i90Hzb6Un3PRNDpj8OjX+LmCc1LwAoyHN C5TL/fUGWagPEus1WwD8TtYi8w9DldM+v6hxB0qQ8GPL7qdIygaOyUZ0ap3y17b5Tp1jFAGeLHhXe cIwzqEiO+ixsySzETGsyGwBHZDA5xDa81l3LX2N8eXw4CBfZYFsOG3D+kL9bjs2QQOrR2GZwLFU7j dXJpkGs9upsrQ1g73HvaadEP2m1LiZ+vlGgg9Du2J7dl94fX7SwUJMHDVCc6anVb/dfI41Ox0bYUf b2DTiVdGNEHvFnr/SQ9jeQ==; Received: from [87.69.77.57] (helo=home-c4e4a596f7) by fencepost.gnu.org with esmtpsa (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1pbHZb-0005OX-PA; Sun, 12 Mar 2023 05:01:02 -0400 Date: Sun, 12 Mar 2023 11:00:46 +0200 Message-Id: <83fsaauywx.fsf@gnu.org> From: Eli Zaretskii To: Wilhelm Kirschbaum In-Reply-To: <87h6urkuyx.fsf@gmail.com> (message from Wilhelm Kirschbaum on Sat, 11 Mar 2023 20:01:32 +0200) Subject: Re: bug#61996: 30.0.50; Submitting elixir-ts-mode and heex-ts-mode References: <87mt4qibnk.fsf@gmail.com> <83cz5m8515.fsf@gnu.org> <874jqx3h47.fsf@gmail.com> <83lek97mm1.fsf@gnu.org> <877cvtfz37.fsf@gmail.com> <83356by7fr.fsf@gnu.org> <87h6urkuyx.fsf@gmail.com> X-Spam-Score: -2.3 (--) X-Debbugs-Envelope-To: 61996 Cc: 61996@debbugs.gnu.org, theo@thornhill.no, casouri@gmail.com X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: debbugs-submit-bounces@debbugs.gnu.org Sender: "Debbugs-submit" X-Spam-Score: -3.3 (---) > From: Wilhelm Kirschbaum > Cc: 61996@debbugs.gnu.org, casouri@gmail.com, theo@thornhill.no > Date: Sat, 11 Mar 2023 20:01:32 +0200 > > > Please accompany the changes with a commit log message according > > to our conventions (see CONTRIBUTE for the conventions; search for > > "ChangeLog" there). In this case, just "New file" log should be > > sufficient for the new files you add. > > > Thanks, was not aware of it. I hope it is correct in the new > patches. Yes, it is. Thanks. > The new patches should hopefully cover all of the above issues. Almost there. Byte compiler shows warnings, which I think are real problems in the code: In elixir-ts--call-parent-start: progmodes/elixir-ts-mode.el:459:38: Warning: Unused lexical argument `node' progmodes/elixir-ts-mode.el:463:15: Warning: reference to free variable `parent' In elixir-ts--forward-sexp: progmodes/elixir-ts-mode.el:482:8: Warning: reference to free variable `heex-ts--sexp-regexp' From debbugs-submit-bounces@debbugs.gnu.org Sun Mar 12 06:00:31 2023 Received: (at 61996) by debbugs.gnu.org; 12 Mar 2023 10:00:31 +0000 Received: from localhost ([127.0.0.1]:59240 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1pbIVB-00063V-4k for submit@debbugs.gnu.org; Sun, 12 Mar 2023 06:00:31 -0400 Received: from mail-wm1-f54.google.com ([209.85.128.54]:43742) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1pbIV7-000637-EA for 61996@debbugs.gnu.org; Sun, 12 Mar 2023 06:00:27 -0400 Received: by mail-wm1-f54.google.com with SMTP id l7-20020a05600c4f0700b003e79fa98ce1so6058945wmq.2 for <61996@debbugs.gnu.org>; Sun, 12 Mar 2023 03:00:25 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20210112; t=1678615219; 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=GLkHhR19xvVls4Gx15D1ogQeyNVq4JhA/4eAD7SxZAA=; b=bsJYHtmdpKdP+Jo8BISIuVlM3dLB/fEiN9XunIFcGXJBrfQZuYjJ6foTFMqi9yyfbS FXFNH37HU/qftRzAqJ4i4oOXlTBjJfmoQ0HopiqEPwO3ob8UPVcWmpa5qE9d/3jNOzBY YqopdQAH5N81rvPKxBx7GicAWw+tjrF7SciwW68SuIEfRg3lkrIB8mLU2X39ZwEYBg9N SOw6CBjn4QdL6VN0KaFleE+vrYgyL68G1VhfHpAFNZ2Gb8v74wBH9/zWuKggFXnElm46 QRhSPoyuDqD82X8XBV09z0MVLFqBFYyaVQuLS+TtqikNQhXh48YWvyC7nKPkj0fLhINn /1tg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20210112; t=1678615219; 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=GLkHhR19xvVls4Gx15D1ogQeyNVq4JhA/4eAD7SxZAA=; b=Tf3Ti5KB6nA9E+TZnid4wcUSJbxj3P9lTnqE2S2y8o3D4otmg8k8mlbzeGhnYLRb44 8catoU5dmO5ousKwYDcPt0FDvuzS/PvuKKJV1RKRZjf+O+lhm3ydegKq6lUK3/NTRD8D +gnXAtn8PVqTTScOQsT+xpuQcl4QA5PxzaXsfsNe+VBhT+JuG7PuKOoGeyTfvmRj41UB eZBpEBIBq0PoE/23GcwVzaKdnJKa+lR9n1IkcBvTOkIrVeCbmfKE4LDQNlhIaa4YngeN k7ng3dnRm5/eLT+1fxkOizFavofhGprGXmCB77HFUtSIRsD/kFuHh0Y8PbPwV/WDD7be 2K2A== X-Gm-Message-State: AO0yUKWyh8cPo9Zwed+OYx7lfBtVclyaHlgb+7nqIsJUzwGyoigIRz1Q x20HyJmwG1u759ORvKh17DU= X-Google-Smtp-Source: AK7set+CRdnVROl2m4uHkKkZI/rgPk//r/gfOIbtkGPbC1A9s6weE0GUtN+HG3U97tnhU5QTrCvOcw== X-Received: by 2002:a05:600c:19d3:b0:3eb:5d17:8ddc with SMTP id u19-20020a05600c19d300b003eb5d178ddcmr7851222wmq.18.1678615218921; Sun, 12 Mar 2023 03:00:18 -0700 (PDT) Received: from melissa.local ([2c0f:ef18:1431:0:b09:9616:db04:c248]) by smtp.gmail.com with ESMTPSA id o10-20020a1c750a000000b003e1f2e43a1csm5293453wmc.48.2023.03.12.03.00.16 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Sun, 12 Mar 2023 03:00:18 -0700 (PDT) References: <87mt4qibnk.fsf@gmail.com> <83cz5m8515.fsf@gnu.org> <874jqx3h47.fsf@gmail.com> <83lek97mm1.fsf@gnu.org> <877cvtfz37.fsf@gmail.com> <83356by7fr.fsf@gnu.org> <87h6urkuyx.fsf@gmail.com> <83fsaauywx.fsf@gnu.org> User-agent: mu4e 1.9.3; emacs 30.0.50 From: Wilhelm Kirschbaum To: Eli Zaretskii Subject: Re: bug#61996: 30.0.50; Submitting elixir-ts-mode and heex-ts-mode Date: Sun, 12 Mar 2023 11:54:33 +0200 In-reply-to: <83fsaauywx.fsf@gnu.org> Message-ID: <87sfeas30x.fsf@gmail.com> MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="=-=-=" X-Spam-Score: 1.0 (+) X-Debbugs-Envelope-To: 61996 Cc: 61996@debbugs.gnu.org, theo@thornhill.no, casouri@gmail.com X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: debbugs-submit-bounces@debbugs.gnu.org Sender: "Debbugs-submit" X-Spam-Score: -1.0 (-) --=-=-= Content-Type: text/plain; format=flowed Eli Zaretskii writes: >> From: Wilhelm Kirschbaum >> Cc: 61996@debbugs.gnu.org, casouri@gmail.com, theo@thornhill.no >> Date: Sat, 11 Mar 2023 20:01:32 +0200 >> >> > Please accompany the changes with a commit log message >> > according >> > to our conventions (see CONTRIBUTE for the conventions; >> > search for >> > "ChangeLog" there). In this case, just "New file" log should >> > be >> > sufficient for the new files you add. >> > >> Thanks, was not aware of it. I hope it is correct in the new >> patches. > > Yes, it is. Thanks. > >> The new patches should hopefully cover all of the above issues. > > Almost there. Byte compiler shows warnings, which I think are > real > problems in the code: > > In elixir-ts--call-parent-start: > progmodes/elixir-ts-mode.el:459:38: Warning: Unused lexical > argument `node' > progmodes/elixir-ts-mode.el:463:15: Warning: reference to free > variable `parent' > > In elixir-ts--forward-sexp: > progmodes/elixir-ts-mode.el:482:8: Warning: reference to free > variable `heex-ts--sexp-regexp' Ah, not sure how I missed them. The new patches have further tweaks and should resolve the above issue. --=-=-= Content-Type: text/x-patch Content-Disposition: attachment; filename=0001-Add-heex-ts-mode-Bug-61996.patch Content-Description: Add heex-ts-mode >From 2ad22dd0255a1a609e8bc13edff6e871c7c846d4 Mon Sep 17 00:00:00 2001 From: Wilhelm H Kirschbaum Date: Sun, 12 Mar 2023 11:44:36 +0200 Subject: [PATCH 1/2] Add heex-ts-mode (Bug#61996) --- admin/notes/tree-sitter/build-module/batch.sh | 1 + admin/notes/tree-sitter/build-module/build.sh | 3 + etc/NEWS | 3 + lisp/progmodes/heex-ts-mode.el | 185 ++++++++++++++++++ .../heex-ts-mode-resources/indent.erts | 47 +++++ test/lisp/progmodes/heex-ts-mode-tests.el | 9 + 6 files changed, 248 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/admin/notes/tree-sitter/build-module/batch.sh b/admin/notes/tree-sitter/build-module/batch.sh index 58272c74549..8b0072782e8 100755 --- a/admin/notes/tree-sitter/build-module/batch.sh +++ b/admin/notes/tree-sitter/build-module/batch.sh @@ -10,6 +10,7 @@ languages= 'dockerfile' 'go' 'go-mod' + 'heex' 'html' 'javascript' 'json' diff --git a/admin/notes/tree-sitter/build-module/build.sh b/admin/notes/tree-sitter/build-module/build.sh index 9dc674237ca..78ecfb5bc82 100755 --- a/admin/notes/tree-sitter/build-module/build.sh +++ b/admin/notes/tree-sitter/build-module/build.sh @@ -36,6 +36,9 @@ grammardir= lang="gomod" org="camdencheek" ;; + "heex") + org="phoenixframework" + ;; "typescript") sourcedir="tree-sitter-typescript/typescript/src" grammardir="tree-sitter-typescript/typescript" diff --git a/etc/NEWS b/etc/NEWS index 13d073c7fb8..ed74d0be1a1 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -230,6 +230,9 @@ This replaces 'doc-view-svg-foreground' and 'doc-view-svg-background'. An optional major mode based on the tree-sitter library for editing HTML files. +*** New major mode heex-ts-mode'. +A major mode based on the tree-sitter library for editing HEEx files. + --- ** The highly accessible Modus themes collection has six items. The 'modus-operandi' and 'modus-vivendi' are the main themes that have diff --git a/lisp/progmodes/heex-ts-mode.el b/lisp/progmodes/heex-ts-mode.el new file mode 100644 index 00000000000..68a537b9229 --- /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-node-start "treesit.c") + +(defgroup heex-ts nil + "Major mode for editing HEEx code." + :prefix "heex-ts-" + :group 'langauges) + +(defcustom heex-ts-indent-offset 2 + "Indentation of HEEx statements." + :version "30.1" + :type 'integer + :safe 'integerp + :group 'heex-ts) + +(defconst heex-ts--sexp-regexp + (rx bol + (or "directive" "tag" "component" "slot" + "attribute" "attribute_value" "quoted_attribute_value") + eol)) + +;; There seems to be no parent directive block for tree-sitter-heex, +;; so we ignore them for now until we learn how to query them. +;; https://github.com/phoenixframework/tree-sitter-heex/issues/28 +(defvar heex-ts--indent-rules + (let ((offset heex-ts-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--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--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--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) + heex-ts--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--forward-sexp) + + ;; Navigation. + (setq-local treesit-defun-type-regexp + (rx bol (or "component" "tag" "slot") eol)) + (setq-local treesit-defun-name-function #'heex-ts--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--font-lock-settings) + + (setq-local treesit-simple-indent-rules heex-ts--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-Bug-61996.patch Content-Description: Add elixir-ts-mode >From 6b7d72facfb397c8cfdc165829d79cf75ab68098 Mon Sep 17 00:00:00 2001 From: Wilhelm H Kirschbaum Date: Sun, 12 Mar 2023 11:45:11 +0200 Subject: [PATCH 2/2] 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. --- admin/notes/tree-sitter/build-module/batch.sh | 1 + admin/notes/tree-sitter/build-module/build.sh | 3 + etc/NEWS | 4 + lisp/progmodes/eglot.el | 2 +- lisp/progmodes/elixir-ts-mode.el | 620 ++++++++++++++++++ .../elixir-ts-mode-resources/indent.erts | 233 +++++++ test/lisp/progmodes/elixir-ts-mode-tests.el | 31 + 7 files changed, 893 insertions(+), 1 deletion(-) 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/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= 'css' 'c-sharp' 'dockerfile' + 'elixir' 'go' 'go-mod' '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 @@ grammardir= "cmake") org="uyha" ;; + "elixir") + org="elixir-lang" + ;; "go-mod") # The parser is called "gomod". lang="gomod" diff --git a/etc/NEWS b/etc/NEWS index ed74d0be1a1..d40fa51d80d 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -233,6 +233,10 @@ HTML files. *** New major mode heex-ts-mode'. A major mode based on the tree-sitter library for editing HEEx files. +*** New major mode elixir-ts-mode'. +A major mode based on the tree-sitter library for editing Elixir +files. + --- ** The highly accessible Modus themes collection has six items. The '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 @@ eglot-server-programs ((java-mode java-ts-mode) . ("jdtls")) (dart-mode . ("dart" "language-server" "--client-id" "emacs.eglot-dart")) - (elixir-mode . ("language_server.sh")) + ((elixir-ts-mode elixir-mode) . ("language_server.sh")) (ada-mode . ("ada_language_server")) (scala-mode . ,(eglot-alternatives '("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..08d73cd55eb --- /dev/null +++ b/lisp/progmodes/elixir-ts-mode.el @@ -0,0 +1,620 @@ +;;; 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-node-eq "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) + +(defface elixir-ts-font-comment-doc-identifier-face + '((t (:inherit font-lock-doc-face))) + "Face used for @comment.doc tags in Elixir files.") + +(defface elixir-ts-font-comment-doc-attribute-face + '((t (:inherit font-lock-doc-face))) + "Face used for @comment.doc.__attribute__ tags in Elixir files.") + +(defface elixir-ts-font-sigil-name-face + '((t (:inherit font-lock-string-face))) + "Face used for @__name__ tags in Elixir files.") + +(defconst elixir-ts--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--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 elixir-ts--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'.") + +(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 "^|$") parent-bol 0) + ((node-is "^}$") parent-bol 0) + ((node-is "^)$") + (lambda (_node parent &rest _) + (elixir-ts--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--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$") + ;; If there is no previous sibling indent + ;; to the call parent, otherwise + ;; indent to the same column as the prev-sibling. + (lambda (node parent &rest _) + (let ((prev-sibling (treesit-node-prev-sibling node t))) + (if prev-sibling + (treesit-node-start prev-sibling) + (elixir-ts--call-parent-start parent)))) + (lambda (node parent &rest _) + (if (treesit-node-prev-sibling node t) 0 ,offset))) + ;; 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. + ((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--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--capture-anonymous-function-end) parent-bol 0) + ((node-is "^end$") + (lambda (_node parent &rest _) + (elixir-ts--call-parent-start 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))))) + +(defvar elixir-ts--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 + `(,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))) + + :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--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--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--definition-keywords-re @font-lock-keyword-face)) + (call + target: (identifier) @font-lock-keyword-face + (:match ,elixir-ts--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--definition-keywords-re @font-lock-keyword-face)) + (call + target: (identifier) @font-lock-keyword-face + (arguments + (binary_operator + operator: "|>" + right: (identifier))) + (:match ,elixir-ts--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--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.") + +(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))))) + +(defun elixir-ts--call-parent-start (node) + "Return the closest parent of NODE that is of type call." + (let ((call-parent + (or (treesit-parent-until + node + (lambda (n) + (equal (treesit-node-type n) "call"))) + node))) + (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))))) + +(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* ((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--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))) + +;;;###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") + + (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. + (if (treesit-ready-p 'heex) + (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-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--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-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..e2c97a787b5 --- /dev/null +++ b/test/lisp/progmodes/elixir-ts-mode-resources/indent.erts @@ -0,0 +1,233 @@ +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: With statement +=-= +with one <- one(), + two <- two(), + {:ok, value} <- get_value(one, two) do + {:ok, value} +else + {:error, %{"Message" => message}} -> + {:error, message} +end +=-=-= + +Name: Pipe statements with fn + +=-= +[1, 2] +|> Enum.map(fn num -> + num + 1 +end) +=-=-= + +Name: Pipe statements stab clases + +=-= +[1, 2] +|> Enum.map(fn + x when x < 10 -> x * 2 + x -> x * 3 +end) +=-=-= + +Name: Pipe statements params + +=-= +[1, 2] +|> foobar( + :one, + :two, + :three, + :four +) +=-=-= + +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: Spec and method + +=-= +@spec foobar( + t, + acc, + (one, something -> :bar | far), + (two -> :bar | far) + ) :: any() + when chunk: any +def foobar(enumerable, acc, chunk_fun, after_fun) do + {_, {res, acc}} = + case after_fun.(acc) do + {:one, "one"} -> + "one" + + {:two, "two"} -> + "two" + end +end +=-=-= + + +Name: Spec with multi-line result + +=-= +@type result :: + {:done, term} + | {:two} + | {:one} + +@type result :: + { + :done, + term + } + | {:two} + | {:one} + +@type boo_bar :: + (foo :: pos_integer, bar :: pos_integer -> any()) + +@spec foo_bar( + t, + (foo -> any), + (() -> any) | (foo, foo -> boolean) | module() + ) :: any + when foo: any +def foo(one, fun, other) +=-=-= + + +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 --=-=-=-- From debbugs-submit-bounces@debbugs.gnu.org Sun Mar 12 07:37:43 2023 Received: (at 61996) by debbugs.gnu.org; 12 Mar 2023 11:37:43 +0000 Received: from localhost ([127.0.0.1]:59335 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1pbK1H-000320-BY for submit@debbugs.gnu.org; Sun, 12 Mar 2023 07:37:43 -0400 Received: from eggs.gnu.org ([209.51.188.92]:56078) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1pbK1F-00031k-JG for 61996@debbugs.gnu.org; Sun, 12 Mar 2023 07:37:42 -0400 Received: from fencepost.gnu.org ([2001:470:142:3::e]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1pbK19-0004OM-Ka; Sun, 12 Mar 2023 07:37:35 -0400 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=gnu.org; s=fencepost-gnu-org; h=References:Subject:In-Reply-To:To:From:Date: mime-version; bh=qLwGfZmaFLlKEk9p0p6Cz7vcLE/Ydd7yi1D6caiezWY=; b=nezdz3LTAXBP ogEZDIP4W25tDBaR0/yCWUUyDRhnZR9NygC6ZxwWNB/WoMm0ZrjJSB9ZnBFZrMqmaQZnpNrscc39H /NMIg5ou8MccV9KB3WGbX917+5YVbhnkMVQ+O5VcDgSmbhnL6i5Pxy15UsLhvwj0MriwIFu4VtbT/ u8dZ/zbyz/UoPhpMDT2ggsPPm1+euRXz+YLUybTUik8UcbEdOJkUh7+3C9RWU+PxNuwH4+OYpUftq U01Q7cxn7w0gmGgDWLMsNc+BEEsYub8DeE4cxSkl3Qsxr2eKrIFsutrEcAxrAjxAVe40/9sxWBgXc eTHptmLqNRwA64ZaZGiIkg==; Received: from [87.69.77.57] (helo=home-c4e4a596f7) by fencepost.gnu.org with esmtpsa (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1pbK17-0008Js-6I; Sun, 12 Mar 2023 07:37:34 -0400 Date: Sun, 12 Mar 2023 13:37:21 +0200 Message-Id: <837cvmurny.fsf@gnu.org> From: Eli Zaretskii To: Wilhelm Kirschbaum In-Reply-To: <87sfeas30x.fsf@gmail.com> (message from Wilhelm Kirschbaum on Sun, 12 Mar 2023 11:54:33 +0200) Subject: Re: bug#61996: 30.0.50; Submitting elixir-ts-mode and heex-ts-mode References: <87mt4qibnk.fsf@gmail.com> <83cz5m8515.fsf@gnu.org> <874jqx3h47.fsf@gmail.com> <83lek97mm1.fsf@gnu.org> <877cvtfz37.fsf@gmail.com> <83356by7fr.fsf@gnu.org> <87h6urkuyx.fsf@gmail.com> <83fsaauywx.fsf@gnu.org> <87sfeas30x.fsf@gmail.com> X-Spam-Score: -2.3 (--) X-Debbugs-Envelope-To: 61996 Cc: 61996@debbugs.gnu.org, theo@thornhill.no, casouri@gmail.com X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: debbugs-submit-bounces@debbugs.gnu.org Sender: "Debbugs-submit" X-Spam-Score: -3.3 (---) > From: Wilhelm Kirschbaum > Cc: 61996@debbugs.gnu.org, casouri@gmail.com, theo@thornhill.no > Date: Sun, 12 Mar 2023 11:54:33 +0200 > > Eli Zaretskii writes: > > > In elixir-ts--call-parent-start: > > progmodes/elixir-ts-mode.el:459:38: Warning: Unused lexical > > argument `node' > > progmodes/elixir-ts-mode.el:463:15: Warning: reference to free > > variable `parent' > > > > In elixir-ts--forward-sexp: > > progmodes/elixir-ts-mode.el:482:8: Warning: reference to free > > variable `heex-ts--sexp-regexp' > > Ah, not sure how I missed them. The new patches have further > tweaks and > should resolve the above issue. Thanks, but the first of the two patches lacks the commit log message. And since you said there are further tweaks, I wasn't sure the one from the previous version was still accurate. From debbugs-submit-bounces@debbugs.gnu.org Sun Mar 12 08:30:37 2023 Received: (at 61996) by debbugs.gnu.org; 12 Mar 2023 12:30:37 +0000 Received: from localhost ([127.0.0.1]:59380 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1pbKqR-0004Sz-22 for submit@debbugs.gnu.org; Sun, 12 Mar 2023 08:30:37 -0400 Received: from mail-wm1-f54.google.com ([209.85.128.54]:40710) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1pbKqN-0004Sk-Ne for 61996@debbugs.gnu.org; Sun, 12 Mar 2023 08:30:34 -0400 Received: by mail-wm1-f54.google.com with SMTP id fm20-20020a05600c0c1400b003ead37e6588so9096502wmb.5 for <61996@debbugs.gnu.org>; Sun, 12 Mar 2023 05:30:31 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20210112; t=1678624226; 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=/jRT0RlI3wkxKeZFWcJw6TpZ+TRUQMTsB40akLwELYc=; b=pvXLsT+wQN0h2/pqcLgHPpua1chrBaB5X9dLVFdfQHk7v0ENY6H6RIG7swqy4mo8Ra WWkrNx6Vv0rjy0/tQo2HASSVeazfhoDkXzfm7fjAxpbm5FxcUD80SRDyA/ahBakUm3EJ hbebTubvEX7B2H+kgijo/Fs1dCt1c6ql1sAYVvxmMffP+C3m4qnPTTlSRU+K0pwqpEzH eoPJRFzFN1VMXyelzHpDVU6I8/ejE6RCKlApSOos2LmU9cCMUbHWb5RNCOxXZdfs+v95 D9Geq9CrFKk3hZLOH/NBpwMv+08OCkFfrOPbTHbjzx9Vw4fQh14057DgYBjlkRcYujEB eTcg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20210112; t=1678624226; 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=/jRT0RlI3wkxKeZFWcJw6TpZ+TRUQMTsB40akLwELYc=; b=56bFInLO0n+iOni1Jkn5I2lrL58ZBV25TNKly3FaTlUzx+uxVEvstUyiBu2Mi2ZZ1I C53CLFYv8WEtnoUrw57zh7jZQd50/rPYUmlIwFwPtM0jN5XqDEHnGTtJZa7lI9r94VfA 7jSPCSC+TdfSo/O2EiqJU+7q95H1JXn1ePjLi6zcxVH2XxoBwqZiAyO66xPmBzAYBqtD 9eRzP1aqKG337mDpwkT5u5/JQraS1wMPblBZxuK4pL1gDeLPVOSmCyYG/MrtEcDiscZJ j/TXnyNY6LNpf5ZLnQ1g1UTcDH3j89ILl6ByFd5sShtziPDXQADchhU9ZUozgy2YnPDd czHQ== X-Gm-Message-State: AO0yUKV5pyS0SpdHtfCUo9B96mKnfILW3yKwIXMsTmBhZsboyiveapc8 IwQD0PI2BqU0cs52bREa1Zw= X-Google-Smtp-Source: AK7set9OAV/qpygEqpCMAbhh0D20ORxGjPXZ8XuxzoJbd8cYTRLtRi/UDm2WNHuGrqHll0Q9XcDgMQ== X-Received: by 2002:a05:600c:4712:b0:3da:28a9:a900 with SMTP id v18-20020a05600c471200b003da28a9a900mr7683275wmo.41.1678624225739; Sun, 12 Mar 2023 05:30:25 -0700 (PDT) Received: from melissa.local ([2c0f:ef18:1431:0:b09:9616:db04:c248]) by smtp.gmail.com with ESMTPSA id p22-20020a05600c359600b003b47b80cec3sm6359733wmq.42.2023.03.12.05.30.23 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Sun, 12 Mar 2023 05:30:24 -0700 (PDT) References: <87mt4qibnk.fsf@gmail.com> <83cz5m8515.fsf@gnu.org> <874jqx3h47.fsf@gmail.com> <83lek97mm1.fsf@gnu.org> <877cvtfz37.fsf@gmail.com> <83356by7fr.fsf@gnu.org> <87h6urkuyx.fsf@gmail.com> <83fsaauywx.fsf@gnu.org> <87sfeas30x.fsf@gmail.com> <837cvmurny.fsf@gnu.org> User-agent: mu4e 1.9.3; emacs 30.0.50 From: Wilhelm Kirschbaum To: Eli Zaretskii Subject: Re: bug#61996: 30.0.50; Submitting elixir-ts-mode and heex-ts-mode Date: Sun, 12 Mar 2023 14:23:01 +0200 In-reply-to: <837cvmurny.fsf@gnu.org> Message-ID: <87fsaa6tk3.fsf@gmail.com> MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="=-=-=" X-Spam-Score: 1.0 (+) X-Debbugs-Envelope-To: 61996 Cc: 61996@debbugs.gnu.org, theo@thornhill.no, casouri@gmail.com X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: debbugs-submit-bounces@debbugs.gnu.org Sender: "Debbugs-submit" X-Spam-Score: -1.0 (-) --=-=-= Content-Type: text/plain; format=flowed Eli Zaretskii writes: >> From: Wilhelm Kirschbaum >> Cc: 61996@debbugs.gnu.org, casouri@gmail.com, theo@thornhill.no >> Date: Sun, 12 Mar 2023 11:54:33 +0200 >> >> Eli Zaretskii writes: >> >> > In elixir-ts--call-parent-start: >> > progmodes/elixir-ts-mode.el:459:38: Warning: Unused lexical >> > argument `node' >> > progmodes/elixir-ts-mode.el:463:15: Warning: reference to >> > free >> > variable `parent' >> > >> > In elixir-ts--forward-sexp: >> > progmodes/elixir-ts-mode.el:482:8: Warning: reference to >> > free >> > variable `heex-ts--sexp-regexp' >> >> Ah, not sure how I missed them. The new patches have further >> tweaks and >> should resolve the above issue. > > Thanks, but the first of the two patches lacks the commit log > message. And since you said there are further tweaks, I wasn't > sure > the one from the previous version was still accurate. Sorry about that. The workflow is still pretty foreign to me and juggling with the old github upstream. I added the commit log. --=-=-= Content-Type: text/x-patch Content-Disposition: attachment; filename=0001-Add-heex-ts-mode-Bug-61996.patch Content-Description: Add heex-ts-mode >From 2b4cee1de0b3a038f6c78da12ddddb338553878b Mon Sep 17 00:00:00 2001 From: Wilhelm H Kirschbaum Date: Sun, 12 Mar 2023 11:44:36 +0200 Subject: [PATCH 1/2] Add heex-ts-mode (Bug#61996) * etc/NEWS: Mention the new mode. * lisp/progmodes/heex-ts-mode.el: New file. * test/lisp/progmodes/heex-ts-mode-tests.el: New file. * test/lisp/progmodes/heex-ts-mode-resources/indent.erts: New file. * admin/notes/tree-sitter/build-module/batch.sh: * admin/notes/tree-sitter/build-module/build.sh: Add HEEx support. --- admin/notes/tree-sitter/build-module/batch.sh | 1 + admin/notes/tree-sitter/build-module/build.sh | 3 + etc/NEWS | 3 + lisp/progmodes/heex-ts-mode.el | 185 ++++++++++++++++++ .../heex-ts-mode-resources/indent.erts | 47 +++++ test/lisp/progmodes/heex-ts-mode-tests.el | 9 + 6 files changed, 248 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/admin/notes/tree-sitter/build-module/batch.sh b/admin/notes/tree-sitter/build-module/batch.sh index 58272c74549..8b0072782e8 100755 --- a/admin/notes/tree-sitter/build-module/batch.sh +++ b/admin/notes/tree-sitter/build-module/batch.sh @@ -10,6 +10,7 @@ languages= 'dockerfile' 'go' 'go-mod' + 'heex' 'html' 'javascript' 'json' diff --git a/admin/notes/tree-sitter/build-module/build.sh b/admin/notes/tree-sitter/build-module/build.sh index 9dc674237ca..78ecfb5bc82 100755 --- a/admin/notes/tree-sitter/build-module/build.sh +++ b/admin/notes/tree-sitter/build-module/build.sh @@ -36,6 +36,9 @@ grammardir= lang="gomod" org="camdencheek" ;; + "heex") + org="phoenixframework" + ;; "typescript") sourcedir="tree-sitter-typescript/typescript/src" grammardir="tree-sitter-typescript/typescript" diff --git a/etc/NEWS b/etc/NEWS index 13d073c7fb8..ed74d0be1a1 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -230,6 +230,9 @@ This replaces 'doc-view-svg-foreground' and 'doc-view-svg-background'. An optional major mode based on the tree-sitter library for editing HTML files. +*** New major mode heex-ts-mode'. +A major mode based on the tree-sitter library for editing HEEx files. + --- ** The highly accessible Modus themes collection has six items. The 'modus-operandi' and 'modus-vivendi' are the main themes that have diff --git a/lisp/progmodes/heex-ts-mode.el b/lisp/progmodes/heex-ts-mode.el new file mode 100644 index 00000000000..68a537b9229 --- /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-node-start "treesit.c") + +(defgroup heex-ts nil + "Major mode for editing HEEx code." + :prefix "heex-ts-" + :group 'langauges) + +(defcustom heex-ts-indent-offset 2 + "Indentation of HEEx statements." + :version "30.1" + :type 'integer + :safe 'integerp + :group 'heex-ts) + +(defconst heex-ts--sexp-regexp + (rx bol + (or "directive" "tag" "component" "slot" + "attribute" "attribute_value" "quoted_attribute_value") + eol)) + +;; There seems to be no parent directive block for tree-sitter-heex, +;; so we ignore them for now until we learn how to query them. +;; https://github.com/phoenixframework/tree-sitter-heex/issues/28 +(defvar heex-ts--indent-rules + (let ((offset heex-ts-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--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--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--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) + heex-ts--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--forward-sexp) + + ;; Navigation. + (setq-local treesit-defun-type-regexp + (rx bol (or "component" "tag" "slot") eol)) + (setq-local treesit-defun-name-function #'heex-ts--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--font-lock-settings) + + (setq-local treesit-simple-indent-rules heex-ts--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-Bug-61996.patch Content-Description: Add elixir-ts-mode >From 56f4a551bc65c9a64fdcf7fb7c2e0731f724ad99 Mon Sep 17 00:00:00 2001 From: Wilhelm H Kirschbaum Date: Sun, 12 Mar 2023 11:45:11 +0200 Subject: [PATCH 2/2] 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. --- admin/notes/tree-sitter/build-module/batch.sh | 1 + admin/notes/tree-sitter/build-module/build.sh | 3 + etc/NEWS | 4 + lisp/progmodes/eglot.el | 2 +- lisp/progmodes/elixir-ts-mode.el | 620 ++++++++++++++++++ .../elixir-ts-mode-resources/indent.erts | 233 +++++++ test/lisp/progmodes/elixir-ts-mode-tests.el | 31 + 7 files changed, 893 insertions(+), 1 deletion(-) 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/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= 'css' 'c-sharp' 'dockerfile' + 'elixir' 'go' 'go-mod' '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 @@ grammardir= "cmake") org="uyha" ;; + "elixir") + org="elixir-lang" + ;; "go-mod") # The parser is called "gomod". lang="gomod" diff --git a/etc/NEWS b/etc/NEWS index ed74d0be1a1..d40fa51d80d 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -233,6 +233,10 @@ HTML files. *** New major mode heex-ts-mode'. A major mode based on the tree-sitter library for editing HEEx files. +*** New major mode elixir-ts-mode'. +A major mode based on the tree-sitter library for editing Elixir +files. + --- ** The highly accessible Modus themes collection has six items. The '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 @@ eglot-server-programs ((java-mode java-ts-mode) . ("jdtls")) (dart-mode . ("dart" "language-server" "--client-id" "emacs.eglot-dart")) - (elixir-mode . ("language_server.sh")) + ((elixir-ts-mode elixir-mode) . ("language_server.sh")) (ada-mode . ("ada_language_server")) (scala-mode . ,(eglot-alternatives '("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..08d73cd55eb --- /dev/null +++ b/lisp/progmodes/elixir-ts-mode.el @@ -0,0 +1,620 @@ +;;; 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-node-eq "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) + +(defface elixir-ts-font-comment-doc-identifier-face + '((t (:inherit font-lock-doc-face))) + "Face used for @comment.doc tags in Elixir files.") + +(defface elixir-ts-font-comment-doc-attribute-face + '((t (:inherit font-lock-doc-face))) + "Face used for @comment.doc.__attribute__ tags in Elixir files.") + +(defface elixir-ts-font-sigil-name-face + '((t (:inherit font-lock-string-face))) + "Face used for @__name__ tags in Elixir files.") + +(defconst elixir-ts--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--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 elixir-ts--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'.") + +(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 "^|$") parent-bol 0) + ((node-is "^}$") parent-bol 0) + ((node-is "^)$") + (lambda (_node parent &rest _) + (elixir-ts--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--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$") + ;; If there is no previous sibling indent + ;; to the call parent, otherwise + ;; indent to the same column as the prev-sibling. + (lambda (node parent &rest _) + (let ((prev-sibling (treesit-node-prev-sibling node t))) + (if prev-sibling + (treesit-node-start prev-sibling) + (elixir-ts--call-parent-start parent)))) + (lambda (node parent &rest _) + (if (treesit-node-prev-sibling node t) 0 ,offset))) + ;; 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. + ((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--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--capture-anonymous-function-end) parent-bol 0) + ((node-is "^end$") + (lambda (_node parent &rest _) + (elixir-ts--call-parent-start 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))))) + +(defvar elixir-ts--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 + `(,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))) + + :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--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--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--definition-keywords-re @font-lock-keyword-face)) + (call + target: (identifier) @font-lock-keyword-face + (:match ,elixir-ts--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--definition-keywords-re @font-lock-keyword-face)) + (call + target: (identifier) @font-lock-keyword-face + (arguments + (binary_operator + operator: "|>" + right: (identifier))) + (:match ,elixir-ts--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--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.") + +(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))))) + +(defun elixir-ts--call-parent-start (node) + "Return the closest parent of NODE that is of type call." + (let ((call-parent + (or (treesit-parent-until + node + (lambda (n) + (equal (treesit-node-type n) "call"))) + node))) + (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))))) + +(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* ((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--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))) + +;;;###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") + + (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. + (if (treesit-ready-p 'heex) + (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-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--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-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..e2c97a787b5 --- /dev/null +++ b/test/lisp/progmodes/elixir-ts-mode-resources/indent.erts @@ -0,0 +1,233 @@ +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: With statement +=-= +with one <- one(), + two <- two(), + {:ok, value} <- get_value(one, two) do + {:ok, value} +else + {:error, %{"Message" => message}} -> + {:error, message} +end +=-=-= + +Name: Pipe statements with fn + +=-= +[1, 2] +|> Enum.map(fn num -> + num + 1 +end) +=-=-= + +Name: Pipe statements stab clases + +=-= +[1, 2] +|> Enum.map(fn + x when x < 10 -> x * 2 + x -> x * 3 +end) +=-=-= + +Name: Pipe statements params + +=-= +[1, 2] +|> foobar( + :one, + :two, + :three, + :four +) +=-=-= + +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: Spec and method + +=-= +@spec foobar( + t, + acc, + (one, something -> :bar | far), + (two -> :bar | far) + ) :: any() + when chunk: any +def foobar(enumerable, acc, chunk_fun, after_fun) do + {_, {res, acc}} = + case after_fun.(acc) do + {:one, "one"} -> + "one" + + {:two, "two"} -> + "two" + end +end +=-=-= + + +Name: Spec with multi-line result + +=-= +@type result :: + {:done, term} + | {:two} + | {:one} + +@type result :: + { + :done, + term + } + | {:two} + | {:one} + +@type boo_bar :: + (foo :: pos_integer, bar :: pos_integer -> any()) + +@spec foo_bar( + t, + (foo -> any), + (() -> any) | (foo, foo -> boolean) | module() + ) :: any + when foo: any +def foo(one, fun, other) +=-=-= + + +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 --=-=-=-- From debbugs-submit-bounces@debbugs.gnu.org Sun Mar 12 08:32:48 2023 Received: (at 61996) by debbugs.gnu.org; 12 Mar 2023 12:32:48 +0000 Received: from localhost ([127.0.0.1]:59384 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1pbKsa-0004WF-D1 for submit@debbugs.gnu.org; Sun, 12 Mar 2023 08:32:48 -0400 Received: from mail-wm1-f51.google.com ([209.85.128.51]:35758) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1pbKsX-0004W2-Va for 61996@debbugs.gnu.org; Sun, 12 Mar 2023 08:32:46 -0400 Received: by mail-wm1-f51.google.com with SMTP id bg16-20020a05600c3c9000b003eb34e21bdfso9141148wmb.0 for <61996@debbugs.gnu.org>; Sun, 12 Mar 2023 05:32:45 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20210112; t=1678624360; 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=qu3dKRwehdgsox1ngqfvxs6+SWCOe1q8PpCympGYZgk=; b=Z/94KEoXRS4AKd3nyYBOivnjuvnJ08jDwtw90yEf/crHOBJVr5ZKmFnSmoq5cUTiWA r9mc2wTy7IXX8KwcJlKlJl8Z5K/Yv1V4TmukA/CJGT0b6bfbzt0hliojL+XF0Uggiqw9 +RtNX+4k6+ijm9dooViqO3dcBfHTd/75ZtBhgD9/VUUTh4P3TxEdMDmG8WhSkdjcG/+4 PB7eN3RECraW28t4EGyljEjJB+FtRaaIYwgzcjxKlFlYa5S7xP54xPzNMS0RBzmBUPYE xmG+ob8Mu4Fod4Yy8Pc0BtzbCgilfJCUzFaLju/Mx3+szN+6rprO//dXqt7uZH0nQuIQ 69dQ== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20210112; t=1678624360; 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=qu3dKRwehdgsox1ngqfvxs6+SWCOe1q8PpCympGYZgk=; b=6knoPMpFXhPVwUyinLzAMMUl5jtVPcq7o0Uo36NGi7dJK8e/G6QlAlW4IQ/b7pUDgQ KrIW7YJTgVdDcI+EITPiWaWB21zJ1fSZLO/6ZYwGqdRSqi/ujiuw3Uf9fkadzxHJWh6P XVl3IR3DKO0ulf6kmqt1MFklzhmZJ0ikwRFYTR5KJr1uzuqAvqEoUr6dx/S7+eTJ6sTT zkXhcZLHLzyySVqWH9mXTnkZce32n6s80pYIenVbLmSuXdW14N8m4WDUzt3RLAO2WqcP 99SYiBwuOZMnfa4g7SBFzLEgUDGjIo43sUS3r9QgGphkpFYD39fxaXyNsglXa70zN2iu /gwA== X-Gm-Message-State: AO0yUKVp22HyDw51yf20jfk3K6l+rJzh52N4SkQzPLllBB0KcowjbLxa lLkmSrov5qAlUk42b8UoDO2rstBkYsrrh441 X-Google-Smtp-Source: AK7set8XP++9UCeFDAGpZ69JRDXLQxmbAyE1ztX8zP6JKNzs3z+w97GvnESHe4ftof+Gt8VVUdYjVw== X-Received: by 2002:a05:600c:4e8e:b0:3e0:1a9:b1d7 with SMTP id f14-20020a05600c4e8e00b003e001a9b1d7mr8288870wmq.19.1678624360024; Sun, 12 Mar 2023 05:32:40 -0700 (PDT) Received: from melissa.local ([2c0f:ef18:1431:0:b09:9616:db04:c248]) by smtp.gmail.com with ESMTPSA id 17-20020a05600c021100b003ed2002fa6dsm2266951wmi.17.2023.03.12.05.32.38 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Sun, 12 Mar 2023 05:32:39 -0700 (PDT) References: <87mt4qibnk.fsf@gmail.com> <83cz5m8515.fsf@gnu.org> <874jqx3h47.fsf@gmail.com> <83lek97mm1.fsf@gnu.org> <877cvtfz37.fsf@gmail.com> <83356by7fr.fsf@gnu.org> <87h6urkuyx.fsf@gmail.com> <83fsaauywx.fsf@gnu.org> <87sfeas30x.fsf@gmail.com> <837cvmurny.fsf@gnu.org> <87fsaa6tk3.fsf@gmail.com> User-agent: mu4e 1.9.3; emacs 30.0.50 From: Wilhelm Kirschbaum To: Eli Zaretskii Subject: Re: bug#61996: 30.0.50; Submitting elixir-ts-mode and heex-ts-mode Date: Sun, 12 Mar 2023 14:32:08 +0200 In-reply-to: <87fsaa6tk3.fsf@gmail.com> Message-ID: <87bkky6tgb.fsf@gmail.com> MIME-Version: 1.0 Content-Type: text/plain; format=flowed X-Spam-Score: 1.0 (+) X-Debbugs-Envelope-To: 61996 Cc: 61996@debbugs.gnu.org, theo@thornhill.no, casouri@gmail.com X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: debbugs-submit-bounces@debbugs.gnu.org Sender: "Debbugs-submit" X-Spam-Score: -1.0 (-) Wilhelm Kirschbaum writes: > Eli Zaretskii writes: > >>> From: Wilhelm Kirschbaum >>> Cc: 61996@debbugs.gnu.org, casouri@gmail.com, >>> theo@thornhill.no >>> Date: Sun, 12 Mar 2023 11:54:33 +0200 >>> Eli Zaretskii writes: >>> > In elixir-ts--call-parent-start: >>> > progmodes/elixir-ts-mode.el:459:38: Warning: Unused >>> > lexical > >>> argument `node' >>> > progmodes/elixir-ts-mode.el:463:15: Warning: reference to >>> > > >>> free > variable `parent' >>> > >>> > In elixir-ts--forward-sexp: >>> > progmodes/elixir-ts-mode.el:482:8: Warning: reference to >>> > > >>> free > variable `heex-ts--sexp-regexp' >>> Ah, not sure how I missed them. The new patches have further >>> tweaks >>> and >>> should resolve the above issue. >> >> Thanks, but the first of the two patches lacks the commit log >> message. And since you said there are further tweaks, I wasn't >> sure >> the one from the previous version was still accurate. > > Sorry about that. The workflow is still pretty foreign to me and > juggling with the old github upstream. I added the commit log. > > [2. Add heex-ts-mode --- text/x-patch; > 0001-Add-heex-ts-mode-Bug-61996.patch]... > > [3. Add elixir-ts-mode --- text/x-patch; > 0002-Add-elixir-ts-mode-Bug-61996.patch]... Please ignore the previous patch, need to fix some tests. From debbugs-submit-bounces@debbugs.gnu.org Sun Mar 12 11:24:33 2023 Received: (at 61996) by debbugs.gnu.org; 12 Mar 2023 15:24:33 +0000 Received: from localhost ([127.0.0.1]:32964 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1pbNYk-00015Z-5T for submit@debbugs.gnu.org; Sun, 12 Mar 2023 11:24:33 -0400 Received: from mail-wm1-f41.google.com ([209.85.128.41]:38551) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1pbNYg-00015D-E7 for 61996@debbugs.gnu.org; Sun, 12 Mar 2023 11:24:29 -0400 Received: by mail-wm1-f41.google.com with SMTP id o5-20020a05600c510500b003ec0e4ec6deso3197904wms.3 for <61996@debbugs.gnu.org>; Sun, 12 Mar 2023 08:24:26 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20210112; t=1678634660; 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=EtriDqUh5r0VKD83LbiOdA8sn50UyNTiGoPd2d0Q1yM=; b=QuTdS38MG/Iz8UUPGdNqTu0S5TX2/lyN3+US64QoB5RdWH2MF8YjCqD2jf7k4JrivJ x9P/cuJ8qeaZg5DS9m9x0pLqbbX7JApvEKcoGMupePCV/vrg0YOaIvOdLD+yAIkK9R9d ncraUaYXqcSk/k7J6gOB8IkXmy1XbPj0tIui3q+W26W8gcD3oDJMjcHYw0sDp/WJGu4W N9Fqp6X18GYGugIS2WctrO7Aqo3jq5aWPT1C7+6PpcAzizArKf1pUxtdLjEOMJSLRMcj COHKJHhwQhr/VKE9jNrQFNd8Gh0D1iWFqcThA5ZkvDt9xh+bd1kMl7ickkecl8AaR8Oa hYtw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20210112; t=1678634660; 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=EtriDqUh5r0VKD83LbiOdA8sn50UyNTiGoPd2d0Q1yM=; b=QZcksgvQVtSOhpsCMWk0c2KDEqwtS/nCFHVFVuuZLD/OrBgegm4TgpKTl2uvSuhNpO J152dHPnqlt+PsooMuXKwLoYfgrioyT9rtMJO+5G0AD1hyWAHyM4VgKC8llroMly7Lxh oXx2YYBDYezBTBenwAlo9N8LI46U62Gl36O61y90FqQ1sN7wz+4F/f3yLyALOtHWQaKg 0yGTJ4xbTLsARC1L1CCWqny7+qvxIw64F9tzLJtDVn8q5nCc6szyB3r18TjN2c1PXLGB zwjK20hMtsir6/UfogtsfTKaM3uHPvaDqG1k6DcqKquz/K6E6dLqsgGxw5oIYQRXVhnY OOhg== X-Gm-Message-State: AO0yUKVCWeeA0VeckJ+iRApS/WAPH3Gb4GjPHje1BwK0VEHLZU8dAe9S NkI7cacQSqZ9gS5FZam3mJU= X-Google-Smtp-Source: AK7set9r8iZe+CY/1EnnYC2m+R4DsH9Ylgkcg4ZNtBJqMRQb1SuEnmef8FPXdwEEP7Hk2vTt544Lnw== X-Received: by 2002:a05:600c:1c96:b0:3eb:399d:ab24 with SMTP id k22-20020a05600c1c9600b003eb399dab24mr8107097wms.28.1678634660284; Sun, 12 Mar 2023 08:24:20 -0700 (PDT) Received: from melissa.local ([2c0f:ef18:1431:0:b09:9616:db04:c248]) by smtp.gmail.com with ESMTPSA id w19-20020a05600c475300b003eb2e33f327sm8165335wmo.2.2023.03.12.08.24.17 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Sun, 12 Mar 2023 08:24:19 -0700 (PDT) References: <87mt4qibnk.fsf@gmail.com> <83cz5m8515.fsf@gnu.org> <874jqx3h47.fsf@gmail.com> <83lek97mm1.fsf@gnu.org> <877cvtfz37.fsf@gmail.com> <83356by7fr.fsf@gnu.org> <87h6urkuyx.fsf@gmail.com> <83fsaauywx.fsf@gnu.org> <87sfeas30x.fsf@gmail.com> <837cvmurny.fsf@gnu.org> User-agent: mu4e 1.9.3; emacs 30.0.50 From: Wilhelm Kirschbaum To: Eli Zaretskii Subject: Re: bug#61996: 30.0.50; Submitting elixir-ts-mode and heex-ts-mode Date: Sun, 12 Mar 2023 17:14:03 +0200 In-reply-to: <837cvmurny.fsf@gnu.org> Message-ID: <87a60i2dsw.fsf@gmail.com> MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="=-=-=" X-Spam-Score: 1.0 (+) X-Debbugs-Envelope-To: 61996 Cc: 61996@debbugs.gnu.org, theo@thornhill.no, casouri@gmail.com X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: debbugs-submit-bounces@debbugs.gnu.org Sender: "Debbugs-submit" X-Spam-Score: -1.0 (-) --=-=-= Content-Type: text/plain; charset=utf-8; format=flowed Content-Transfer-Encoding: quoted-printable Eli Zaretskii writes: >> From: Wilhelm Kirschbaum >> Cc: 61996@debbugs.gnu.org, casouri@gmail.com, theo@thornhill.no >> Date: Sun, 12 Mar 2023 11:54:33 +0200 >>=20 >> Eli Zaretskii writes: >>=20 >> > In elixir-ts--call-parent-start: >> > progmodes/elixir-ts-mode.el:459:38: Warning: Unused lexical=20 >> > argument `node' >> > progmodes/elixir-ts-mode.el:463:15: Warning: reference to=20 >> > free=20 >> > variable `parent' >> > >> > In elixir-ts--forward-sexp: >> > progmodes/elixir-ts-mode.el:482:8: Warning: reference to=20 >> > free=20 >> > variable `heex-ts--sexp-regexp' >>=20 >> Ah, not sure how I missed them. The new patches have further=20 >> tweaks and >> should resolve the above issue. > > Thanks, but the first of the two patches lacks the commit log > message. And since you said there are further tweaks, I wasn't=20 > sure > the one from the previous version was still accurate. Attached are the updated patches with added test cases and=20 indentation rule enhancements. I still see this warning on a non-treesitter build: In elixir-ts--forward-sexp: elixir-ts-mode.el:490:8: Warning: reference to free variable =E2=80=98heex-ts--sexp-regexp=E2=80=99 But not sure why and how to fix it, because defcons=20 heex-ts--sexp-regexp and (require 'heex-ts-mode) is called. Random concern: how will backwards compatibility work when the=20 grammars get updated? It might just be better to add both modes in one patch if more=20 changes are required perhaps?=20 --=-=-= Content-Type: text/x-patch Content-Disposition: attachment; filename=0001-Add-heex-ts-mode-Bug-61996.patch Content-Description: Add heex-ts-mode >From 7544e2fe4192b7143d723bc811eeb127bbc1e1e3 Mon Sep 17 00:00:00 2001 From: Wilhelm H Kirschbaum Date: Sun, 12 Mar 2023 17:08:50 +0200 Subject: [PATCH 1/2] Add heex-ts-mode (Bug#61996) * etc/NEWS: Mention the new mode. * lisp/progmodes/heex-ts-mode.el: New file. * test/lisp/progmodes/heex-ts-mode-tests.el: New file. * test/lisp/progmodes/heex-ts-mode-resources/indent.erts: New file. * admin/notes/tree-sitter/build-module/batch.sh: * admin/notes/tree-sitter/build-module/build.sh: Add HEEx support. --- admin/notes/tree-sitter/build-module/batch.sh | 1 + admin/notes/tree-sitter/build-module/build.sh | 3 + etc/NEWS | 3 + lisp/progmodes/heex-ts-mode.el | 185 ++++++++++++++++++ .../heex-ts-mode-resources/indent.erts | 47 +++++ test/lisp/progmodes/heex-ts-mode-tests.el | 9 + 6 files changed, 248 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/admin/notes/tree-sitter/build-module/batch.sh b/admin/notes/tree-sitter/build-module/batch.sh index 58272c74549..8b0072782e8 100755 --- a/admin/notes/tree-sitter/build-module/batch.sh +++ b/admin/notes/tree-sitter/build-module/batch.sh @@ -10,6 +10,7 @@ languages= 'dockerfile' 'go' 'go-mod' + 'heex' 'html' 'javascript' 'json' diff --git a/admin/notes/tree-sitter/build-module/build.sh b/admin/notes/tree-sitter/build-module/build.sh index 9dc674237ca..78ecfb5bc82 100755 --- a/admin/notes/tree-sitter/build-module/build.sh +++ b/admin/notes/tree-sitter/build-module/build.sh @@ -36,6 +36,9 @@ grammardir= lang="gomod" org="camdencheek" ;; + "heex") + org="phoenixframework" + ;; "typescript") sourcedir="tree-sitter-typescript/typescript/src" grammardir="tree-sitter-typescript/typescript" diff --git a/etc/NEWS b/etc/NEWS index 13d073c7fb8..ed74d0be1a1 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -230,6 +230,9 @@ This replaces 'doc-view-svg-foreground' and 'doc-view-svg-background'. An optional major mode based on the tree-sitter library for editing HTML files. +*** New major mode heex-ts-mode'. +A major mode based on the tree-sitter library for editing HEEx files. + --- ** The highly accessible Modus themes collection has six items. The 'modus-operandi' and 'modus-vivendi' are the main themes that have diff --git a/lisp/progmodes/heex-ts-mode.el b/lisp/progmodes/heex-ts-mode.el new file mode 100644 index 00000000000..68a537b9229 --- /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-node-start "treesit.c") + +(defgroup heex-ts nil + "Major mode for editing HEEx code." + :prefix "heex-ts-" + :group 'langauges) + +(defcustom heex-ts-indent-offset 2 + "Indentation of HEEx statements." + :version "30.1" + :type 'integer + :safe 'integerp + :group 'heex-ts) + +(defconst heex-ts--sexp-regexp + (rx bol + (or "directive" "tag" "component" "slot" + "attribute" "attribute_value" "quoted_attribute_value") + eol)) + +;; There seems to be no parent directive block for tree-sitter-heex, +;; so we ignore them for now until we learn how to query them. +;; https://github.com/phoenixframework/tree-sitter-heex/issues/28 +(defvar heex-ts--indent-rules + (let ((offset heex-ts-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--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--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--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) + heex-ts--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--forward-sexp) + + ;; Navigation. + (setq-local treesit-defun-type-regexp + (rx bol (or "component" "tag" "slot") eol)) + (setq-local treesit-defun-name-function #'heex-ts--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--font-lock-settings) + + (setq-local treesit-simple-indent-rules heex-ts--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-Bug-61996.patch Content-Description: Add elixir-ts-mode >From 6eb506f39bef5e210573e2b7dae2006a13098e2f Mon Sep 17 00:00:00 2001 From: Wilhelm H Kirschbaum Date: Sun, 12 Mar 2023 17:10:43 +0200 Subject: [PATCH 2/2] 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. --- admin/notes/tree-sitter/build-module/batch.sh | 1 + admin/notes/tree-sitter/build-module/build.sh | 3 + etc/NEWS | 4 + lisp/progmodes/eglot.el | 2 +- lisp/progmodes/elixir-ts-mode.el | 634 ++++++++++++++++++ .../elixir-ts-mode-resources/indent.erts | 308 +++++++++ test/lisp/progmodes/elixir-ts-mode-tests.el | 31 + 7 files changed, 982 insertions(+), 1 deletion(-) 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/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= 'css' 'c-sharp' 'dockerfile' + 'elixir' 'go' 'go-mod' '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 @@ grammardir= "cmake") org="uyha" ;; + "elixir") + org="elixir-lang" + ;; "go-mod") # The parser is called "gomod". lang="gomod" diff --git a/etc/NEWS b/etc/NEWS index ed74d0be1a1..d40fa51d80d 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -233,6 +233,10 @@ HTML files. *** New major mode heex-ts-mode'. A major mode based on the tree-sitter library for editing HEEx files. +*** New major mode elixir-ts-mode'. +A major mode based on the tree-sitter library for editing Elixir +files. + --- ** The highly accessible Modus themes collection has six items. The '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 @@ eglot-server-programs ((java-mode java-ts-mode) . ("jdtls")) (dart-mode . ("dart" "language-server" "--client-id" "emacs.eglot-dart")) - (elixir-mode . ("language_server.sh")) + ((elixir-ts-mode elixir-mode) . ("language_server.sh")) (ada-mode . ("ada_language_server")) (scala-mode . ,(eglot-alternatives '("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 @@ +;;; 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-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) + +(defface elixir-ts-font-comment-doc-identifier-face + '((t (:inherit font-lock-doc-face))) + "Face used for @comment.doc tags in Elixir files.") + +(defface elixir-ts-font-comment-doc-attribute-face + '((t (:inherit font-lock-doc-face))) + "Face used for @comment.doc.__attribute__ tags in Elixir files.") + +(defface elixir-ts-font-sigil-name-face + '((t (:inherit font-lock-string-face))) + "Face used for @__name__ tags in Elixir files.") + +(defconst elixir-ts--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--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 elixir-ts--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 (treesit-node-prev-sibling node t) 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 "^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 "^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) + ((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$") + ,'elixir-ts--argument-indent-anchor + ,'elixir-ts--argument-indent-offset) + ;; 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. + ((parent-is "ERROR") prev-line 0) + ((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))))) + +(defvar elixir-ts--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 + `(,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))) + + :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--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--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--definition-keywords-re @font-lock-keyword-face)) + (call + target: (identifier) @font-lock-keyword-face + (:match ,elixir-ts--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--definition-keywords-re @font-lock-keyword-face)) + (call + target: (identifier) @font-lock-keyword-face + (arguments + (binary_operator + operator: "|>" + right: (identifier))) + (:match ,elixir-ts--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--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.") + +(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))))) + +(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* ((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--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))) + +;;;###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") + + (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. + (if (treesit-ready-p 'heex) + (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-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--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-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..748455cc3f2 --- /dev/null +++ b/test/lisp/progmodes/elixir-ts-mode-resources/indent.erts @@ -0,0 +1,308 @@ +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: Map in function def + +=-= +def foobar() do + %{ + one: "one", + two: "two", + three: "three", + four: "four" + } +end +=-=-= + +Name: Map in tuple + +=-= +def foo() do + {:ok, + %{ + state + | extra_arguments: extra_arguments, + max_children: max_children, + max_restarts: max_restarts, + max_seconds: max_seconds, + strategy: strategy + }} +end +=-=-= + +Name: Nested maps + +=-= +%{ + foo: "bar", + bar: %{ + foo: "bar" + } +} + +def foo() do + %{ + foo: "bar", + bar: %{ + foo: "bar" + } + } +end +=-=-= + +Name: Block assignments + +=-= +foo = + if true do + "yes" + else + "no" + end +=-=-= + +Name: Function rescue + +=-= +def foo do + "bar" +rescue + e -> + "bar" +end +=-=-= + +Name: With statement +=-= +with one <- one(), + two <- two(), + {:ok, value} <- get_value(one, two) do + {:ok, value} +else + {:error, %{"Message" => message}} -> + {:error, message} +end +=-=-= + +Name: Pipe statements with fn + +=-= +[1, 2] +|> Enum.map(fn num -> + num + 1 +end) +=-=-= + +Name: Pipe statements stab clases + +=-= +[1, 2] +|> Enum.map(fn + x when x < 10 -> x * 2 + x -> x * 3 +end) +=-=-= + +Name: Pipe statements params + +=-= +[1, 2] +|> foobar( + :one, + :two, + :three, + :four +) +=-=-= + +Name: Parameter maps + +=-= +def something(%{ + one: :one, + two: :two + }) do + {:ok, "done"} +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: Spec and method + +=-= +@spec foobar( + t, + acc, + (one, something -> :bar | far), + (two -> :bar | far) + ) :: any() + when chunk: any +def foobar(enumerable, acc, chunk_fun, after_fun) do + {_, {res, acc}} = + case after_fun.(acc) do + {:one, "one"} -> + "one" + + {:two, "two"} -> + "two" + end +end +=-=-= + +Name: Spec with multi-line result + +=-= +@type result :: + {:done, term} + | {:two} + | {:one} + +@type result :: + { + :done, + term + } + | {:two} + | {:one} + +@type boo_bar :: + (foo :: pos_integer, bar :: pos_integer -> any()) + +@spec foo_bar( + t, + (foo -> any), + (() -> any) | (foo, foo -> boolean) | module() + ) :: any + when foo: any +def foo(one, fun, other) +=-=-= + +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" +=-=-= + +Name: Tuple with same line first node + +=-= +{:one, + :two} + +{:ok, + fn one -> + one + |> String.upcase(one) + end} +=-=-= + +Name: Long tuple + +=-= +{"January", "February", "March", "April", "May", "June", "July", "August", "September", + "October", "November", "December"} +=-=-= 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 --=-=-=-- From debbugs-submit-bounces@debbugs.gnu.org Sun Mar 12 11:47:04 2023 Received: (at 61996-done) by debbugs.gnu.org; 12 Mar 2023 15:47:04 +0000 Received: from localhost ([127.0.0.1]:32983 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1pbNuZ-0001hK-Kq for submit@debbugs.gnu.org; Sun, 12 Mar 2023 11:47:03 -0400 Received: from eggs.gnu.org ([209.51.188.92]:44596) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1pbNuW-0001gn-Qw for 61996-done@debbugs.gnu.org; Sun, 12 Mar 2023 11:47:02 -0400 Received: from fencepost.gnu.org ([2001:470:142:3::e]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1pbNuQ-00020J-R6; Sun, 12 Mar 2023 11:46:54 -0400 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=gnu.org; s=fencepost-gnu-org; h=MIME-version:References:Subject:In-Reply-To:To:From: Date; bh=D3rsKGBKn3pfWxxNveFggBuFOVpgGfhcNWc23fC8mlw=; b=ah+Rw9xEfaP/ttTLz9OM XtUyngx3rxUqzP6PW6k71TjgWwxyVzo2208gYdrTY7+LUXauhqA1gw+PMhaG8ddbvla7b9gYALs2e /rpOKHf2IUfOTBM9O4wtZ090VpFmzEnDmDk4cRIkFOs/nRK0dQWF1r85HHEbFGyyVQgqQAkt+eNdO QPAx1P6/BOfV/88WXiSVSeG9mHogGK9nlriubrKMJ6pXnfQK8nYLBCZE1IvqZxRPnwHKor2/Nw5m/ AL8KjplNcITEzOWy4Zk21yBfssBnGX6e0DkRIvYWNBqL2C9b3B3cpPYsrjqk4HWTaeCUj58JmLBH3 5S/FmVAZ4qImjg==; Received: from [87.69.77.57] (helo=home-c4e4a596f7) by fencepost.gnu.org with esmtpsa (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1pbNuP-0002es-Vo; Sun, 12 Mar 2023 11:46:54 -0400 Date: Sun, 12 Mar 2023 17:46:41 +0200 Message-Id: <83sfeat1jy.fsf@gnu.org> From: Eli Zaretskii To: Wilhelm Kirschbaum In-Reply-To: <87a60i2dsw.fsf@gmail.com> (message from Wilhelm Kirschbaum on Sun, 12 Mar 2023 17:14:03 +0200) Subject: Re: bug#61996: 30.0.50; Submitting elixir-ts-mode and heex-ts-mode References: <87mt4qibnk.fsf@gmail.com> <83cz5m8515.fsf@gnu.org> <874jqx3h47.fsf@gmail.com> <83lek97mm1.fsf@gnu.org> <877cvtfz37.fsf@gmail.com> <83356by7fr.fsf@gnu.org> <87h6urkuyx.fsf@gmail.com> <83fsaauywx.fsf@gnu.org> <87sfeas30x.fsf@gmail.com> <837cvmurny.fsf@gnu.org> <87a60i2dsw.fsf@gmail.com> MIME-version: 1.0 Content-type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit X-Spam-Score: -2.3 (--) X-Debbugs-Envelope-To: 61996-done Cc: 61996-done@debbugs.gnu.org, theo@thornhill.no, casouri@gmail.com X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: debbugs-submit-bounces@debbugs.gnu.org Sender: "Debbugs-submit" X-Spam-Score: -3.3 (---) > From: Wilhelm Kirschbaum > Cc: 61996@debbugs.gnu.org, casouri@gmail.com, theo@thornhill.no > Date: Sun, 12 Mar 2023 17:14:03 +0200 > > Attached are the updated patches with added test cases and > indentation rule enhancements. Thanks, installed on master. > I still see this warning on a non-treesitter build: > > In elixir-ts--forward-sexp: > elixir-ts-mode.el:490:8: Warning: reference to free variable > ‘heex-ts--sexp-regexp’ Doesn't happen here, so I think we are good. > Random concern: how will backwards compatibility work when the > grammars get updated? Let's discuss this when such problems actually happen. The answer depends on what kind of incompatibilities are introduced by changes in the grammars. > It might just be better to add both modes in one patch if more > changes are required perhaps? I'm not sure I understand the question. In general, we like each commit to be as self-contained and independent of the others as possible. Not sure if this answers your question. Thanks, I'm closing this bug. From debbugs-submit-bounces@debbugs.gnu.org Sun Mar 12 14:04:44 2023 Received: (at 61996-done) by debbugs.gnu.org; 12 Mar 2023 18:04:44 +0000 Received: from localhost ([127.0.0.1]:33074 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1pbQ3n-0005Mi-Rm for submit@debbugs.gnu.org; Sun, 12 Mar 2023 14:04:44 -0400 Received: from mail-wr1-f49.google.com ([209.85.221.49]:42870) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1pbQ3m-0005MU-4i for 61996-done@debbugs.gnu.org; Sun, 12 Mar 2023 14:04:42 -0400 Received: by mail-wr1-f49.google.com with SMTP id j2so9305442wrh.9 for <61996-done@debbugs.gnu.org>; Sun, 12 Mar 2023 11:04:42 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20210112; t=1678644276; 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=qwLJuCv7GQkEj9jrLXNKZJuMstgwPI+QXmeZNamhPdA=; b=gZfy5eStN1xbNwufcSWxDAhz95aiHKdlwzmtqp9gGywDQPrOF7jiKougrG90azLr1R Z2rT92Xv/ZnSd7o5yzvDOaauF6eci/cOu64Ovzg0BphYhYDcOVLJzGeiTTMrtLMTjUdA E56OPOyTt5HsnPLIOMxRMD1x1RGgMSqnKRMQbs9o23ayuwnPWEEoghsBN8psdjqqwBg+ 1IGQwX2PbrmSA0Dd4Nl8235UHEakr4ecO42+YgYrvOWnnZ+Uy9SGQPwjObArdjBFheQ+ 5pXlse2cysH6N+MOA1epdOvHTaLpt3hPNlwwOsi96igiAk7yD7AqilhJaJmFjmA/NkzB k5qA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20210112; t=1678644276; 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=qwLJuCv7GQkEj9jrLXNKZJuMstgwPI+QXmeZNamhPdA=; b=wXMGNYuPa1bYH/EDAypwc7oaEj/Bu9bQhdndO+ECb7aGAS2Xr8SwSnXOtV8EOpvTnf PZWHzZALH/5tr1jXohfGfxorYegv5aZBxgHOZ9rYv+nfg0qxQPdlr5c60KA+QT+q5liJ vjplQ6oThux2VFC2BaWg8WnuW/4SUqdbAOYLk6tj+aBvxKV9OXCpyw5zoJ71YwKp8fQ+ SslUCmiA0oK3JxHDk53zM++4BWsALYO3gMBi/BuSg/CaPXzZ8h2wV3QlnjjXSnArIUfb i3G354I3IkSE1yrmDleIu+aW8R+tV02UbN7xdJrc7CZJJkP973h1emtFzFz7+jIJwAfy 9E6A== X-Gm-Message-State: AO0yUKVTBBVisjpbwBtzoobfifQKPdL7JbYVaJFHHzU/JKHkIHf2xYAJ z/ViSFkMCiI0hnwL2nP5IeEmg6Zk4K248PUu X-Google-Smtp-Source: AK7set/AldYZgzquzShwaFycQs6E1JcL3NGFqHYVZqELOOGGDj0V2F+2eiY4b21YcBt8Fgk70n2XUA== X-Received: by 2002:a5d:56c9:0:b0:2c7:6ce2:bb37 with SMTP id m9-20020a5d56c9000000b002c76ce2bb37mr21062006wrw.29.1678644276355; Sun, 12 Mar 2023 11:04:36 -0700 (PDT) Received: from melissa.local ([2c0f:ef18:1431:0:b09:9616:db04:c248]) by smtp.gmail.com with ESMTPSA id p3-20020a5d48c3000000b002c54e26bca5sm5518079wrs.49.2023.03.12.11.04.34 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Sun, 12 Mar 2023 11:04:36 -0700 (PDT) References: <87mt4qibnk.fsf@gmail.com> <83cz5m8515.fsf@gnu.org> <874jqx3h47.fsf@gmail.com> <83lek97mm1.fsf@gnu.org> <877cvtfz37.fsf@gmail.com> <83356by7fr.fsf@gnu.org> <87h6urkuyx.fsf@gmail.com> <83fsaauywx.fsf@gnu.org> <87sfeas30x.fsf@gmail.com> <837cvmurny.fsf@gnu.org> <87a60i2dsw.fsf@gmail.com> <83sfeat1jy.fsf@gnu.org> User-agent: mu4e 1.9.3; emacs 30.0.50 From: Wilhelm Kirschbaum To: Eli Zaretskii Subject: Re: bug#61996: 30.0.50; Submitting elixir-ts-mode and heex-ts-mode Date: Sun, 12 Mar 2023 20:02:58 +0200 In-reply-to: <83sfeat1jy.fsf@gnu.org> Message-ID: <87wn3lvob3.fsf@gmail.com> MIME-Version: 1.0 Content-Type: text/plain; format=flowed X-Spam-Score: 0.0 (/) X-Debbugs-Envelope-To: 61996-done Cc: 61996-done@debbugs.gnu.org, theo@thornhill.no, casouri@gmail.com X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: debbugs-submit-bounces@debbugs.gnu.org Sender: "Debbugs-submit" X-Spam-Score: -1.0 (-) Eli Zaretskii writes: >> It might just be better to add both modes in one patch if more >> changes are required perhaps? > > I'm not sure I understand the question. > > In general, we like each commit to be as self-contained and > independent of the others as possible. Not sure if this answers > your > question. > > Thanks, I'm closing this bug. Thanks for the help on this. From unknown Sun Jun 22 22:41:43 2025 Received: (at fakecontrol) by fakecontrolmessage; To: internal_control@debbugs.gnu.org From: Debbugs Internal Request Subject: Internal Control Message-Id: bug archived. Date: Mon, 10 Apr 2023 11:24:07 +0000 User-Agent: Fakemail v42.6.9 # This is a fake control message. # # The action: # bug archived. thanks # This fakemail brought to you by your local debbugs # administrator