diff --git a/doc/mini-ai.txt b/doc/mini-ai.txt index 5fc60f8c..e5173499 100644 --- a/doc/mini-ai.txt +++ b/doc/mini-ai.txt @@ -798,6 +798,10 @@ Notes: Verify with `:=vim.treesitter.query.get('lang', 'textobjects')` and see if the target capture is recognized as one. - It uses buffer's |filetype| to determine query language. +- It first searches the language under cursor for matches. If no matches are + found, it fallbacks to searching parent languages (up to the buffer's root + language). If no matches are found again, it fallbacks to recursively + searching all children languages (from the language under cursor). - On large files it is slower than pattern-based textobjects. Still very fast though (one search should be magnitude of milliseconds or tens of milliseconds on really large file). diff --git a/doc/mini-surround.txt b/doc/mini-surround.txt index 931749ab..1daf6328 100644 --- a/doc/mini-surround.txt +++ b/doc/mini-surround.txt @@ -824,6 +824,10 @@ Notes: Verify with `:=vim.treesitter.query.get('lang', 'textobjects')` and see if the target capture is recognized as one. - It uses buffer's |filetype| to determine query language. +- It first searches the language under cursor for matches. If no matches are + found, it fallbacks to searching parent languages (up to the buffer's root + language). If no matches are found again, it fallbacks to recursively + searching all children languages (from the language under cursor). - On large files it is slower than pattern-based textobjects. Still very fast though (one search should be magnitude of milliseconds or tens of milliseconds on really large file). diff --git a/lua/mini/ai.lua b/lua/mini/ai.lua index 20b6106a..b3ee4343 100644 --- a/lua/mini/ai.lua +++ b/lua/mini/ai.lua @@ -993,6 +993,10 @@ end --- Verify with `:=vim.treesitter.query.get('lang', 'textobjects')` and see --- if the target capture is recognized as one. --- - It uses buffer's |filetype| to determine query language. +--- - It first searches the language under cursor for matches. If no matches are +--- found, it fallbacks to searching parent languages (up to the buffer's root +--- language). If no matches are found again, it fallbacks to recursively +--- searching all children languages (from the language under cursor). --- - On large files it is slower than pattern-based textobjects. Still very --- fast though (one search should be magnitude of milliseconds or tens of --- milliseconds on really large file). @@ -1608,22 +1612,29 @@ H.get_matched_ranges_builtin = function(captures) -- Get parser (LanguageTree) at cursor (important for injected languages) local pos = vim.api.nvim_win_get_cursor(0) local lang_tree = parser:language_for_range({ pos[1] - 1, pos[2], pos[1] - 1, pos[2] }) + local init_lang_tree = lang_tree local missing_query_langs = {} local res = {} -- Maybe go up parent trees to work with injected languages while vim.tbl_isempty(res) and lang_tree ~= nil do - local lang = lang_tree:lang() - -- Get query file depending on the local language - local query = vim.treesitter.query.get(lang, 'textobjects') + H.append_lang_ranges(res, missing_query_langs, buf_id, captures, lang_tree) - if query ~= nil then H.append_ranges(res, buf_id, query, captures, lang_tree) end - if query == nil then missing_query_langs[lang] = true end - - -- `LanguageTree:parent()` was added in Neovim<0.10 + -- `LanguageTree:parent()` was added in Neovim=0.10 -- TODO: Drop extra check after compatibility with Neovim=0.9 is dropped lang_tree = lang_tree.parent and lang_tree:parent() or nil end + -- Fallback to children trees for injected languages + 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 if vim.tbl_isempty(res) and not vim.tbl_isempty(missing_query_langs) then H.error_treesitter('query', vim.tbl_keys(missing_query_langs)) @@ -1632,6 +1643,14 @@ H.get_matched_ranges_builtin = function(captures) return res end +H.append_lang_ranges = function(res, missing_query_langs, buf_id, captures, lang_tree) + local lang = lang_tree:lang() + local query = vim.treesitter.query.get(lang, 'textobjects') + + if query ~= nil then H.append_ranges(res, buf_id, query, captures, lang_tree) end + if query == nil then missing_query_langs[lang] = true end +end + H.append_ranges = function(res, buf_id, query, captures, lang_tree) -- Compute ranges of matched captures local capture_is_requested = vim.tbl_map(function(c) return vim.tbl_contains(captures, '@' .. c) end, query.captures) diff --git a/lua/mini/surround.lua b/lua/mini/surround.lua index 67141948..e0f5a48c 100644 --- a/lua/mini/surround.lua +++ b/lua/mini/surround.lua @@ -1006,6 +1006,10 @@ MiniSurround.gen_spec = { input = {}, output = {} } --- Verify with `:=vim.treesitter.query.get('lang', 'textobjects')` and see --- if the target capture is recognized as one. --- - It uses buffer's |filetype| to determine query language. +--- - It first searches the language under cursor for matches. If no matches are +--- found, it fallbacks to searching parent languages (up to the buffer's root +--- language). If no matches are found again, it fallbacks to recursively +--- searching all children languages (from the language under cursor). --- - On large files it is slower than pattern-based textobjects. Still very --- fast though (one search should be magnitude of milliseconds or tens of --- milliseconds on really large file). @@ -1545,35 +1549,36 @@ H.get_matched_range_pairs_builtin = function(captures) -- Get parser (LanguageTree) at cursor (important for injected languages) local pos = vim.api.nvim_win_get_cursor(0) local lang_tree = parser:language_for_range({ pos[1] - 1, pos[2], pos[1] - 1, pos[2] }) + local init_lang_tree = lang_tree local missing_query_langs = {} -- Compute matched ranges for both outer and inner captures -- Maybe go up parent trees to work with injected languages - local outer_ranges, inner_ranges = {}, {} - while (vim.tbl_isempty(inner_ranges) or vim.tbl_isempty(outer_ranges)) and lang_tree ~= nil do - local lang = lang_tree:lang() - -- Get query file depending on the local language - local query = vim.treesitter.query.get(lang, 'textobjects') - - if query ~= nil then - for _, tree in ipairs(lang_tree:trees()) do - local root = tree:root() - vim.list_extend(outer_ranges, H.get_match_ranges_builtin(root, buf_id, query, captures.outer:sub(2))) - vim.list_extend(inner_ranges, H.get_match_ranges_builtin(root, buf_id, query, captures.inner:sub(2))) - end - end - if query == nil then missing_query_langs[lang] = true end + local outer, inner = {}, {} + while (vim.tbl_isempty(inner) or vim.tbl_isempty(outer)) and lang_tree ~= nil do + H.append_lang_ranges(outer, inner, missing_query_langs, buf_id, captures, lang_tree) -- `LanguageTree:parent()` was added in Neovim<0.10 -- TODO: Drop extra check after compatibility with Neovim=0.9 is dropped lang_tree = lang_tree.parent and lang_tree:parent() or nil end + -- Fallback to children trees for injected languages + 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 -- Match outer and inner ranges: for each outer range pick the biggest inner -- range that lies within outer local res = {} - for i, outer in ipairs(outer_ranges) do - res[i] = { outer = outer, inner = H.get_biggest_nested_range(inner_ranges, outer) } + for i, o in ipairs(outer) do + res[i] = { outer = o, inner = H.get_biggest_nested_range(inner, o) } end if vim.tbl_isempty(res) and not vim.tbl_isempty(missing_query_langs) then @@ -1583,6 +1588,22 @@ H.get_matched_range_pairs_builtin = function(captures) return res end +H.append_lang_ranges = function(outer, inner, missing_query_langs, buf_id, captures, lang_tree) + local lang = lang_tree:lang() + local query = vim.treesitter.query.get(lang, 'textobjects') + + if query ~= nil then H.append_ranges(outer, inner, buf_id, query, captures, lang_tree) end + if query == nil then missing_query_langs[lang] = true end +end + +H.append_ranges = function(outer, inner, buf_id, query, captures, lang_tree) + for _, tree in ipairs(lang_tree:trees()) do + local root = tree:root() + vim.list_extend(outer, H.get_match_ranges_builtin(root, buf_id, query, captures.outer:sub(2))) + vim.list_extend(inner, H.get_match_ranges_builtin(root, buf_id, query, captures.inner:sub(2))) + end +end + H.get_match_ranges_builtin = function(root, buf_id, query, capture) local res = {} -- TODO: Remove `opts.all`after compatibility with Neovim=0.10 is dropped diff --git a/tests/test_ai.lua b/tests/test_ai.lua index 4187d32e..57b24ddf 100644 --- a/tests/test_ai.lua +++ b/tests/test_ai.lua @@ -857,6 +857,16 @@ T['gen_spec']['treesitter()']['works with parent of injected language'] = functi validate_find(lines, { 3, 0 }, { 'a', 'F' }, { { 1, 13 }, { 5, 3 } }) end +T['gen_spec']['treesitter()']['works with injected child language'] = function() + local lines = { + 'vim.cmd([[', + 'set cursorline', + 'lua local a = function() return true end', + ']])', + } + validate_find(lines, { 1, 0 }, { 'a', 'F' }, { { 3, 15 }, { 3, 40 } }) +end + T['gen_spec']['treesitter()']['works with row-exclusive, col-0 end range'] = function() child.lua([[MiniAi.config.custom_textobjects = { c = MiniAi.gen_spec.treesitter({ a = '@chunk.outer', i = '@chunk.outer' }), diff --git a/tests/test_surround.lua b/tests/test_surround.lua index 3ba41898..da15487b 100644 --- a/tests/test_surround.lua +++ b/tests/test_surround.lua @@ -412,6 +412,16 @@ T['gen_spec']['input']['treesitter()']['works with parent of injected language'] validate_no_find(lines, { 1, 0 }, type_keys, 'sf', 'F') end +T['gen_spec']['input']['treesitter()']['works with injected child language'] = function() + local lines = { + 'vim.cmd([[', + 'set cursorline', + 'lua local a = function() return true end', + ']])', + } + validate_find(lines, { 1, 0 }, { { 3, 14 } }, type_keys, 'sfn', 'F') +end + T['gen_spec']['input']['treesitter()']['respects `opts.use_nvim_treesitter`'] = function() child.lua([[MiniSurround.config.custom_surroundings = { F = { input = MiniSurround.gen_spec.input.treesitter({ outer = '@function.outer', inner = '@function.inner' }) },