From 802e64922bcee40c8362b9627aa33a0de0c068d7 Mon Sep 17 00:00:00 2001 From: Wilhelm H Kirschbaum Date: Sun, 12 Mar 2023 17:08:50 +0200 Subject: [PATCH] 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 @@ case "${lang}" in 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 e43aac614c9..682928afa8e 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -248,6 +248,9 @@ following to you init file: 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