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

From: Juri Linkov <[email-masked]>
Date: Sun, 06 Apr 2025 09:51:27 +0300

>>> 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

>> 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?