Skip to content

feat(ALL): check children treesitter languages after parents#2407

Open
TheLeoP wants to merge 2 commits into
nvim-mini:mainfrom
TheLeoP:poc
Open

feat(ALL): check children treesitter languages after parents#2407
TheLeoP wants to merge 2 commits into
nvim-mini:mainfrom
TheLeoP:poc

Conversation

@TheLeoP
Copy link
Copy Markdown
Member

@TheLeoP TheLeoP commented May 6, 2026

This PR is only a proof of concept of what a fix for #2397 would look like.

Thoughts:

  • The easiest way to test this could be to create queries for injecting Lua into some Lua strings
  • mini.surround doesn't seem to need this change since the cursor always needs to already be on top of the region with a treesitter injection. So, it will be parsed even before this PR when going up the tree.

@TheLeoP TheLeoP marked this pull request as draft May 6, 2026 03:21
@echasnovski
Copy link
Copy Markdown
Member

Thanks for the PR!

  • The easiest way to test this could be to create queries for injecting Lua into some Lua strings

There are already tests for injected languages (this one for 'mini.ai'). They use built-in Lua -> Vimscript injections.

I think tests for this could use even Lua -> Vimscript -> Lua injections. Like here:

vim.cmd([[
  set cursorline
  lua local a = 1
]])
  • mini.surround doesn't seem to need this change since the cursor always needs to already be on top of the region with a treesitter injection. So, it will be parsed even before this PR when going up the tree.

There is less overall complexity if tree-sitter related (and even some other) code in 'mini.ai' and 'mini.surround' is kept as aligned as possible.


As per this PR, please reduce code duplication. Both parents and children are meant to be processed exactly the same, right? In both 'mini.ai' and 'mini.surround' this processing is rather complicated, so even with two times duplication it adds a lot.

@TheLeoP TheLeoP marked this pull request as ready for review May 20, 2026 14:48
@TheLeoP
Copy link
Copy Markdown
Member Author

TheLeoP commented May 20, 2026

There are already tests for injected languages

I forgot about that test and I wrote it myself :p. Thanks for the pointer!

Using Lua -> Vimscript -> Lua injections was indeed easier to avoid adding new queries.

code duplication and mini.ai/mini.surround alignment on treesitter implementation

I think I improved the alignment/code duplication between mini.ai and mini.surround, but I don't usually write small functions. So, let me know if you have any more feedback.

Copy link
Copy Markdown
Member

@echasnovski echasnovski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This indeed looks like a well structured change and improvement!

Apart from variable naming nit picks, could you please also:

  • Add a concise note in each function's help about how it searches for the match.
  • Add a 'mini.surround' test.
  • Split into two commits for for 'mini.ai' and 'mini.surround'. Each with a concise description in Details: list about why this is necessary. With also Resolve #2397 line afterwards.

Comment thread lua/mini/ai.lua Outdated
Comment thread lua/mini/surround.lua Outdated
Comment thread lua/mini/surround.lua Outdated
@TheLeoP
Copy link
Copy Markdown
Member Author

TheLeoP commented May 22, 2026

Add a concise note in each function's help about how it searches for the match.

Are you referring to H.get_matched_ranges_builtin? Or to H.get_nodes_range_builtin and H.get_match_ranges_builtin?

Add a concise note in each function's help about how it searches for the match.

This is about adding a comment to the private helper functions, right? Or is it about modifying the comments used to generate the documentation for the user-facing MiniAi.gen_spec.treesitter() and MiniSurround.gen_spec.input.treesitter()?

Add a 'mini.surround' test.

I don't think a test for this change would be possible inside of mini.surround. The cursor needs to always be inside of a surround, which means that the surround would be either on the same LanguageTree or in a parent one. I can't think of a scenario where the surrounding would be on a child language tree.

Comment thread lua/mini/surround.lua Outdated
Comment thread tests/test_ai.lua Outdated
@echasnovski
Copy link
Copy Markdown
Member

echasnovski commented May 22, 2026

This is about adding a comment to the private helper functions, right? Or is it about modifying the comments used to generate the documentation for the user-facing MiniAi.gen_spec.treesitter() and MiniSurround.gen_spec.input.treesitter()?

Sorry, should have been more clear. Mentioning function's help in my mind means the text that is shown when doing :h <function-name>. For commenting local functions it is usually "function's comment" or "function's annotation".

And the explanation is very much needed because I just now (possibly) misremembered how this is supposed to work in the 'mini.ai' test. Is it intended and/or reasonable that the search is only done for "one level" children? In particular with this Lua code with cursor at the top, executing vanF doesn't work because the target is two levels deep (Lua->Vimscript->Lua):

vim.cmd([[
set cursorline
lua local a = function() return true end
]])

I think I would have expected all children to be used here, no matter however deep. Do you think it is reasonable?

Add a 'mini.surround' test.

I don't think a test for this change would be possible inside of mini.surround. The cursor needs to always be inside of a surround, which means that the surround would be either on the same LanguageTree or in a parent one. I can't think of a scenario where the surrounding would be on a child language tree.

The 'mini.ai' and 'mini.surround' are very aligned in how they find their targets (textobjects and surroundings). In this particular case, using next variant of surrounding action (like sfn to "find next surrounding") will work. Like this:

