meain / evil-textobj-tree-sitter

Tree-sitter powered textobjects for evil mode in Emacs
Apache License 2.0
197 stars 14 forks source link

Emacs treesit experiments #109

Open dvzubarev opened 10 months ago

dvzubarev commented 10 months ago

Hi, I wanted to share my experiments with the Emacs builtin treesit library. Recently, there were added some functions for creating arbitrary thing at points from the parse tree nodes. I had an idea to implement text objects based on these things. The basic idea is to search current thing at point. Then extend or shrink its bounds according to current modifier (inner, outer, etc.).

There are pros and cons for this approach in comparison with the query based approach. On the good side, It is more efficient approach, since it does not require to query the whole buffer with bunch of queries. Its working by local traversing of the tree nodes, that are near the point. So performance of this approach should not be affected by the size of the buffer. Also, it allows more fine-grained control of text object ranges. For example, for parameter outer, its possible to include surrounding spaces. There are also possibilities to make fancier text objects.

The main downside is the lack of language support. Since we cannot use community queries, we have to create settings for each language from the scratch.

I ended up crafting a package out of this experiments for my personal use - https://github.com/dvzubarev/evil-ts-obj. Please feel free to check it out, it would be great to hear some feedback.

meain commented 10 months ago

This looks interesting. Relying on the named fields seems like a good idea(not sure about the support for every language though). FYI, there is a package which does something vaguely similar foxfriday/evil-ts.

It is more efficient approach, since it does not require to query the whole buffer with bunch of queries.

Hmm, that sound useful. That said, I've not really had run into any issues with perf even on relatively large buffers.

Also, it allows more fine-grained control of text object ranges. For example, for parameter outer, its possible to include surrounding spaces.

The elisp-tree-sitter variant lets you do similar things. The reason why we can't support this is due to missing functionality to get matches.

The main downside is the lack of language support.

Yeah, my plan from the start was to just copy the "homework" done by neovim folks. It has not always been that smooth because of neovim using a lot of custom operators.

dvzubarev commented 10 months ago

The elisp-tree-sitter variant lets you do similar things. The reason why we can't support this is due to missing functionality to get matches.

No it's not. There are a lot of edge cases in situations when cursor is not on a node, but somewhere between. Like on spaces or on a separator etc. See https://github.com/nvim-treesitter/nvim-treesitter-textobjects/issues/69 Another problem is extending text object to the next sibling and including surrounding spaces (for parameter text objects). It seems they eventually fixed this problem by writing custom lua code (see https://github.com/nvim-treesitter/nvim-treesitter-textobjects/pull/235), not via using custom predicates.

meain commented 10 months ago

I see what you mean. I misunderstood your initial comment. We don't really do that as of now.

For what its worth, the current behavior here is a bit different compared to what is explained in https://github.com/nvim-treesitter/nvim-treesitter-textobjects/issues/69.

For the first example:

print( "a" , "b" , "c" )

... if you were to do parameter.inner, it would select the "b" as we select the very next one if we are not on one. I copied the behavior as I got used to this for selecting brackets in vim. parameter.outer will also technically work in the expected way. This however has downsides in that if there are no immediate next "parameter" we end up selecting the next one anywhere in the buffer which also seems to be the behavior can be kinda jarring.

As for the second case:

def foo(bar):
    print(bar)
  ^

... similar to the first case doing a function.inner will do the right thing here as well just because we select the next entry.

dvzubarev commented 10 months ago

if you were to do parameter.inner, it would select the "b" as we select the very next one if we are not on one.

Yes, but this approach does not work if there are nested text objects. For example print( call(a,| b, c) ) will select call(a,| b, c) as parameter.inner. In most cases, it is not what I want to do.

meain commented 10 months ago

Ahh, I see. That would definitely need custom handling.