Subject: bug#78703: beginning-of-defun and friends still wrong in typescript-ts-mode

From: Troy Brown <[email-masked]>
Date: Sat, 7 Jun 2025 10:03:24 -0400

On Sat, Jun 7, 2025 at 2:09 AM Yuan Fu <[email-masked]> wrote:
>
> So for this tactic, point should move out of the enclosing defun if it is inside a defun; and if point isn’t inside any defun it should move to the previous defund-beginning?

Care must be taken when talking about being "inside" or "not inside" a
defun.  Unless point is before, after, or between top-level defuns, it
will always be inside some defun.  I previously suggested
differentiating behavior based on whether point was at a defun
boundary.  By boundary, I mean point is either immediately before or
immediately after a defun.

It's also important to point out that classes, namespaces, packages,
etc. could all be defuns, not just functions.  Therefore the nesting
as being described is not a pathological exercise, but a much more
common occurrence than one might initially consider.

As far as behavior, I think the way I've observed it working in
non-Tree-sitter modes was more intuitive than the current Tree-sitter
behavior (but I haven't exhaustively checked).  Additionally, I think
it should work as described in the documentation.   Much of what I'm
saying was already expressed in Bug#68664.  There, I tried to codify
how I believed the non-Tree-sitter modes were behaving and how
intuitively I thought it should work for Tree-sitter modes.

Regardless of what tactic is configured, `treesit-beginning-of-defun`
must go to the beginning of the enclosing defun when point is not at a
defun boundary.  I think this is fundamental or the assumptions made
when using `beginning-of-defun` in the general case no longer hold
(such as its use in `prog-fill-reindent-defun`).  I think the tactic
should only come into play when point is already at a defun boundary.

After that, I'm less concerned about its behavior, but think the
current `nested` behavior of visiting the previous sibling defun until
there are no more, then visiting the parent defun makes sense.  The
key difference is that you only visit the previous sibling if point is
at a defun boundary...otherwise visit the defun containing point.

Furthermore, consider the following `top-level` tactic example using
`c++-ts-mode`.  Here, we have a C++ namespace (which is considered a
defun for C++) as well as a "doSomething" function defun within the
namespace.  If we place point on the "printf" and press `M-q`,
triggering `prog-fill-reindent-defun`, we'll see that the entire
namespace has been re-indented (including "int i;").  I'd argue that
was not intuitively what I would have expected to happen.  Instead, I
would have expected only "doSomething" to have been re-indented.

```cpp
// -*- mode: c++-ts
#include <cstdio>

namespace Hello
{
int i;

  void doSomething(void)
  {
printf("doSomething\n");
  }
};

// Local Variables:
// treesit-defun-tactic: top-level
// End:
```

If the behavior of `treesit-beginning-of-defun` was to first move to
the beginning of the enclosing defun when point is not at a defun
boundary (as previously described), the expected behavior of only
reindenting "doSomething" would have occurred.  If
`treesit-beginning-of-defun` worked the way I propose, and you really
wanted to re-indent the entire namespace, it would have been as simple
as moving to the beginning of the enclosing defun and triggering
`prog-fill-reindent-defun` there (i.e., `C-M-a` `M-q`).

> Any suggestions for a good name for this tactic? Right now we have `nested` and `top-level`.

See above.  I don't think a new tactic is necessary, just a change in
how `treesit-beginning-of-defun` works when point is not at a defun
boundary, regardless of the configured tactic.