Subject: Re: bug#78402: treesit after-change-functions

From: Yuan Fu <[email-masked]>
Date: Fri, 6 Jun 2025 00:55:04 -0700

> On Jun 4, 2025, at 10:16 AM, Juri Linkov <[email-masked]> wrote:
> 
>> Hmm, not sure what I’m doing wrong but I still can’t trigger the
>> error. I’m using emacs -q, with Stefan’s patch, enabled liquid mode
>> and outline-minor-mode, and deleted the <script> tag on line 19.
>> Any ideas?
> 
> Sorry, I missed essential non-default settings:
> 
>  (setq outline-minor-mode-cycle t)
>  (setq outline-minor-mode-use-buttons t)
> 
> Also (setq debug-on-error t) is nice to have.
> So here is a complete test case:
> 
> 0. emacs -Q
> 1. M-: (setq outline-minor-mode-cycle t)
> 2. M-: (setq outline-minor-mode-use-buttons t)
> 3. M-: (setq debug-on-error t)
> 4. C-x p f test/manual/indent/html-multi.html
> 5. M-x load-library RET treesit-x
> 6. M-x liquid-generic-ts-mode
> 7. M-x outline-minor-mode
> 8. move point to the first 's' in 'script' on line 19
> 9. type 'M-d' ('kill-word')

Aha, I reproduced it. It didn’t happen when I kill-word, but happened when I undo. I’m still trying to understand why the node-outdated error can happen. Each parser has a tick, which is incremented whenever it re-parses. When we create a node from the parser, the node inherits the current tick number of the parser. We only consider a node outdated when its tick is less than its parser’s tick, meaning the parser re-parsed after the node is created. For a parser to re-parse, it has to a) receive a buffer edit or b) change it’s ranges; and then someone needs to request a node from it.

That means in the following backtrace, the parser received a buffer edit or changed its ranges after the `prev` node is created. And that’s baffling to me.

  treesit-node-start(#<treesit-node-outdated>)
  (if (consp smaller) (car smaller) (treesit-node-start smaller))
  (let ((larger-start (if (consp larger) (car larger) (treesit-node-start larger))) (larger-end (if (consp larger) (cdr larger) (treesit-node-end larger))) (smaller-start (if (consp smaller) (car smaller) (treesit-node-start smaller))) (smaller-end (if (consp smaller) (cdr smaller) (treesit-node-end smaller)))) (cond ((eq strict 't) (let nil (and (< larger-start smaller-start) (< smaller-end larger-end)))) ((eq strict 'partial) (let nil (and (or (not (eq larger-start smaller-start)) (not (eq larger-end smaller-end))) (<= larger-start smaller-start smaller-end larger-end)))) (t (let nil (<= larger-start smaller-start smaller-end larger-end)))))
  treesit-node-enclosed-p(#<treesit-node-outdated> #<treesit-node element in 26-379>)
  (not (treesit-node-enclosed-p prev parent))
  (and parent prev (not (treesit-node-enclosed-p prev parent)))
  (if (and parent prev (not (treesit-node-enclosed-p prev parent))) (progn (setq prev nil)))
  (let ((prev (treesit-thing-prev pos thing)) (next (treesit-thing-next pos thing)) (parent (treesit-thing-at pos thing t))) (if (and parent prev (not (treesit-node-enclosed-p prev parent))) (progn (setq prev nil))) (if (and parent next (not (treesit-node-enclosed-p next parent))) (progn (setq next nil))) (if (and (eq tactic 'top-level) parent) (progn (progn (setq parent (treesit-node-top-level parent thing t)) (setq prev nil) (setq next nil)))) (if (eq tactic 'restricted) (setq pos (funcall advance (cond ((and (null next) (null prev)) parent) ((> arg 0) next) (t prev)))) (if (> arg 0) (if (and (eq side 'beg) (cond (next (and ... ...)) (parent t))) (setq pos (or (treesit-navigate-thing (treesit-node-end ...) 1 'beg thing tactic t) (throw 'term nil))) (setq pos (funcall advance (or next parent)))) (if (and (eq side 'end) (cond (prev (and ... ...)) (parent t))) (setq pos (or (treesit-navigate-thing (treesit-node-start ...) -1 'end thing tactic t) (throw 'term nil))) (setq pos (funcall advance (or prev parent)))))) (setq counter (- counter 1)))

> 
>>>> BTW, I noticed that when tree-sitter-provided outline is in effect,
>>>> pressing tab on the lines determined as outline titles—like first line
>>>> of a defun—toggles folding instead of indenting the code. That feels
>>>> intrusive IMHO. It’s not a problem for “traditional” kind of outline
>>>> headers in comments, but code is another story.
>>>> 
>>>> Take this code as example:
>>>> 
>>>> function MyBigFunc(param1
>>>>                 param2) {
>>>> const abc = 'def';
>>>> function embed() {
>>>>   return true;
>>>> }
>>>> }
>>>> 
>>>> Right now I can’t indent the embed function, because pressing TAB
>>>> folds it. Another common practice is to select the whole MyBigFunc
>>>> function and press TAB to indent everything in the region, that
>>>> wouldn’t work if point happens to be on the first line, which is
>>>> common.
>>> 
>>> Indeed, there is a clash between TAB keybindings.
>>> So to address this ambiguity we created a special option
>>> 'outline-minor-mode-cycle-filter' that defines in what context
>>> TAB should fold outlines.  For example, you can customize:
>>> 
>>> (setopt outline-minor-mode-cycle-filter (lambda () (bolp)))
>>> 
>>> Then TAB will fold only when pressed at the beginning of the line.
>>> Anywhere else TAB will indent the line.
>> 
>> Thanks. Digging further, I’m only hitting this problem because I have
>> outline-minor-mode-cycle set to t. So technically with default
>> settings, one wouldn’t have this problem.
>> 
>> But I can’t be the only one that sets outline-minor-mode-cycle to t,
>> expecting to only cycle outline sections in comments. Can we do something
>> to prevent people tripping on this? Maybe change the default value of
>> outline-minor-mode-cycle-filter to exclude code?
> 
> Do you use the default 'outline-regexp'?  For example in emacs-lisp-mode
> it matches not only comments, but also opening parens at the beginning
> of function definitions in code.  So I don't understand how it would be
> useful to fold only comments, but not code.

Ah, I guess never pressed TAB on the first line of a defun in elisp mode. I still think it’ll be problematic on languages that has nested defun (eg, javascript). But maybe I’m being old man yell at cloud. I guess we can see if there’s someone that actually complains about it.

Yuan