Subject: Re: bug#77255: Treesit font-lock override for embed ranges

From: Vincenzo Pupillo <[email-masked]>
Date: Wed, 26 Mar 2025 17:07:58 +0100

Caio Juri,

In data mercoledì 26 marzo 2025 08:27:14 Ora standard dell’Europa centrale, 
Juri Linkov ha scritto:
> > It looks like we need a new keyword like ':override t' for
> > 'treesit-range-rules' that would override host font-lock rules.
> 
> Or maybe not.  I managed to do this without any changes in core.
> Also not sure if such functions as treesit-merge-font-lock-feature-list
> and treesit-replace-font-lock-feature-settings could be used here.
> 
treesit-replace-font-lock-feature-settings works reliably only if the 
replacement is done for rules of the same language. So something like:

#+begin_src emacs-lisp
(setq-local liquid-ts-mode--font-lock-feature-list (treesit-replace-font-lock-
feature-settings
		 (treesit-font-lock-rules
		  :language 'html
		  :override t
		  :feature 'string
		  `((attribute
		     (attribute_name) @_name
		     (:pred mhtml-ts-mode--not-match @_name)
		     (quoted_attribute_value) @font-lock-string-face)))

		 html-ts-mode--treesit-font-lock-settings)
#+end_src

Then:
 
(defvar mhtml-ts-mode--treesit-font-lock-feature-list
  (treesit-merge-font-lock-feature-list
   liquid-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'.")

However, we could modify treesit-replace-font-lock-feature-settings to check 
the language in addition to the feature.

Vincenzo

> The solution below works simply by replacing the html rule with
> another rule that matches only HTML attributes that don't contain
> js code:
> 
> #+begin_src emacs-lisp
>     (setq-local treesit-range-settings
>                 (append treesit-range-settings
>                         (treesit-range-rules
> 
>                          :embed 'javascript
>                          :host 'html
>                          :local t
> 
>                          `((attribute
>                             (attribute_name) @_name
>                             (:match ,(rx (or "x-data" "x-bind" "x-text"))
> @_name) (quoted_attribute_value
>                              (attribute_value) @cap))))))
> 
>     (setq-local treesit-font-lock-settings
>                 (mapcar (lambda (s)
>                           (if (and (eq (treesit-query-language
>                                         (treesit-font-lock-setting-query s))
> 'html)
>                                    (eq (treesit-font-lock-setting-feature s)
> 'string))
>                               (car (treesit-font-lock-rules
> 
>                                     :language 'html
>                                     :override t
>                                     :feature 'string
> 
>                                     `((attribute
>                                        (attribute_name) @_name
>                                        ;; (:match (not ,(rx (or "x-data"
> "x-bind" "x-text"))) @_name) ;; (:pred (lambda (node)
>                                        ;;           (not (string-match-p (rx
> (or "x-data" "x-bind" "x-text")) ;;                 (treesit-node-text node
> t)))) ;;        @_name)
>                                        (:pred mhtml-ts-mode--not-match
> @_name) (quoted_attribute_value) @font-lock-string-face)))) s))
>                         treesit-font-lock-settings))
> #+end_src
> 
> The commented out code shows attempts to use a negated :match
> that is not supported.  Also it seems a lambda for :pred is
> also not supported.  So needed to add a separate function:
> 
> #+begin_src emacs-lisp
> (defun mhtml-ts-mode--not-match (node)
>   (not (string-match-p (rx (or "x-data" "x-bind" "x-text"))
>                        (treesit-node-text node t))))
> #+end_src
> 
> Then everything works: all HTML attributes are highlighted
> except those that should highlight js code in them.