From 00115e886f2a132fe406164245c0a509eed8e437 Mon Sep 17 00:00:00 2001 From: Vincenzo Pupillo Date: Fri, 14 Feb 2025 18:38:51 +0100 Subject: [PATCH] Add mhtml-ts-mode. New major-mode alternative to mhtml-mode, based on treesitter, for editing files containing html, javascript and css. * etc/NEWS: Mention the new mode and new functions. * lisp/textmodes/mhtml-ts-mode.el: New file. * lisp/progmodes/js.el (js--treesit-thing-settings): New variable. (js--treesit-font-lock-feature-list); New variable. (js--treesit-simple-imenu-settings): New variable. (js--treesit-defun-type-regexp): New variable. (js--treesit-jsdoc-comment-regexp): New variable. (js-ts-mode): Use of new variables instead of direct assignment of values. * lisp/textmodes/css-mode.el (css-mode--menu): New variable. (css-mode-map): Use new variable. (css--treesit-font-lock-feature-list): New variable. (css--treesit-simple-imenu-settings): New variable. (css--treesit-defun-type-regexp): New variable. (cs-ts-mode): Use of new variables instead of direct assignment of values. * lisp/textmodes/html-ts-mode.el (html-ts-mode--treesit-things-settings): New variable. (html-ts-mode--treesit-font-lock-feature-list): New variable. (html-ts-mode--treesit-simple-imenu-settings): New variable. (html-ts-mode--treesit-defun-type-regexp): New variable. (html-ts-mode): Use of new variables instead of direct assignment of values. * lisp/treesit.el (treesit-merge-font-lock-feature-list): New fuction. (treesit-replace-font-lock-feature-settings): New fuction. (treesit-modify-indent-rules): New function. (cherry picked from commit 05a96fd39809f11a3820e2164b23ebf9df192b13) --- lisp/progmodes/js.el | 72 ++-- lisp/textmodes/css-mode.el | 47 ++- lisp/textmodes/html-ts-mode.el | 54 +-- lisp/textmodes/mhtml-ts-mode.el | 594 ++++++++++++++++++++++++++++++++ lisp/treesit.el | 68 ++++ 5 files changed, 772 insertions(+), 63 deletions(-) create mode 100644 lisp/textmodes/mhtml-ts-mode.el diff --git a/lisp/progmodes/js.el b/lisp/progmodes/js.el index 81cf02664c6..6184298e24a 100644 --- a/lisp/progmodes/js.el +++ b/lisp/progmodes/js.el @@ -3917,6 +3917,44 @@ See `treesit-thing-settings' for more information.") (defvar js--treesit-jsdoc-beginning-regexp (rx bos "/**") "Regular expression matching the beginning of a jsdoc block comment.") +(defvar js--treesit-thing-settings + `((javascript + (sexp ,(js--regexp-opt-symbol js--treesit-sexp-nodes)) + (list ,(js--regexp-opt-symbol js--treesit-list-nodes)) + (sentence ,(js--regexp-opt-symbol js--treesit-sentence-nodes)) + (text ,(js--regexp-opt-symbol '("comment" + "string_fragment"))))) + "Settings for `treesit-thing-settings'.") + +(defvar js--treesit-font-lock-feature-list + '(( comment document definition) + ( keyword string) + ( assignment constant escape-sequence jsx number + pattern string-interpolation) + ( bracket delimiter function operator property)) + "Settings for `treesit-font-lock-feature-list'.") + +(defvar js--treesit-simple-imenu-settings + `(("Function" "\\`function_declaration\\'" nil nil) + ("Variable" "\\`lexical_declaration\\'" + js--treesit-valid-imenu-entry nil) + ("Class" ,(rx bos (or "class_declaration" + "method_definition") + eos) + nil nil)) + "Settings for `treesit-simple-imenu'.") + +(defvar js--treesit-defun-type-regexp + (rx (or "class_declaration" + "method_definition" + "function_declaration" + "lexical_declaration")) + "Settings for `treesit-defun-type-regexp'.") + +(defvar js--treesit-jsdoc-comment-regexp + (rx (or "comment" "line_comment" "block_comment" "description")) + "Regexp for `c-ts-common--comment-regexp'.") + ;;;###autoload (define-derived-mode js-ts-mode js-base-mode "JavaScript" "Major mode for editing JavaScript. @@ -3946,29 +3984,15 @@ See `treesit-thing-settings' for more information.") ;; Indent. (setq-local treesit-simple-indent-rules js--treesit-indent-rules) ;; Navigation. - (setq-local treesit-defun-type-regexp - (rx (or "class_declaration" - "method_definition" - "function_declaration" - "lexical_declaration"))) + (setq-local treesit-defun-type-regexp js--treesit-defun-type-regexp) + (setq-local treesit-defun-name-function #'js--treesit-defun-name) - (setq-local treesit-thing-settings - `((javascript - (sexp ,(js--regexp-opt-symbol js--treesit-sexp-nodes)) - (list ,(js--regexp-opt-symbol js--treesit-list-nodes)) - (sentence ,(js--regexp-opt-symbol js--treesit-sentence-nodes)) - (text ,(js--regexp-opt-symbol '("comment" - "string_fragment")))))) + (setq-local treesit-thing-settings js--treesit-thing-settings) ;; Fontification. (setq-local treesit-font-lock-settings js--treesit-font-lock-settings) - (setq-local treesit-font-lock-feature-list - '(( comment document definition) - ( keyword string) - ( assignment constant escape-sequence jsx number - pattern string-interpolation) - ( bracket delimiter function operator property))) + (setq-local treesit-font-lock-feature-list js--treesit-font-lock-feature-list) (when (treesit-ready-p 'jsdoc t) (setq-local treesit-range-settings @@ -3978,17 +4002,11 @@ See `treesit-thing-settings' for more information.") :local t `(((comment) @capture (:match ,js--treesit-jsdoc-beginning-regexp @capture))))) - (setq c-ts-common--comment-regexp (rx (or "comment" "line_comment" "block_comment" "description")))) + (setq c-ts-common--comment-regexp js--treesit-jsdoc-comment-regexp)) ;; Imenu - (setq-local treesit-simple-imenu-settings - `(("Function" "\\`function_declaration\\'" nil nil) - ("Variable" "\\`lexical_declaration\\'" - js--treesit-valid-imenu-entry nil) - ("Class" ,(rx bos (or "class_declaration" - "method_definition") - eos) - nil nil))) + (setq-local treesit-simple-imenu-settings js--treesit-simple-imenu-settings) + (treesit-major-mode-setup) (add-to-list 'auto-mode-alist diff --git a/lisp/textmodes/css-mode.el b/lisp/textmodes/css-mode.el index efcfcecf9d9..85fe59afef8 100644 --- a/lisp/textmodes/css-mode.el +++ b/lisp/textmodes/css-mode.el @@ -893,13 +893,7 @@ cannot be completed sensibly: `custom-ident', (modify-syntax-entry ?? "." st) st)) -(defvar-keymap css-mode-map - :doc "Keymap used in `css-mode'." - " " #'css-lookup-symbol - ;; `info-complete-symbol' is not used. - " " #'completion-at-point - "C-c C-f" #'css-cycle-color-format - :menu +(defvar css-mode--menu '("CSS" :help "CSS-specific features" ["Reformat block" fill-paragraph @@ -910,7 +904,17 @@ cannot be completed sensibly: `custom-ident', ["Describe symbol" css-lookup-symbol :help "Display documentation for a CSS symbol"] ["Complete symbol" completion-at-point - :help "Complete symbol before point"])) + :help "Complete symbol before point"]) + "Menu bar for `css-mode'") + +(defvar-keymap css-mode-map + :doc "Keymap used in `css-mode'." + " " #'css-lookup-symbol + ;; `info-complete-symbol' is not used. + " " #'completion-at-point + "C-c C-f" #'css-cycle-color-format + :menu + css-mode--menu) (eval-and-compile (defconst css--uri-re @@ -1771,6 +1775,21 @@ rgb()/rgba()." (replace-regexp-in-string "[\n ]+" " " s))) res))))))) +(defvar css--treesit-font-lock-feature-list + '((selector comment query keyword) + (property constant string) + (error variable function operator bracket)) + "Settings for `treesit-font-lock-feature-list'.") + +(defvar css--treesit-simple-imenu-settings + `(( nil ,(rx bos (or "rule_set" "media_statement") eos) + nil nil)) + "Settings for `treesit-simple-imenu'.") + +(defvar css--treesit-defun-type-regexp + "rule_set" + "Settings for `treesit-defun-type-regexp'.") + (define-derived-mode css-base-mode prog-mode "CSS" "Generic mode to edit Cascading Style Sheets (CSS). @@ -1825,16 +1844,12 @@ can also be used to fill comments. ;; Tree-sitter specific setup. (setq treesit-primary-parser (treesit-parser-create 'css)) (setq-local treesit-simple-indent-rules css--treesit-indent-rules) - (setq-local treesit-defun-type-regexp "rule_set") + (setq-local treesit-defun-type-regexp css--treesit-defun-type-regexp) (setq-local treesit-defun-name-function #'css--treesit-defun-name) (setq-local treesit-font-lock-settings css--treesit-settings) - (setq-local treesit-font-lock-feature-list - '((selector comment query keyword) - (property constant string) - (error variable function operator bracket))) - (setq-local treesit-simple-imenu-settings - `(( nil ,(rx bos (or "rule_set" "media_statement") eos) - nil nil))) + (setq-local treesit-font-lock-feature-list css--treesit-font-lock-feature-list) + (setq-local treesit-simple-imenu-settings css--treesit-simple-imenu-settings) + (treesit-major-mode-setup) (add-to-list 'auto-mode-alist '("\\.css\\'" . css-ts-mode)))) diff --git a/lisp/textmodes/html-ts-mode.el b/lisp/textmodes/html-ts-mode.el index 0f07fbedeed..26efe1be726 100644 --- a/lisp/textmodes/html-ts-mode.el +++ b/lisp/textmodes/html-ts-mode.el @@ -88,6 +88,35 @@ `((attribute_name) @font-lock-variable-name-face)) "Tree-sitter font-lock settings for `html-ts-mode'.") +(defvar html-ts-mode--treesit-things-settings + `((html + (sexp ,(regexp-opt '("element" + "text" + "attribute" + "value"))) + (list ,(rx (or + ;; Also match script_element and style_element + "element" + ;; HTML comments have the element syntax + "comment"))) + (sentence ,(rx (and bos (or "tag_name" "attribute") eos))) + (text ,(regexp-opt '("comment" "text"))))) + "Settings for `treesit-thing-settings'.") + +(defvar html-ts-mode--treesit-font-lock-feature-list + '((comment keyword definition) + (property string) + () ()) + "Settings for `treesit-font-lock-feature-list'.") + +(defvar html-ts-mode--treesit-simple-imenu-settings + '((nil "element" nil nil)) + "Settings for `treesit-simple-imenu'.") + +(defvar html-ts-mode--treesit-defun-type-regexp + "element" + "Settings for `treesit-defun-type-regexp'.") + (defun html-ts-mode--defun-name (node) "Return the defun name of NODE. Return nil if there is no name or if NODE is not a defun node." @@ -120,33 +149,18 @@ Return nil if there is no name or if NODE is not a defun node." (setq-local treesit-simple-indent-rules html-ts-mode--indent-rules) ;; Navigation. - (setq-local treesit-defun-type-regexp "element") + (setq-local treesit-defun-type-regexp html-ts-mode--treesit-defun-type-regexp) + (setq-local treesit-defun-name-function #'html-ts-mode--defun-name) - (setq-local treesit-thing-settings - `((html - (sexp ,(regexp-opt '("element" - "text" - "attribute" - "value"))) - (list ,(rx (or - ;; Also match script_element and style_element - "element" - ;; HTML comments have the element syntax - "comment"))) - (sentence ,(rx (and bos (or "tag_name" "attribute") eos))) - (text ,(regexp-opt '("comment" "text")))))) + (setq-local treesit-thing-settings html-ts-mode--treesit-things-settings) ;; Font-lock. (setq-local treesit-font-lock-settings html-ts-mode--font-lock-settings) - (setq-local treesit-font-lock-feature-list - '((comment keyword definition) - (property string) - () ())) + (setq-local treesit-font-lock-feature-list html-ts-mode--treesit-font-lock-feature-list) ;; Imenu. - (setq-local treesit-simple-imenu-settings - '((nil "element" nil nil))) + (setq-local treesit-simple-imenu-settings html-ts-mode--treesit-simple-imenu-settings) ;; Outline minor mode. (setq-local treesit-outline-predicate #'html-ts-mode--outline-predicate) diff --git a/lisp/textmodes/mhtml-ts-mode.el b/lisp/textmodes/mhtml-ts-mode.el new file mode 100644 index 00000000000..9be1a14c257 --- /dev/null +++ b/lisp/textmodes/mhtml-ts-mode.el @@ -0,0 +1,594 @@ +;;; mhtml-ts-mode.el --- Major mode for HTML using tree-sitter -*- lexical-binding: t; -*- + +;; Copyright (C) 2024 Free Software Foundation, Inc. + +;; Author: Vincenzo Pupillo +;; Maintainer: Vincenzo Pupillo +;; Created: Nov 2024 +;; Keywords: HTML languages hypermedia 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 `mhtml-ts-mode' which is a major mode +;; for editing HTML files with embedded JavaScript and CSS. +;; Tree Sitter is used to parse each of these languages. +;; +;; Please note that this package requires `html-ts-mode', which +;; registers itself as the major mode for editing HTML. +;; +;; This package is compatible and has been tested with the following +;; tree-sitter grammars: +;; * https://github.com/tree-sitter/tree-sitter-html +;; * https://github.com/tree-sitter/tree-sitter-javascript +;; * https://github.com/tree-sitter/tree-sitter-jsdoc +;; * https://github.com/tree-sitter/tree-sitter-css +;; +;; Features +;; +;; * Indent +;; * Flymake +;; * IMenu +;; * Navigation +;; * Which-function +;; * Tree-sitter parser installation helper + +;;; Code: + +(require 'treesit) +(require 'html-ts-mode) +(require 'css-mode) ;; for embed css into html +(require 'js) ;; for embed javascript into html + +(eval-when-compile + (require 'rx)) + +;; This tells the byte-compiler where the functions are defined. +;; Is only needed when a file needs to be able to byte-compile +;; in a Emacs not built with tree-sitter library. +(treesit-declare-unavailable-functions) + +;; In a multi-language major mode can be useful to have an "installer" to +;; simplify the installation of the grammars supported by the major-mode. +(defvar mhtml-ts-mode--language-source-alist + '((html . ("https://github.com/tree-sitter/tree-sitter-html" "v0.23.2")) + (javascript . ("https://github.com/tree-sitter/tree-sitter-javascript" "v0.23.1")) + (jsdoc . ("https://github.com/tree-sitter/tree-sitter-jsdoc" "v0.23.2")) + (css . ("https://github.com/tree-sitter/tree-sitter-css" "v0.23.1"))) + "Treesitter language parsers required by `mhtml-ts-mode'. +You can customize this variable if you want to stick to a specific +commit and/or use different parsers.") + +(defun mhtml-ts-mode-install-parsers () + "Install all the required treesitter parsers. +`mhtml-ts-mode--language-source-alist' defines which parsers to install." + (interactive) + (let ((treesit-language-source-alist mhtml-ts-mode--language-source-alist)) + (dolist (item mhtml-ts-mode--language-source-alist) + (treesit-install-language-grammar (car item))))) + +;;; Custom variables + +(defgroup mhtml-ts-mode nil + "Major mode for editing HTML files, based on `html-ts-mode'. +Works with JS and CSS and for that use `js-ts-mode' and `css-ts-mode'." + :prefix "mhtml-ts-mode-" + ;; :group 'languages + :group 'html) + +(defcustom mhtml-ts-mode-js-css-indent-offset 2 + "JavaScript and CSS indent spaces related to the + +When nil, indentation of the tag body starts just below the +tag, like: + + + +When `ignore', the tag body starts in the first column, like: + + " + :type '(choice (const nil) (const t) (const ignore)) + :safe 'symbolp + :set #'mhtml-ts-mode--tag-relative-indent-offset + :version "31.1") + +(defcustom mhtml-ts-mode-css-fontify-colors t + "Whether CSS colors should be fontified using the color as the background. +If non-nil, text representing a CSS color will be fontified +such that its background is the color itself. +Works like `css--fontify-region'." + :tag "HTML colors the CSS properties values." + :version "31.1" + :type 'boolean + :safe 'booleanp) + +(defvar mhtml-ts-mode-saved-pretty-print-command nil + "The command last used to pretty print in this buffer.") + +(defun mhtml-ts-mode-pretty-print (command) + "Prettify the current buffer. +Argument COMMAND The command to use." + (interactive + (list (read-string + "Prettify command: " + (or mhtml-ts-mode-saved-pretty-print-command + (concat mhtml-ts-mode-pretty-print-command " "))))) + (setq mhtml-ts-mode-saved-pretty-print-command command) + (save-excursion + (shell-command-on-region + (point-min) (point-max) + command (buffer-name) t + "*mhtml-ts-mode-pretty-pretty-print-errors*" t))) + +(defun mhtml-ts-mode--switch-fill-defun (&rest arguments) + "Switch between `fill-paragraph' and `prog-fill-reindent-defun'. +In an HTML region it calls `fill-paragraph' as does `html-ts-mode', +otherwise it calls `prog-fill-reindent-defun'. +Optional ARGUMENTS to to be passed to it." + (interactive) + (if (eq (treesit-language-at (point)) 'html) + (funcall-interactively #'fill-paragraph arguments) + (funcall-interactively #'prog-fill-reindent-defun arguments))) + +(defvar-keymap mhtml-ts-mode-map + :doc "Keymap for `mhtml-ts-mode' buffers." + :parent html-mode-map + ;; `mhtml-ts-mode' derive from `html-ts-mode' so the keymap is the + ;; same, we need to add some mapping from others languages. + "C-c C-f" #'css-cycle-color-format + "M-q" #'mhtml-ts-mode--switch-fill-defun) + +;; Place the CSS menu in the menu bar as well. +(easy-menu-define mhtml-ts-mode-menu mhtml-ts-mode-map + "Menu bar for `mhtml-ts-mode'." + css-mode--menu) + +;; To enable some basic treesiter functionality, you should define +;; a function that recognizes which grammar is used at-point. +;; This function should be assigned to `treesit-language-at-point-function' +(defun mhtml-ts-mode--language-at-point (point) + "Return the language at POINT assuming the point is within a HTML buffer." + (let* ((node (treesit-node-at point 'html)) + (parent (treesit-node-parent node)) + (node-query (format "(%s (%s))" + (treesit-node-type parent) + (treesit-node-type node)))) + (cond + ((equal "(script_element (raw_text))" node-query) (js--treesit-language-at-point point)) + ((equal "(style_element (raw_text))" node-query) 'css) + (t 'html)))) + +;; Custom font-lock function that's used to apply color to css color +;; The signature of the function should be conforming to signature +;; QUERY-SPEC required by `treesit-font-lock-rules'. +(defun mhtml-ts-mode--colorize-css-value (node override start end &rest _) + "Colorize CSS property value like `css--fontify-region'. +For NODE, OVERRIDE, START, and END, see `treesit-font-lock-rules'." + (if (and mhtml-ts-mode-css-fontify-colors + (string-equal "plain_value" (treesit-node-type node))) + (let ((color (css--compute-color start (treesit-node-text node t)))) + (when color + (with-silent-modifications + (add-text-properties + (treesit-node-start node) (treesit-node-end node) + (list 'face (list :background color + :foreground (readable-foreground-color + color) + :box '(:line-width -1))))))) + (treesit-fontify-with-override + (treesit-node-start node) (treesit-node-end node) + 'font-lock-variable-name-face + override start end))) + +;; Embedded languages ​​should be indented according to the language +;; that embeds them. +;; This function signature complies with `treesit-simple-indent-rules' +;; ANCHOR. +(defun mhtml-ts-mode--js-css-tag-bol (_node _parent &rest _) + "Find the first non-space characters of html tags