Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 40 additions & 21 deletions lua/mini/ai.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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
Expand All @@ -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

Expand Down
46 changes: 33 additions & 13 deletions lua/mini/surround.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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

Expand Down
33 changes: 33 additions & 0 deletions tests/test_ai.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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()
Expand Down
15 changes: 15 additions & 0 deletions tests/test_surround.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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' }) },
Expand Down
Loading