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

From: Yuan Fu <[email-masked]>
Date: Wed, 26 Mar 2025 21:20:31 -0700

> On Mar 26, 2025, at 12:27 AM, Juri Linkov <[email-masked]> wrote:
> 
>> 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.
> 
> 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.

Looks reasonable to me. But if it’s a minor mode, we might need to have a way to negate the change made to treesit-font-lock-settings? OTOH if we use :override, we might run into an override arm race when enabling multiple minor modes, etc.

Yuan