Subject: Re: bug#77256: Treesit language-at-point

From: Yuan Fu <[email-masked]>
Date: Wed, 9 Apr 2025 00:26:04 -0700

> On Apr 5, 2025, at 11:51 PM, Juri Linkov <[email-masked]> wrote:
> 
>>>> Unfortunately, I discovered that it causes treesit-node-outdated errors.
>>>> 
>>>> This is because overlays are updated by 'pre-redisplay-functions'
>>>> later than outline-minor-mode uses them by 'after-change-functions',
>>>> since 'after-change-functions' runs before 'pre-redisplay-functions'.
>>> 
>>> This fixes the problem:
>>> 
>>>  (defun outline--fix-buttons-after-change (beg end _len)
>>> +  (when (fboundp 'treesit-update-ranges)
>>> +    (treesit-update-ranges beg end))
>>>    ;; Handle whole lines
>>>    (save-excursion (goto-char beg) (setq beg (pos-bol)))
>>>    (save-excursion (goto-char end) (setq end (pos-eol)))
>> 
>> Using treesit-update-ranges is the right approach.  But is there no
>> better place to put it?  I assume there should be a function that
>> involves tree-sitter, in front of which you can put this call.
> 
> 'treesit-outline-search' has no information about the positions of
> the beginning and end of the range of changed text.  Only
> 'after-change-functions' has access to this information.
> 
> So what we could do is this, and I've tested that this approach works:
> 
> #+begin_src diff
> diff --git a/lisp/treesit.el b/lisp/treesit.el
> index 07861603244..c77b28c4647 100644
> --- a/lisp/treesit.el
> +++ b/lisp/treesit.el
> @@ -4044,6 +4112,10 @@ treesit-outline-level
> 
>     level))
> 
> +(defun treesit--after-change (beg end _len)
> +  "Force updating the ranges after each text change."
> +  (treesit-update-ranges beg end))
> +
> ;;; Hideshow mode
> 
> (defun treesit-hs-block-end ()
> @@ -4338,7 +4410,16 @@ treesit-major-mode-setup
>       (setq treesit-outline-predicate
>             #'treesit-outline-predicate--from-imenu))
>     (setq-local outline-search-function #'treesit-outline-search
> -                outline-level #'treesit-outline-level))
> +                outline-level #'treesit-outline-level)
> +    (add-hook 'outline-minor-mode-hook
> +              (lambda ()
> +                (if (bound-and-true-p outline-minor-mode)
> +                    (add-hook 'after-change-functions
> +                              #'treesit--after-change
> +                              0 t)
> +                  (remove-hook 'after-change-functions
> +                               #'treesit--after-change t)))
> +              nil t))
> 
>   ;; Remove existing local parsers.
>   (dolist (ov (overlays-in (point-min) (point-max)))
> #+end_src

This seems a bit convoluted to me. What do you think about adding some hook or generic function to outline-minor-mode?

>>> But can we do better?  I see that 'treesit-major-mode-setup'
>>> adds the notifier
>>> 
>>>     (treesit-parser-add-notifier
>>>      treesit-primary-parser #'treesit--font-lock-mark-ranges-to-fontify)
>>> 
>>> to 'after_change_functions'.  But why the treesit
>>> 'after_change_functions' notifier is called after the
>>> 'after-change-functions' hook?  Can we change their order?
>> 
>> Because we parse lazily. And I should’ve called them after-parse functions
>> in the docstring (good thing I named them notifiers, not
>> after-change-functions)—they get called after the parser re-parses, but the
>> parser doesn’t have to re-parse immediately after a buffer change. It only
>> re-parses when some Lisp asks for a node from the parse tree.
>> 
>> In practice, that usually happens in pre-redisplay-function where we set
>> ranges for embedded parsers, which causes the primary parser to re-parse
>> and return a root node for querying. As a (intended) side-effect, the
>> notifiers get called and mark relevant regions to be re-fontified.
> 
> Would it be possible on the first attempt of Lisp code to access a node
> to detect such a situation that there are some outdated nodes and
> to update their ranges automatically?

Probably not, since you need to have correct ranges to know which parser to use to get nodes in the first place.

Yuan