Package: emacs;
Reported by: Vincenzo Pupillo <v.pupillo <at> gmail.com>
Date: Fri, 29 Nov 2024 21:58:01 UTC
Severity: wishlist
Fixed in version 31.0.50
Done: Juri Linkov <juri <at> linkov.net>
Bug is archived. No further changes may be made.
Message #70 received at 74610 <at> debbugs.gnu.org (full text, mbox):
From: Eli Zaretskii <eliz <at> gnu.org> To: casouri <at> gmail.com, juri <at> linkov.net, Vincenzo Pupillo <v.pupillo <at> gmail.com> Cc: 74610 <at> debbugs.gnu.org Subject: Re: bug#74610: 31.0.50; Submitting mhtml-ts-mode, treesitter alternative to mhtml-mode Date: Sat, 08 Feb 2025 11:01:48 +0200
Ping! Can we please make progress with this issue? is the patch ready to be installed? > From: Vincenzo Pupillo <v.pupillo <at> gmail.com> > Cc: Juri Linkov <juri <at> linkov.net>, 74610 <at> debbugs.gnu.org, > Eli Zaretskii <eliz <at> gnu.org> > Date: Sun, 19 Jan 2025 18:09:31 +0100 > > Ciao Yuan, > In data sabato 18 gennaio 2025 02:37:53 Ora standard dell’Europa centrale, > Yuan Fu ha scritto: > > > On Jan 14, 2025, at 1:41 PM, Vincenzo Pupillo <v.pupillo <at> gmail.com> wrote: > > > > > > Ciao Yuan and Juri, > > > this is an updated version of mhtml-ts-mode. > > > I have tried to reduce as much as possible copies of parts of the major > > > modes from which it is derived. > > > To do this, I had to move some values that were assigned directly to > > > treesit's own variables (in ccs-mode.el, in js.el, and in > > > html-ts-mode.el) into new variables. > > > I also added three new functions to treesit.el to make it easier to > > > combine > > > parts derived from the other major modes. So now any changes to these new > > > variables are directly reflected in the behavior of mhtml-ts-mode. > > > There are a few things I would like to highlight: > > > 1. treesit-font-lock-feature-lists are not defined per parser, so simply > > > merging the different lists will cause display differences from the > > > original major-modes; for example “function” is defined at level 3 in > > > css-ts-mode but at level 4 in js-ts-mode. > > > > Yeah, that’s not pretty, I don’t have a good solution for now. Technically a > > major mode can use treesit-font-lock-recompute-features after > > treesit-major-mode-setup to do fine adjustments, but it’s ugly. > > > 2. treesit-defun-type-regexp has the same problem as treesit-font-lock- > > > feature-list, so I had to define it myself. > > > > Maybe you can use treesit-thing-settings instead? > Ok, done. > > > > > > But other than that, it works pretty well. > > > > > > IMHO, a global "list" where you can define "font-lock", "indent-list", > > > "font- lock-feature", etc. by language (perhaps with getter and setter > > > methods) might make it easier to define new multilingual major-modes. It > > > could improve the decoupling between multilingual major-modes and the > > > major-modes they are derived from. It could also better decouple the > > > internal implementation of treesit.el from the treesitter-based > > > major-modes. > > > > > > Let me know what you think. > > > > That’s what I had in mind. That’s also why I added a new variable > > treesit-aggregated-simple-imenu-settings instead of piggybacking on > > treesit-simple-imenu-settings. A separate variable is simpler to borrow > > settings from. > Yes, in this new patch I used treesit-aggregated-simple-imenu-settings. > > > But I didn’t have time to think it though yet. (By language, or by mode and > > language? Should packages set it on load, or define a function that loads > > these settings? Etc.) > > > I would say by mode and language, both mhtml-ts mode and html-ts mode have the > same primary parser. For the second question, I don't know. I don't have > enough experience with it to know what the pros and cons are. > > > Yuan > > Vincenzo > > From 199af27f1d761d100f6a8a6163c27e35e6ab2ea0 Mon Sep 17 00:00:00 2001 > From: Vincenzo Pupillo <v.pupillo <at> gmail.com> > Date: Sun, 19 Jan 2025 17:49:30 +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. > --- > etc/NEWS | 28 ++ > lisp/progmodes/js.el | 72 ++-- > lisp/textmodes/css-mode.el | 47 ++- > lisp/textmodes/html-ts-mode.el | 45 ++- > lisp/textmodes/mhtml-ts-mode.el | 600 ++++++++++++++++++++++++++++++++ > lisp/treesit.el | 65 ++++ > 6 files changed, 798 insertions(+), 59 deletions(-) > create mode 100644 lisp/textmodes/mhtml-ts-mode.el > > diff --git a/etc/NEWS b/etc/NEWS > index 0b849dec450..6360a082e9f 100644 > --- a/etc/NEWS > +++ b/etc/NEWS > @@ -965,6 +965,12 @@ destination window is chosen using 'display-buffer-alist'. Example: > > > * New Modes and Packages in Emacs 31.1 > +** New major modes based on the tree-sitter library > + > +*** New major mode 'mhtml-ts-mode'. > +An optional major mode based on the tree-sitter library for editing html > +files. This mode handles indentation, fontification, and commenting for > +embedded JavaScript and CSS. > > > * Incompatible Lisp Changes in Emacs 31.1 > @@ -1098,6 +1104,28 @@ language symbol. For example, 'cpp' is translated to "C++". A new > variable 'treesit-language-display-name-alist' holds the translations of > language symbols where that translation is not trivial. > > ++++ > +*** New function 'treesit-merge-font-lock-feature-list'. > +This function the merge two tree-sitter font lock feature lists. > +Returns a new font lock feature list with no duplicates in the same level. > +It can be used to merge font lock feature lists in a multi-language major mode. > + > ++++ > +*** New function 'treesit-replace-font-lock-feature-settings'. > +Given two treesit-font-lock-settings replaces the feature in the second > +font-lock-settings with the same feature in the first > +font-lock-settings. In a multi-linguage major mode it is sometimes > +necessary to replace features from one of the major modes, with others > +that are better suited to the new multilingual context. > + > ++++ > +*** New function 'treesit-modify-indent-rules'. > +Given two treesit ident rules, it replaces, adds, or prepends the new > +rules to the old ones, then returns a new treesit indent rules. > +In a multi-linguage major mode it is sometimes necessary to modify rules > +from one of the major modes, with others that are better suited to the > +new multilingual context. > + > +++ > *** New command 'treesit-explore' > This command replaces 'treesit-explore-mode'. It turns on > diff --git a/lisp/progmodes/js.el b/lisp/progmodes/js.el > index 101b882c718..be9d6f64aef 100644 > --- a/lisp/progmodes/js.el > +++ b/lisp/progmodes/js.el > @@ -3901,6 +3901,44 @@ js--treesit-list-nodes > (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. > @@ -3931,29 +3969,15 @@ js-ts-mode > ;; 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 > @@ -3963,17 +3987,11 @@ js-ts-mode > :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 53340195386..35c61e4f66d 100644 > --- a/lisp/textmodes/css-mode.el > +++ b/lisp/textmodes/css-mode.el > @@ -893,13 +893,7 @@ css-mode-syntax-table > (modify-syntax-entry ?? "." st) > st)) > > -(defvar-keymap css-mode-map > - :doc "Keymap used in `css-mode'." > - "<remap> <info-lookup-symbol>" #'css-lookup-symbol > - ;; `info-complete-symbol' is not used. > - "<remap> <complete-symbol>" #'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 @@ css-mode-map > ["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'." > + "<remap> <info-lookup-symbol>" #'css-lookup-symbol > + ;; `info-complete-symbol' is not used. > + "<remap> <complete-symbol>" #'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 @@ css--extract-index-name > (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 @@ css-ts-mode > ;; 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 dad49b7ed4c..5700c2b406b 100644 > --- a/lisp/textmodes/html-ts-mode.el > +++ b/lisp/textmodes/html-ts-mode.el > @@ -87,6 +87,31 @@ html-ts-mode--font-lock-settings > `((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 ,(regexp-opt '("element")) 'symbols) > + (sentence "tag") > + (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 > + '(("Element" "\\`tag_name\\'" 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." > @@ -107,30 +132,18 @@ html-ts-mode > (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 ,(regexp-opt '("element")) 'symbols) > - (sentence "tag") > - (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 > - '(("Element" "\\`tag_name\\'" nil nil))) > + (setq-local treesit-simple-imenu-settings html-ts-mode--treesit-simple-imenu-settings) > > ;; Outline minor mode. > (setq-local treesit-outline-predicate "\\`element\\'") > diff --git a/lisp/textmodes/mhtml-ts-mode.el b/lisp/textmodes/mhtml-ts-mode.el > new file mode 100644 > index 00000000000..272d00a1ef6 > --- /dev/null > +++ b/lisp/textmodes/mhtml-ts-mode.el > @@ -0,0 +1,600 @@ > +;;; mhtml-ts-mode.el --- Major mode for HTML using tree-sitter -*- lexical-binding: t; -*- > + > +;; Copyright (C) 2024 Free Software Foundation, Inc. > + > +;; Author: Vincenzo Pupillo <v.pupillo <at> gmail.com> > +;; Maintainer: Vincenzo Pupillo <v.pupillo <at> gmail.com> > +;; 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 <https://www.gnu.org/licenses/>. > + > +;;; 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 <script> and <style> HTML tags. > +By default should have same value as `html-ts-mode-indent-offset'." > + :tag "HTML javascript or css indent offset" > + :version "31.1" > + :type 'integer > + :safe 'integerp) > + > +(defcustom mhtml-ts-mode-pretty-print-command > + ;; prefer tidy because it's used by sgml-mode > + (let ((executable nil)) > + (cond ((setq executable (executable-find "tidy")) > + (format > + "%s --gnu-emacs yes --wrap 0 --indent-spaces %s -q -i -" > + executable html-ts-mode-indent-offset)) > + ((setq executable (executable-find "xmllint")) > + (format "%s --html --quiet --format -" executable)) > + (t "Install tidy, ore some other HTML pretty print tool, and set `mhtml-ts-mode-pretty-print-command'."))) > + "The command to pretty print the current HTML buffer." > + :type 'string > + :version "31.1") > + > +(defvar mhtml-ts-mode--js-css-indent-offset > + mhtml-ts-mode-js-css-indent-offset > + "Internal copy of `mhtml-ts-mode-js-css-indent-offset'. > +The value changes, by `mhtml-ts-mode--tag-relative-indent-offset' according to > +the value of `mhtml-ts-mode-tag-relative-indent'.") > + > +(defun mhtml-ts-mode--tag-relative-indent-offset (sym val) > + "Custom setter for `mhtml-ts-mode-tag-relative-indent'. > +Apart from setting the default value of SYM to VAL, also change the > +value of SYM in `mhtml-ts-mode' buffers to VAL. SYM should be > +`mhtml-ts-mode-tag-relative-indent', and VAL should be t, nil or > +`ignore'. When sym is `mhtml-ts-mode-tag-relative-indent' set the > +value of `mhtml-ts-mode--js-css-indent-offset' to 0 if VAL is t, > +otherwise to `mhtml-ts-mode-js-css-indent-offset'." > + (set-default sym val) > + (when (eq sym 'mhtml-ts-mode-tag-relative-indent) > + (setq > + mhtml-ts-mode--js-css-indent-offset > + (if (eq val t) > + mhtml-ts-mode-js-css-indent-offset > + 0)))) > + > +(defcustom mhtml-ts-mode-tag-relative-indent t > + "How <script> and <style> bodies are indented relative to the tag. > + > +When t, indentation looks like: > + > + <script> > + code(); > + </script> > + > +When nil, indentation of the tag body starts just below the > +tag, like: > + > + <script> > + code(); > + </script> > + > +When `ignore', the tag body starts in the first column, like: > + > + <script> > +code(); > + </script>" > + :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) > + > +;; Not used at the moment. > +(defun mthml-ts-mode--js-language-at-point (point) > + "Return the language at POINT assuming the point is within a Javascript region." > + (let* ((node (treesit-node-at point 'javascript)) > + (node-type (treesit-node-type node)) > + (node-start (treesit-node-start node)) > + (node-end (treesit-node-end node))) > + (if (not (treesit-ready-p 'jsdoc t)) > + 'javascript > + (if (equal node-type "comment") > + (save-excursion > + ;; (message "node start = %s , end = %s" node-start node-end) > + (goto-char node-start) > + (if (search-forward "/**" node-end t) > + 'jsdoc > + 'javascript)) > + 'javascript)))) > + > +;; 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) 'javascript) > + ;; ((equal "(script_element (raw_text))" node-query) 'mthml-ts-mode--js-language-at-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 <script> or <style>. > +Return `line-beginning-position' when `treesit-node-at' is html, or > +`mhtml-ts-mode-tag-relative-indent' is equal to ignore. > +NODE and PARENT are ignored." > + (if (or (eq (treesit-language-at (point)) 'html) > + (eq mhtml-ts-mode-tag-relative-indent 'ignore)) > + (line-beginning-position) > + ;; Ok, we are in js or css block. > + (save-excursion > + (re-search-backward "<script.*>\\|<style.*>" nil t)))) > + > +;; Treesit supports 4 level of decoration, `treesit-font-lock-level' > +;; define which level to use. Major modes categorize their fontification > +;; features, these categories are defined by `treesit-font-lock-rules' of > +;; each major-mode using :feature keyword. > +;; In a multiple language Major mode it's a good idea to provide, for each > +;; level, the union of the :feature of the same level. > +;; TODO: Since the feature-list is not defined per "parser" (like, for > +;; example, the thing-settings), the same feature can appear in > +;; different levels, so the appearance of a multiple main mode can be > +;; different from the main mode used. For e.g the feature "function" is > +;; at level 4 for Javascript while it is at level 3 for CSS. > +(defvar mhtml-ts-mode--treesit-font-lock-feature-list > + (treesit-merge-font-lock-feature-list > + html-ts-mode--treesit-font-lock-feature-list > + (treesit-merge-font-lock-feature-list > + js--treesit-font-lock-feature-list > + css--treesit-font-lock-feature-list)) > + "Settings for `treesit-font-lock-feature-list'.") > + > +(defvar mhtml-ts-mode--treesit-font-lock-settings > + (append html-ts-mode--font-lock-settings > + js--treesit-font-lock-settings > + ;; Let's replace a css rule with a new one that adds color to > + ;; the css value. > + (treesit-replace-font-lock-feature-settings > + (treesit-font-lock-rules > + :language 'css > + :override t > + :feature 'variable > + '((plain_value) @font-lock-variable-name-face > + (plain_value) @mhtml-ts-mode--colorize-css-value)) > + css--treesit-settings)) > + "Settings for `treesit-font-lock-settings'.") > + > +(defvar mhtml-ts-mode--treesit-thing-settings > + ;; In addition to putting together the various definitions, we need to > + ;; add 'defun' which is used to support `imenu' and 'which-function'. > + (list > + ;; HTML thing settings > + (append > + (car html-ts-mode--treesit-things-settings) > + `((defun ,(regexp-opt (list html-ts-mode--treesit-defun-type-regexp))))) > + ;; Javascript thing settings > + (append > + (car js--treesit-thing-settings) > + `((defun ,js--treesit-defun-type-regexp))) > + ;; CSS thing settings > + `(css > + (defun ,(regexp-opt (list css--treesit-defun-type-regexp))))) > + "Settings for `treesit-thing-settings'.") > + > +(defvar mhtml-ts-mode--treesit-indent-rules > + (treesit--indent-rules-optimize > + (append html-ts-mode--indent-rules > + ;; Extended rules for js and css, to > + ;; indent appropriately when injected > + ;; into html > + (treesit-modify-indent-rules > + `((javascript ((parent-is "program") > + mhtml-ts-mode--js-css-tag-bol > + mhtml-ts-mode--js-css-indent-offset))) > + js--treesit-indent-rules > + :replace) > + (treesit-modify-indent-rules > + `((css ((parent-is "stylesheet") > + mhtml-ts-mode--js-css-tag-bol > + mhtml-ts-mode--js-css-indent-offset))) > + css--treesit-indent-rules 'prepend) > + :replace)) > + "Settings for `treesit-simple-indent-rules'.") > + > +(defvar mhtml-ts-mode--treesit-aggregated-simple-imenu-settings > + `((html ,@html-ts-mode--treesit-simple-imenu-settings) > + (javascript ,@js--treesit-simple-imenu-settings) > + (css ,@css--treesit-simple-imenu-settings)) > + "Settings for `treesit-simple-imenu'.") > + > +;; TODO: treesit-defun-type-regexp should have an aggregated version, > +;; like treesit-aggregated-simple-imenu-settings. Otherwise we can't > +;; reuse the regex defined in the major mode we use. > +(defvar mhtml-ts-mode--treesit-defun-type-regexp > + (regexp-opt '("class_declaration" > + "method_definition" > + "function_declaration" > + "lexical_declaration" > + "element" > + "rule_set")) > + "Settings for `treesit-defun-type-regexp'.") > + > +;; In order to support `prettify-symbols-mode', just `append' the prettify > +;; alist of all the languages. In our case only javascript defined this alist. > +(defvar mhtml-ts-mode--prettify-symbols-alist js--prettify-symbols-alist > + "Alist of symbol prettifications for various supported languages.") > + > +;; In order to support `which-fuction-mode' we should define > +;; a function that return the defun name. > +;; In a multilingual treesit mode, this can be implemented simply by > +;; calling language-specific functions. > +(defun mhtml-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." > + (let ((html-name (html-ts-mode--defun-name node)) > + (js-name (js--treesit-defun-name node)) > + (css-name (css--treesit-defun-name node))) > + (cond > + (html-name html-name) > + (js-name js-name) > + (css-name css-name)))) > + > +;;; Flymake integration > + > +(defvar-local mhtml-ts-mode--flymake-process nil > + "Store the Flymake process.") > + > +(defun mhtml-ts-mode-flymake-mhtml (report-fn &rest _args) > + "MHTML backend for Flymake. > +Calls REPORT-FN directly. Requires tidy." > + (when (process-live-p mhtml-ts-mode--flymake-process) > + (kill-process mhtml-ts-mode--flymake-process)) > + (let ((tidy (executable-find "tidy")) > + (source (current-buffer)) > + (diagnostics-pattern (eval-when-compile > + (rx bol > + "line " (group (+ num)) ;; :1 line > + " column " (group (+ num)) ;; :2 column > + " - " (group (+? nonl)) ;; :3 type > + ": " (group (+? nonl)) ;; :4 msg > + eol)))) > + (if (not tidy) > + (error "Unable to find tidy command") > + (save-restriction > + (widen) > + (setq mhtml-ts-mode--flymake-process > + (make-process > + :name "mhtml-ts-mode-flymake" > + :noquery t > + :connection-type 'pipe > + :buffer (generate-new-buffer "*mhtml-ts-mode-flymake*") > + :command `(,tidy "--gnu-emacs" "yes" "-e" "-q") > + :sentinel > + (lambda (proc _event) > + (when (eq 'exit (process-status proc)) > + (unwind-protect > + (if (with-current-buffer source > + (eq proc mhtml-ts-mode--flymake-process)) > + (with-current-buffer (process-buffer proc) > + (goto-char (point-min)) > + (let (diags) > + (while (search-forward-regexp diagnostics-pattern nil t) > + (let* ((pos > + (flymake-diag-region > + source > + (string-to-number (match-string 1)) > + (string-to-number (match-string 2)))) ;; line and column > + (type (cond ((equal (match-string 3) "Warning") :warning) > + ((equal (match-string 3) "Error") :error))) ;; type of message > + (msg (match-string 4))) ;; message > + (push (flymake-make-diagnostic source (car pos) (cdr pos) type msg) > + diags))) > + (funcall report-fn diags))) > + (flymake-log :warning "Canceling obsolete check %s" proc)) > + (kill-buffer (process-buffer proc))))))) > + (process-send-region mhtml-ts-mode--flymake-process (point-min) (point-max)) > + (process-send-eof mhtml-ts-mode--flymake-process))))) > + > +(define-derived-mode mhtml-ts-mode html-ts-mode > + '("HTML+" (:eval (let ((lang (mhtml-ts-mode--language-at-point (point)))) > + (cond ((eq lang 'html) "") > + ((eq lang 'javascript) "JS") > + ((eq lang 'css) "CSS"))))) > + "Major mode for editing HTML with embedded JavaScript and CSS. > +Powered by tree-sitter." > + (if (not (and > + (treesit-ready-p 'html) > + (treesit-ready-p 'javascript) > + (treesit-ready-p 'css))) > + (error "Tree-sitter parsers for HTML isn't available. You can > + install the parsers with M-x `mhtml-ts-mode-install-parsers'") > + > + ;; When an language is embedded, you should initialize some variable > + ;; just like it's done in the original mode. > + > + ;; Comment. > + ;; indenting settings for js-ts-mode. > + (c-ts-common-comment-setup) > + (setq-local comment-multi-line t) > + > + ;; Font-lock. > + > + ;; There are two ways to handle embedded code: > + ;; 1. Use a single parser for all the embedded code in the buffer. In > + ;; this case, the embedded code blocks are concatenated together and are > + ;; seen as a single continuous document to the parser. > + ;; 2. Each embedded code block gets its own parser. Each parser only sees > + ;; that particular code block. > + > + ;; If you go with 2 for a language, the local parsers are created and > + ;; destroyed automatically by Emacs. So don't create a global parser for > + ;; that embedded language here. > + > + ;; Create the parsers, only the global ones. > + ;; jsdoc is a local parser, don't create a parser for it. > + (treesit-parser-create 'css) > + (treesit-parser-create 'javascript) > + > + ;; Multi-language modes must set the primary parser. > + (setq-local treesit-primary-parser (treesit-parser-create 'html)) > + > + (setq-local treesit-range-settings > + (treesit-range-rules > + :embed 'javascript > + :host 'html > + '((script_element > + (start_tag (tag_name)) > + (raw_text) @cap)) > + > + ;; Another rule could be added that when it matches an > + ;; attribute_value that has as its parent an > + ;; attribute_name "style" it captures it and then > + ;; passes it to the css parser. > + :embed 'css > + :host 'html > + '((style_element > + (start_tag (tag_name)) > + (raw_text) @cap)))) > + > + ;; jsdoc is not mandatory for js-ts-mode, so we respect this by > + ;; adding jsdoc range rules only when jsdoc is available. > + (when (treesit-ready-p 'jsdoc t) > + (setq-local treesit-range-settings > + (append treesit-range-settings > + (treesit-range-rules > + :embed 'jsdoc > + :host 'javascript > + :local t > + `(((comment) @cap > + (:match ,js--treesit-jsdoc-beginning-regexp @cap)))))) > + (setq-local c-ts-common--comment-regexp > + js--treesit-jsdoc-comment-regexp)) > + > + > + ;; Many treesit fuctions need to know the language at-point. > + ;; So you should define such a function. > + (setq-local treesit-language-at-point-function #'mhtml-ts-mode--language-at-point) > + (setq-local prettify-symbols-alist mhtml-ts-mode--prettify-symbols-alist) > + > + ;; Indent. > + > + ;; Since `mhtml-ts-mode' inherits indentation rules from `html-ts-mode', `js-ts-mode' > + ;; and `css-ts-mode', if you want to change the offset you have to act on the > + ;; *-offset variables defined for those languages. > + > + ;; JavaScript and CSS must be indented relative to their code block. > + ;; This is done by inserting a special rule before the normal > + ;; indentation rules of these languages. > + ;; The value of `mhtml-ts-mode--js-css-indent-offset' changes based on > + ;; `mhtml-ts-mode-tag-relative-indent' and can be used to indent > + ;; JavaScript and CSS code relative to the HTML that contains them, > + ;; just like in mhtml-mode. > + (setq-local treesit-simple-indent-rules mhtml-ts-mode--treesit-indent-rules) > + > + ;; Navigation. > + > + ;; This is for which-function-mode. > + ;; Since mhtml-ts-mode is derived from html-ts-mode, which sets > + ;; the value of `treesit-defun-type-regexp', you have to reset it to nil > + ;; otherwise `imenu' and `which-function-mode' will not work. > + (setq-local treesit-defun-type-regexp nil) > + > + ;; This is for finding defun name, it's used by IMenu as default > + ;; function no specific functions are defined. > + (setq-local treesit-defun-name-function #'mhtml-ts-mode--defun-name) > + > + ;; Define what are 'thing' for treesit. > + ;; 'Thing' is a symbol representing the thing, like `defun', `sexp', or > + ;; `sentence'. > + ;; As an alternative, if you want just defun, you can define a `treesit-defun-type-regexp'. > + (setq-local treesit-thing-settings mhtml-ts-mode--treesit-thing-settings) > + > + ;; Font-lock. > + > + ;; In a multi-language scenario, font lock settings are usually a > + ;; concatenation of language rules. As you can see, it is possible > + ;; to extend/modify the default rule or use a different set of > + ;; rules. See `php-ts-mode--custom-html-font-lock-settings' for more > + ;; advanced usage. > + (setq-local treesit-font-lock-settings mhtml-ts-mode--treesit-font-lock-settings) > + > + ;; Tells treesit the list of features to fontify. > + (setq-local treesit-font-lock-feature-list mhtml-ts-mode--treesit-font-lock-feature-list) > + > + ;; Imenu > + > + ;; Setup Imenu: if no function is specified, try to find an object > + ;; using `treesit-defun-name-function'. > + (setq-local treesit-aggregated-simple-imenu-settings > + mhtml-ts-mode--treesit-aggregated-simple-imenu-settings) > + > + (treesit-major-mode-setup) > + > + ;; This is sort of a prog-mode as well as a text mode. > + (run-mode-hooks 'prog-mode-hook) > + > + ;; Flymake > + (add-hook 'flymake-diagnostic-functions #'mhtml-ts-mode-flymake-mhtml nil 'local))) > + > +;; Add nome extra parents. > +(derived-mode-add-parents 'mhtml-ts-mode '(css-mode js-mode)) > + > +(when (and (treesit-ready-p 'html) (treesit-ready-p 'javascript) (treesit-ready-p 'css)) > + (add-to-list > + 'auto-mode-alist '("\\.[sx]?html?\\(\\.[a-zA-Z_]+\\)?\\'" . mhtml-ts-mode))) > + > +(provide 'mhtml-ts-mode) > +;;; mhtml-ts-mode.el ends here > diff --git a/lisp/treesit.el b/lisp/treesit.el > index 8d86d142e3f..00b7d266e74 100644 > --- a/lisp/treesit.el > +++ b/lisp/treesit.el > @@ -1297,6 +1297,40 @@ treesit-font-lock-recompute-features > ((memq feature remove-list) nil) > (t current-value)))))) > > +(defun treesit-merge-font-lock-feature-list (features-list-1 features-list-2) > + "Merge two tree-sitter font lock feature lists. > +Returns a new font lock feature list with no duplicates in the same level. > +It can be used to merge font lock feature lists in a multi-language major mode. > +FEATURES-LIST-1 and FEATURES-LIST-2 are list of lists of feature symbols." > + (let ((result nil) > + (features-1 (car features-list-1)) > + (features-2 (car features-list-2))) > + (while (or features-1 features-2) > + (cond > + ((and features-1 (not features-2)) (push features-1 result)) > + ((and (not features-1) features-2) (push features-2 result)) > + ((and features-1 features-2) (push (cl-union features-1 features-2) result))) > + (setq features-list-1 (cdr features-list-1) > + features-list-2 (cdr features-list-2) > + features-1 (car features-list-1) > + features-2 (car features-list-2))) > + (nreverse result))) > + > +(defun treesit-replace-font-lock-feature-settings (new-settings settings) > + "Replaces :feature in SETTINGS with :feature from NEW-SETTINGS. > +Both SETTINGS and NEW-SETTINGS must be a value suitable for > +`treesit-font-lock-settings'. > +Return a value suitable for `treesit-font-lock-settings'" > + (let ((result nil)) > + (dolist (new-setting new-settings) > + (let ((new-feature (treesit-font-lock-setting-feature new-setting))) > + (dolist (setting settings) > + (let ((feature (treesit-font-lock-setting-feature setting))) > + (if (eq new-feature feature) > + (push new-setting result) > + (push setting result)))))) > + (nreverse result))) > + > (defun treesit-add-font-lock-rules (rules &optional how feature) > "Add font-lock RULES to the current buffer. > > @@ -2401,6 +2435,37 @@ treesit--indent-rules-optimize > offset))))) > (cons lang (mapcar #'optimize-rule indent-rules))))) > > +(defun treesit-modify-indent-rules (new-rules rules &optional how) > + "Modify RULES using NEW-RULES. > +As default replace rules with the same anchor. > +When HOW is :prepend NEW-RULES are prepend to RULES, when > +:append NEW-RULES are appended to RULES, when :replace (the default) > +NEW-RULES replace rule in RULES which the same anchor." > + (let ((n-lang (car (car new-rules))) > + (lang (car (car rules)))) > + (when (not (eq n-lang lang)) > + (error "The language must be the same")) > + (let* ((nr (cdr (car new-rules))) > + (r (cdr (car rules))) > + (tmp nil) > + (result > + (cond > + ((eq how :prepend) > + (append nr r)) > + ((eq how :append) > + (append r nr)) > + ((or (eq how :replace) t) > + (nreverse > + (progn > + (dolist (new-rule nr) > + (dolist (rule r) > + (if (equal (nth 0 new-rule) (nth 0 rule)) > + (push new-rule tmp) > + (push rule tmp)))) > + tmp)))))) > + (push lang result) > + `(,result)))) > + > ;;; Search > > (defun treesit-search-forward-goto > -- > 2.48.1 >
GNU bug tracking system
Copyright (C) 1999 Darren O. Benham,
1997,2003 nCipher Corporation Ltd,
1994-97 Ian Jackson.