]> git.eshelyaron.com Git - emacs.git/commitdiff
Add mhtml-ts-mode.
authorVincenzo Pupillo <v.pupillo@gmail.com>
Fri, 14 Feb 2025 17:38:51 +0000 (18:38 +0100)
committerEshel Yaron <me@eshelyaron.com>
Tue, 18 Feb 2025 08:51:53 +0000 (09:51 +0100)
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
lisp/textmodes/css-mode.el
lisp/textmodes/html-ts-mode.el
lisp/textmodes/mhtml-ts-mode.el [new file with mode: 0644]
lisp/treesit.el

index 81cf02664c646098fe5a22a14889fdc6ca7bef03..6184298e24a93410d688feaf477a5fdd18d2003a 100644 (file)
@@ -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
index efcfcecf9d9549f1aeb6808ecc131ffd00e71d09..85fe59afef8b29fdc6cee88878e0d5d2d9b9c988 100644 (file)
@@ -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'."
-  "<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 @@ 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'."
+  "<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 @@ 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))))
index 0f07fbedeed560bd46f2ddd734779379740238c4..26efe1be7262808f6ad5362609288d5ee4b7090c 100644 (file)
    `((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 (file)
index 0000000..9be1a14
--- /dev/null
@@ -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 <v.pupillo@gmail.com>
+;; Maintainer: Vincenzo Pupillo <v.pupillo@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)
+
+;; 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 <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
+            `((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
+            `((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.")
+
+(defun mhtml-ts-mode--html-defun-name (node)
+  "Return the defun name of NODE.
+Return nil if there is no name or if NODE is not a defun node."
+  (when (string-match-p "element" (treesit-node-type node))
+    (treesit-node-text
+     node
+     ;; (treesit-search-subtree node "\\`tag_name\\'" nil nil 2)
+     t)))
+
+;; 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)
+
+    ;; (setq-local treesit-outline-predicate nil)
+
+    (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
index 5fabdc323142815595b1c67a0f643e3974deb04f..f121b15141a88d705c1942df0f0a6daeac21987e 100644 (file)
@@ -1313,6 +1313,40 @@ and leave settings for other languages unchanged."
                        ((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.
 
@@ -2498,6 +2532,40 @@ end of existing rules."
               (append rules existing-rules)))))
     (setf (alist-get language treesit-simple-indent-rules) new-rules)))
 
+(defun treesit-modify-indent-rules (lang new-rules rules &optional how)
+  "Modify a copy of 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."
+  (cond
+   ((not (alist-get lang rules))
+    (error "No rules for language %s in RULES" lang))
+   ((not (alist-get lang new-rules))
+    (error "No rules for language %s in NEW-RULES" lang))
+   (t (let* ((copy-of-rules (copy-tree js--treesit-indent-rules))
+            (lang-rules (alist-get lang copy-of-rules))
+            (lang-new-rules (alist-get lang new-rules)))
+       (cond
+        ((eq how :prepend)
+         (setf (alist-get lang copy-of-rules)
+               (append lang-new-rules lang-rules)))
+        ((eq how :append)
+         (setf (alist-get lang copy-of-rules)
+               (append lang-rules lang-new-rules)))
+        ((or (eq how :replace) t)
+         (let ((tail-new-rules lang-new-rules)
+               (tail-rules lang-rules)
+               (new-rule nil)
+               (rule nil))
+           (while (setq new-rule (car tail-new-rules))
+             (while (setq rule (car tail-rules))
+               (when (equal (nth 0 new-rule) (nth 0 rule))
+                 (setf (car tail-rules) new-rule))
+               (setq tail-rules (cdr tail-rules)))
+             (setq tail-new-rules (cdr tail-new-rules))))))
+       copy-of-rules))))
+
 ;;; Search
 
 (defun treesit-search-forward-goto