T['gen_spec']['input']['treesitter()']['works with direct injected child language'] = function()
  local lines = {
    'vim.cmd([[',
    'set cursorline',
    'lua local a = function () end',
    ']])',
  }
  validate_find(lines, { 2, 0 }, { { 3, 14 } }, type_keys, 'sfn', 'F')
end

@TheLeoP
Copy link
Copy Markdown
Member Author

TheLeoP commented May 28, 2026

Is it intended and/or reasonable that the search is only done for "one level" children?

It was intended, I misunderstood your expectation and though you expected only first level children to be checked. Sorry.

I think I would have expected all children to be used here, no matter however deep. Do you think it is reasonable?

I would expect the same thing. The only remaining question would be, do you think it would be better to search all the children LanguageTree depth first or breadth first? The results would be different on some specific edge-cases, since we are stopping the search as soon as any value is found. I think that depth first could be a more intuitive behavior for end users.

TheLeoP added 2 commits May 27, 2026 21:10
Details:
- Users may expect next/previous textobjects in injected languages
  located before/after the cursor to work, even if they are not part of
  the LanguageTree under cursor or its ancestors. This is specially
  important for languages that rely heavily on injections like markdown
  (injecting markdown_inline).

- So, we fallback to searching in direct injected children languages if
  no textobject is found for current LanguageTree or its ancestors.

Resolve nvim-mini#2397
Details:
- Users may expect next/previous textobjects in injected languages
  located before/after the cursor to work, even if they are not part of
  the LanguageTree under cursor or its ancestors. This is specially
  important for languages that rely heavily on injections like markdown
  (injecting markdown_inline).

- So, we fallback to searching in direct injected children languages if
  no textobject is found for current LanguageTree or its ancestors.
Comment thread lua/mini/ai.lua
Copy link
Copy Markdown
Member

@echasnovski echasnovski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The recursive approach is probably cleaner than iterative one.

Also, please, add the concise note about how traversal is done in respective help annotations.

Comment thread lua/mini/ai.lua
Comment on lines +1624 to +1635
local check_children
check_children = function(l_tree)
if not vim.tbl_isempty(res) then return end

for _, child in pairs(l_tree:children()) do
H.append_lang_ranges(res, missing_query_langs, buf_id, captures, child)
if not vim.tbl_isempty(res) then return end

check_children(child)
end
end
check_children(init_lang_tree)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. I think this code will be rare, so guarding it whole behind the if vim.tbl_isempty() is reasonable (although adds one extra indent).
  2. The problem with early return inside a cycle is that there is no guarantee about the order of the traversal. So this will have to be "fall back to all child trees".

So something like this:

Suggested change
local check_children
check_children = function(l_tree)
if not vim.tbl_isempty(res) then return end
for _, child in pairs(l_tree:children()) do
H.append_lang_ranges(res, missing_query_langs, buf_id, captures, child)
if not vim.tbl_isempty(res) then return end
check_children(child)
end
end
check_children(init_lang_tree)
if vim.tbl_isempty(res) then
local check_children
check_children = function(l_tree)
for _, child in pairs(l_tree:children()) do
H.append_lang_ranges(res, missing_query_langs, buf_id, captures, child)
check_children(child)
end
end
check_children(init_lang_tree)
end

Comment thread lua/mini/ai.lua
Comment thread lua/mini/surround.lua
Comment on lines +1562 to +1573
local check_children
check_children = function(l_tree)
if not vim.tbl_isempty(inner) and not vim.tbl_isempty(outer) then return end

for _, child in pairs(l_tree:children()) do
H.append_lang_ranges(outer, inner, missing_query_langs, buf_id, captures, child)
if not vim.tbl_isempty(inner) and not vim.tbl_isempty(outer) then return end

check_children(child)
end
end
check_children(init_lang_tree)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to 'mini.ai' case:

Suggested change
local check_children
check_children = function(l_tree)
if not vim.tbl_isempty(inner) and not vim.tbl_isempty(outer) then return end
for _, child in pairs(l_tree:children()) do
H.append_lang_ranges(outer, inner, missing_query_langs, buf_id, captures, child)
if not vim.tbl_isempty(inner) and not vim.tbl_isempty(outer) then return end
check_children(child)
end
end
check_children(init_lang_tree)
if vim.tbl_isempty(inner) or vim.tbl_isempty(outer) then
local check_children
check_children = function(l_tree)
for _, child in pairs(l_tree:children()) do
H.append_lang_ranges(outer, inner, missing_query_langs, buf_id, captures, child)
check_children(child)
end
end
check_children(init_lang_tree)
end

@echasnovski
Copy link
Copy Markdown
Member

It was intended, I misunderstood your expectation and though you expected only first level children to be checked. Sorry.

No worries. I should have been clearer.

I would expect the same thing. The only remaining question would be, do you think it would be better to search all the children LanguageTree depth first or breadth first? The results would be different on some specific edge-cases, since we are stopping the search as soon as any value is found. I think that depth first could be a more intuitive behavior for end users.

The problem with early return inside a cycle is that there is no guarantee about the order of the traversal. So this will have to be "fall back to all child trees".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants