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

From: Juri Linkov <[email-masked]>
Date: Wed, 09 Apr 2025 20:16:19 +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
>
> This seems a bit convoluted to me.  What do you think about adding
> some hook or generic function to outline-minor-mode?

Is this better?
diff --git a/lisp/outline.el b/lisp/outline.el
index 1209b1ce766..ff1322b1e93 100644
--- a/lisp/outline.el
+++ b/lisp/outline.el
@@ -1980,7 +1980,11 @@ outline--fix-up-all-buttons
          (outline--insert-button (if close-p 'close 'open))))
      (or from (point-min)) (or to (point-max)))))
 
-(defun outline--fix-buttons-after-change (beg end _len)
+(defvar outline-after-change nil
+  "List of functions to call after each text change in outline-mode.")
+
+(defun outline--fix-buttons-after-change (beg end len)
+  (run-hook-with-args 'outline-after-change beg end len)
   ;; Handle whole lines
   (save-excursion (goto-char beg) (setq beg (pos-bol)))
   (save-excursion (goto-char end) (setq end (pos-eol)))
diff --git a/lisp/treesit.el b/lisp/treesit.el
index 8e57a6dae14..05523bc857c 100644
--- a/lisp/treesit.el
+++ b/lisp/treesit.el
@@ -4069,7 +4069,8 @@ treesit-outline-level
     level))
 
 (defun treesit--after-change (beg end _len)
-  "Force updating the ranges after each text change."
+  "Force updating the ranges in BEG...END.
+Expected to be called after each text change."
   (treesit-update-ranges beg end))
 
 ;;; Hideshow mode
@@ -4367,15 +4368,7 @@ treesit-major-mode-setup
             #'treesit-outline-predicate--from-imenu))
     (setq-local outline-search-function #'treesit-outline-search
                 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))
+    (add-hook 'outline-after-change #'treesit--after-change nil t))
 
   ;; Remove existing local parsers.
   (dolist (ov (overlays-in (point-min) (point-max)))
>>>> 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.