diff --git a/lua/mini/ai.lua b/lua/mini/ai.lua index 99b1feff..d155d4e9 100644 --- a/lua/mini/ai.lua +++ b/lua/mini/ai.lua @@ -1552,6 +1552,21 @@ H.get_matched_ranges_plugin = function(captures) return res end +function H.append_ranges(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) + + for _, tree in ipairs(lang_tree:trees()) do + -- TODO: Remove `opts.all`after compatibility with Neovim=0.10 is dropped + for _, match, metadata in query:iter_matches(tree:root(), buf_id, nil, nil, { all = true }) do + for capture_id, nodes in pairs(match) do + local mt = metadata[capture_id] + if capture_is_requested[capture_id] then table.insert(res, H.get_nodes_range_builtin(nodes, buf_id, mt)) end + end + end + end +end + H.get_matched_ranges_builtin = function(captures) -- Get buffer's parser (LanguageTree) local buf_id = vim.api.nvim_get_current_buf() @@ -1562,24 +1577,24 @@ 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 lang = lang_tree:lang() - - -- Get query file depending on the local language - local query = vim.treesitter.query.get(lang, 'textobjects') - if query == nil then H.error_treesitter('query', lang) end - - -- Compute ranges of matched captures - local capture_is_requested = vim.tbl_map(function(c) return vim.tbl_contains(captures, '@' .. c) end, query.captures) + local missing_query_langs = {} local res = {} - for _, tree in ipairs(lang_tree:trees()) do - -- TODO: Remove `opts.all`after compatibility with Neovim=0.10 is dropped - for _, match, metadata in query:iter_matches(tree:root(), buf_id, nil, nil, { all = true }) do - for capture_id, nodes in pairs(match) do - local mt = metadata[capture_id] - if capture_is_requested[capture_id] then table.insert(res, H.get_nodes_range_builtin(nodes, buf_id, mt)) end - end - end + -- Recursively query parent LanguageTree as fallback (important for 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') + + 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 + -- TODO: Change to `lang_tree:parent()` after compatibility with Neovim=0.9 is dropped + lang_tree = lang_tree.parent and lang_tree:parent() or nil + 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)) end return res @@ -1602,13 +1617,17 @@ H.get_nodes_range_builtin = function(nodes, buf_id, metadata) return { left[1], left[2], left[3], right[4], right[5], right[6] } end -H.error_treesitter = function(failed_get, lang) +H.error_treesitter = function(failed_get, langs) local buf_id, ft = vim.api.nvim_get_current_buf(), vim.bo.filetype - if lang == nil then - local has_lang, ft_lang = pcall(vim.treesitter.language.get_lang, ft) - lang = has_lang and ft_lang or ft + if langs == nil then + local ok, ft_lang = pcall(vim.treesitter.language.get_lang, ft) + -- `vim.treesitter.language.get_lang()` defaults to `ft` only on Neovim>0.11 + -- TODO: Remove `and ft_lang ~= nil` after compatibility with Neovim=0.10 is dropped + langs = (ok and ft_lang ~= nil) and { ft_lang } or { ft } end - local msg = string.format('Can not get %s for buffer %d and language "%s".', failed_get, buf_id, lang) + local langs_str = table.concat(vim.tbl_map(function(lang) return string.format('"%s"', lang) end, langs), ', ') + local langs_noun = #langs == 1 and 'language' or 'languages' + local msg = string.format('Can not get %s for buffer %d and %s %s.', failed_get, buf_id, langs_noun, langs_str) H.error(msg) end diff --git a/lua/mini/surround.lua b/lua/mini/surround.lua index 4a00c2b9..db0b448f 100644 --- a/lua/mini/surround.lua +++ b/lua/mini/surround.lua @@ -1536,18 +1536,27 @@ 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 lang = lang_tree:lang() - - -- Get query file depending on the local language - local query = vim.treesitter.query.get(lang, 'textobjects') - if query == nil then H.error_treesitter('query') end + local missing_query_langs = {} -- Compute matched ranges for both outer and inner captures local outer_ranges, inner_ranges = {}, {} - 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))) + 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 + + -- `LanguageTree:parent()` was added in Neovim<0.10 + -- TODO: Change to `lang_tree:parent()` after compatibility with Neovim=0.9 is dropped + lang_tree = lang_tree.parent and lang_tree:parent() or nil end -- Match outer and inner ranges: for each outer range pick the biggest inner @@ -1556,6 +1565,11 @@ H.get_matched_range_pairs_builtin = function(captures) for i, outer in ipairs(outer_ranges) do res[i] = { outer = outer, inner = H.get_biggest_nested_range(inner_ranges, outer) } 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)) + end + return res end @@ -1601,11 +1615,17 @@ H.get_biggest_nested_range = function(ranges, parent) return best_range end -H.error_treesitter = function(failed_get) +H.error_treesitter = function(failed_get, langs) local buf_id, ft = vim.api.nvim_get_current_buf(), vim.bo.filetype - local has_lang, lang = pcall(vim.treesitter.language.get_lang, ft) - lang = has_lang and lang or ft - local msg = string.format('Can not get %s for buffer %d and language "%s".', failed_get, buf_id, lang) + if langs == nil then + local ok, ft_lang = pcall(vim.treesitter.language.get_lang, ft) + -- `vim.treesitter.language.get_lang()` defaults to `ft` only on Neovim>0.11 + -- TODO: Remove `and ft_lang ~= nil` after compatibility with Neovim=0.10 is dropped + langs = (ok and ft_lang ~= nil) and { ft_lang } or { ft } + end + local langs_str = table.concat(vim.tbl_map(function(lang) return string.format('"%s"', lang) end, langs), ', ') + local langs_noun = #langs == 1 and 'language' or 'languages' + local msg = string.format('Can not get %s for buffer %d and %s %s.', failed_get, buf_id, langs_noun, langs_str) H.error(msg) end diff --git a/tests/test_ai.lua b/tests/test_ai.lua index f97edbbf..9bb5ee1a 100644 --- a/tests/test_ai.lua +++ b/tests/test_ai.lua @@ -844,6 +844,19 @@ T['gen_spec']['treesitter()']['works with quantified captures'] = function() validate_find(lines, { 3, 0 }, { 'a', 'P', { n_times = 3 } }, { { 3, 19 }, { 3, 23 } }) end +T['gen_spec']['treesitter()']['works with parent of injected language'] = function() + if child.fn.has('nvim-0.10') == 0 then MiniTest.skip('`LanguageTree:parent()` requires Neovim>=0.10') end + + local lines = { + 'local foo = function()', + ' vim.cmd([[', + 'set cursorline', + ']])', + 'end', + } + validate_find(lines, { 3, 0 }, { 'a', 'F' }, { { 1, 13 }, { 5, 3 } }) +end + T['gen_spec']['treesitter()']['respects plugin options'] = function() local lines = get_lines() @@ -906,6 +919,26 @@ T['gen_spec']['treesitter()']['validates builtin treesitter presence'] = functio function() child.lua('MiniAi.find_textobject("a", "F")') end, '%(mini%.ai%) Can not get parser for buffer 3 and language "my_aaa"%.' ) + + if child.fn.has('nvim-0.10') == 0 then return end + -- - Should show each language + child.cmd('enew') + child.bo.filetype = 'help' + set_lines({ '>vim', ' set cursorline', '<' }) + set_cursor(2, 0) + expect.error( + function() child.lua('MiniAi.find_textobject("a", "F")') end, + '%(mini%.ai%) Can not get query for buffer 3 and languages "vimd?o?c?", "vimd?o?c?"%.' + ) + + -- - Should show each language once + child.cmd('edit tmp.vim') + set_lines({ 'set indentexpr=VimIndent()' }) + set_cursor(1, 0) + expect.error( + function() child.lua('MiniAi.find_textobject("a", "F")') end, + '%(mini%.ai%) Can not get query for buffer 4 and language "vim"%.' + ) end T['gen_spec']['user_prompt()'] = new_set() diff --git a/tests/test_surround.lua b/tests/test_surround.lua index e80cbf74..52377f35 100644 --- a/tests/test_surround.lua +++ b/tests/test_surround.lua @@ -327,6 +327,21 @@ T['gen_spec']['input']['treesitter()']['works with no inner captures'] = functio validate_find(lines, { 10, 2 }, { { 10, 12 }, { 10, 2 } }, type_keys, 'sf', 'o') end +T['gen_spec']['input']['treesitter()']['works with parent of injected language'] = function() + if child.fn.has('nvim-0.10') == 0 then MiniTest.skip('`LanguageTree:parent()` requires Neovim>=0.10') end + + local lines = { + 'local foo = function()', + ' vim.cmd([[', + 'set cursorline', + ']])', + 'end', + } + + validate_find(lines, { 3, 0 }, { { 4, 2 }, { 5, 2 }, { 1, 12 }, { 2, 1 } }, type_keys, 'sf', 'F') + validate_no_find(lines, { 1, 0 }, type_keys, 'sf', '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' }) },