From mboxrd@z Thu Jan 1 00:00:00 1970 Path: news.gmane.io!.POSTED.blaine.gmane.org!not-for-mail From: Wilhelm Kirschbaum Newsgroups: gmane.emacs.bugs Subject: bug#61996: 30.0.50; Submitting elixir-ts-mode and heex-ts-mode Date: Mon, 06 Mar 2023 09:04:13 +0200 Message-ID: <87mt4qibnk.fsf@gmail.com> Mime-Version: 1.0 Content-Type: multipart/mixed; boundary="=-=-=" Injection-Info: ciao.gmane.io; posting-host="blaine.gmane.org:116.202.254.214"; logging-data="9421"; mail-complaints-to="usenet@ciao.gmane.io" User-Agent: mu4e 1.9.3; emacs 30.0.50 Cc: casouri@gmail.com, theo@thornhill.no To: 61996@debbugs.gnu.org Original-X-From: bug-gnu-emacs-bounces+geb-bug-gnu-emacs=m.gmane-mx.org@gnu.org Mon Mar 06 08:27:28 2023 Return-path: Envelope-to: geb-bug-gnu-emacs@m.gmane-mx.org Original-Received: from lists.gnu.org ([209.51.188.17]) by ciao.gmane.io with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.92) (envelope-from ) id 1pZ5Fn-0002C4-IM for geb-bug-gnu-emacs@m.gmane-mx.org; Mon, 06 Mar 2023 08:27:27 +0100 Original-Received: from localhost ([::1] helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1pZ5FR-0006gA-01; Mon, 06 Mar 2023 02:27:05 -0500 Original-Received: from eggs.gnu.org ([2001:470:142:3::10]) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1pZ5FP-0006g0-Et for bug-gnu-emacs@gnu.org; Mon, 06 Mar 2023 02:27:03 -0500 Original-Received: from debbugs.gnu.org ([209.51.188.43]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_128_GCM_SHA256:128) (Exim 4.90_1) (envelope-from ) id 1pZ5FO-00008D-S8 for bug-gnu-emacs@gnu.org; Mon, 06 Mar 2023 02:27:02 -0500 Original-Received: from Debian-debbugs by debbugs.gnu.org with local (Exim 4.84_2) (envelope-from ) id 1pZ5FO-0002P2-H0 for bug-gnu-emacs@gnu.org; Mon, 06 Mar 2023 02:27:02 -0500 X-Loop: help-debbugs@gnu.org Resent-From: Wilhelm Kirschbaum Original-Sender: "Debbugs-submit" Resent-CC: bug-gnu-emacs@gnu.org Resent-Date: Mon, 06 Mar 2023 07:27:02 +0000 Resent-Message-ID: Resent-Sender: help-debbugs@gnu.org X-GNU-PR-Message: report 61996 X-GNU-PR-Package: emacs X-Debbugs-Original-To: bug-gnu-emacs@gnu.org Original-Received: via spool by submit@debbugs.gnu.org id=B.16780875679167 (code B ref -1); Mon, 06 Mar 2023 07:27:02 +0000 Original-Received: (at submit) by debbugs.gnu.org; 6 Mar 2023 07:26:07 +0000 Original-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 Original-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 Original-Received: from eggs.gnu.org ([2001:470:142:3::10]) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1pZ5EP-0006cB-HN for bug-gnu-emacs@gnu.org; Mon, 06 Mar 2023 02:26:01 -0500 Original-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 Original-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) Original-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) 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-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list X-BeenThere: bug-gnu-emacs@gnu.org List-Id: "Bug reports for GNU Emacs, the Swiss army knife of text editors" List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: bug-gnu-emacs-bounces+geb-bug-gnu-emacs=m.gmane-mx.org@gnu.org Original-Sender: bug-gnu-emacs-bounces+geb-bug-gnu-emacs=m.gmane-mx.org@gnu.org Xref: news.gmane.io gmane.emacs.bugs:257373 Archived-At: --=-=-= 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 --=-=-=--