Files
neovim/test/functional/treesitter/parser_spec.lua
Jaehwang Jung 7fa292c52d fix(treesitter): outdated highlight due to tree with outdated region
Problem:
A region managed by an injected parser may shrink after re-running the
injection query. If the updated region goes out of the range to be
parsed, then the corresponding tree will remain outdated, possibly
retaining the nodes that shouldn't exist anymore. This results in
outdated highlights.

Solution:
Re-parse an invalid tree if its region intersects the range to be
parsed.
2023-12-24 09:47:59 +01:00

1228 lines
38 KiB
Lua

local helpers = require('test.functional.helpers')(after_each)
local clear = helpers.clear
local dedent = helpers.dedent
local eq = helpers.eq
local insert = helpers.insert
local exec_lua = helpers.exec_lua
local pcall_err = helpers.pcall_err
local feed = helpers.feed
local is_os = helpers.is_os
describe('treesitter parser API', function()
before_each(function()
clear()
exec_lua[[
vim.g.__ts_debug = 1
]]
end)
it('parses buffer', function()
insert([[
int main() {
int x = 3;
}]])
exec_lua([[
parser = vim.treesitter.get_parser(0, "c")
tree = parser:parse()[1]
root = tree:root()
lang = vim.treesitter.language.inspect('c')
]])
eq("<tree>", exec_lua("return tostring(tree)"))
eq("<node translation_unit>", exec_lua("return tostring(root)"))
eq({0,0,3,0}, exec_lua("return {root:range()}"))
eq(1, exec_lua("return root:child_count()"))
exec_lua("child = root:child(0)")
eq("<node function_definition>", exec_lua("return tostring(child)"))
eq({0,0,2,1}, exec_lua("return {child:range()}"))
eq("function_definition", exec_lua("return child:type()"))
eq(true, exec_lua("return child:named()"))
eq("number", type(exec_lua("return child:symbol()")))
eq({'function_definition', true}, exec_lua("return lang.symbols[child:symbol()]"))
exec_lua("anon = root:descendant_for_range(0,8,0,9)")
eq("(", exec_lua("return anon:type()"))
eq(false, exec_lua("return anon:named()"))
eq("number", type(exec_lua("return anon:symbol()")))
eq({'(', false}, exec_lua("return lang.symbols[anon:symbol()]"))
exec_lua("descendant = root:descendant_for_range(1,2,1,12)")
eq("<node declaration>", exec_lua("return tostring(descendant)"))
eq({1,2,1,12}, exec_lua("return {descendant:range()}"))
eq("(declaration type: (primitive_type) declarator: (init_declarator declarator: (identifier) value: (number_literal)))", exec_lua("return descendant:sexpr()"))
feed("2G7|ay")
exec_lua([[
tree2 = parser:parse()[1]
root2 = tree2:root()
descendant2 = root2:descendant_for_range(1,2,1,13)
]])
eq(false, exec_lua("return tree2 == tree1"))
eq(false, exec_lua("return root2 == root"))
eq("<node declaration>", exec_lua("return tostring(descendant2)"))
eq({1,2,1,13}, exec_lua("return {descendant2:range()}"))
eq(true, exec_lua("return child == child"))
-- separate lua object, but represents same node
eq(true, exec_lua("return child == root:child(0)"))
eq(false, exec_lua("return child == descendant2"))
eq(false, exec_lua("return child == nil"))
eq(false, exec_lua("return child == tree"))
eq("string", exec_lua("return type(child:id())"))
eq(true, exec_lua("return child:id() == child:id()"))
-- separate lua object, but represents same node
eq(true, exec_lua("return child:id() == root:child(0):id()"))
eq(false, exec_lua("return child:id() == descendant2:id()"))
eq(false, exec_lua("return child:id() == nil"))
eq(false, exec_lua("return child:id() == tree"))
-- unchanged buffer: return the same tree
eq(true, exec_lua("return parser:parse()[1] == tree2"))
end)
local test_text = [[
void ui_refresh(void)
{
int width = INT_MAX, height = INT_MAX;
bool ext_widgets[kUIExtCount];
for (UIExtension i = 0; (int)i < kUIExtCount; i++) {
ext_widgets[i] = true;
}
bool inclusive = ui_override();
for (size_t i = 0; i < ui_count; i++) {
UI *ui = uis[i];
width = MIN(ui->width, width);
height = MIN(ui->height, height);
foo = BAR(ui->bazaar, bazaar);
for (UIExtension j = 0; (int)j < kUIExtCount; j++) {
ext_widgets[j] &= (ui->ui_ext[j] || inclusive);
}
}
}]]
it('allows to iterate over nodes children', function()
insert(test_text);
local res = exec_lua([[
parser = vim.treesitter.get_parser(0, "c")
func_node = parser:parse()[1]:root():child(0)
res = {}
for node, field in func_node:iter_children() do
table.insert(res, {node:type(), field})
end
return res
]])
eq({
{"primitive_type", "type"},
{"function_declarator", "declarator"},
{"compound_statement", "body"}
}, res)
end)
it('does not get parser for empty filetype', function()
insert(test_text);
eq('.../treesitter.lua:0: There is no parser available for buffer 1 and one'
.. ' could not be created because lang could not be determined. Either'
.. ' pass lang or set the buffer filetype',
pcall_err(exec_lua, 'vim.treesitter.get_parser(0)'))
-- Must provide language for buffers with an empty filetype
exec_lua("vim.treesitter.get_parser(0, 'c')")
end)
it('allows to get a child by field', function()
insert(test_text);
local res = exec_lua([[
parser = vim.treesitter.get_parser(0, "c")
func_node = parser:parse()[1]:root():child(0)
local res = {}
for _, node in ipairs(func_node:field("type")) do
table.insert(res, {node:type(), node:range()})
end
return res
]])
eq({{ "primitive_type", 0, 0, 0, 4 }}, res)
local res_fail = exec_lua([[
parser = vim.treesitter.get_parser(0, "c")
return #func_node:field("foo") == 0
]])
assert(res_fail)
end)
local query = [[
((call_expression function: (identifier) @minfunc (argument_list (identifier) @min_id)) (eq? @minfunc "MIN"))
"for" @keyword
(primitive_type) @type
(field_expression argument: (identifier) @fieldarg)
]]
it("supports runtime queries", function()
local ret = exec_lua [[
return vim.treesitter.query.get("c", "highlights").captures[1]
]]
eq('variable', ret)
end)
it("supports caching queries", function()
local long_query = query:rep(100)
local function q(n)
return exec_lua ([[
local query, n = ...
local before = vim.uv.hrtime()
for i=1,n,1 do
cquery = vim.treesitter.query.parse("c", ...)
end
local after = vim.uv.hrtime()
return after - before
]], long_query, n)
end
local firstrun = q(1)
local manyruns = q(100)
-- First run should be at least 200x slower than an 100 subsequent runs.
local factor = is_os('win') and 100 or 200
assert(factor * manyruns < firstrun, ('firstrun: %f ms, manyruns: %f ms'):format(firstrun / 1e6, manyruns / 1e6))
end)
it('support query and iter by capture', function()
insert(test_text)
local res = exec_lua([[
cquery = vim.treesitter.query.parse("c", ...)
parser = vim.treesitter.get_parser(0, "c")
tree = parser:parse()[1]
res = {}
for cid, node in cquery:iter_captures(tree:root(), 0, 7, 14) do
-- can't transmit node over RPC. just check the name and range
table.insert(res, {cquery.captures[cid], node:type(), node:range()})
end
return res
]], query)
eq({
{ "type", "primitive_type", 8, 2, 8, 6 },
{ "keyword", "for", 9, 2, 9, 5 },
{ "type", "primitive_type", 9, 7, 9, 13 },
{ "minfunc", "identifier", 11, 12, 11, 15 },
{ "fieldarg", "identifier", 11, 16, 11, 18 },
{ "min_id", "identifier", 11, 27, 11, 32 },
{ "minfunc", "identifier", 12, 13, 12, 16 },
{ "fieldarg", "identifier", 12, 17, 12, 19 },
{ "min_id", "identifier", 12, 29, 12, 35 },
{ "fieldarg", "identifier", 13, 14, 13, 16 }
}, res)
end)
it('support query and iter by match', function()
insert(test_text)
local res = exec_lua([[
cquery = vim.treesitter.query.parse("c", ...)
parser = vim.treesitter.get_parser(0, "c")
tree = parser:parse()[1]
res = {}
for pattern, match in cquery:iter_matches(tree:root(), 0, 7, 14) do
-- can't transmit node over RPC. just check the name and range
local mrepr = {}
for cid,node in pairs(match) do
table.insert(mrepr, {cquery.captures[cid], node:type(), node:range()})
end
table.insert(res, {pattern, mrepr})
end
return res
]], query)
eq({
{ 3, { { "type", "primitive_type", 8, 2, 8, 6 } } },
{ 2, { { "keyword", "for", 9, 2, 9, 5 } } },
{ 3, { { "type", "primitive_type", 9, 7, 9, 13 } } },
{ 4, { { "fieldarg", "identifier", 11, 16, 11, 18 } } },
{ 1, { { "minfunc", "identifier", 11, 12, 11, 15 }, { "min_id", "identifier", 11, 27, 11, 32 } } },
{ 4, { { "fieldarg", "identifier", 12, 17, 12, 19 } } },
{ 1, { { "minfunc", "identifier", 12, 13, 12, 16 }, { "min_id", "identifier", 12, 29, 12, 35 } } },
{ 4, { { "fieldarg", "identifier", 13, 14, 13, 16 } } }
}, res)
end)
it('supports getting text of multiline node', function()
insert(test_text)
local res = exec_lua([[
local parser = vim.treesitter.get_parser(0, "c")
local tree = parser:parse()[1]
return vim.treesitter.get_node_text(tree:root(), 0)
]])
eq(test_text, res)
local res2 = exec_lua([[
local parser = vim.treesitter.get_parser(0, "c")
local root = parser:parse()[1]:root()
return vim.treesitter.get_node_text(root:child(0):child(0), 0)
]])
eq('void', res2)
end)
it('support getting text where start of node is one past EOF', function()
local text = [[
def run
a = <<~E
end]]
insert(text)
eq('', exec_lua[[
local fake_node = {}
function fake_node:start()
return 3, 0, 23
end
function fake_node:end_()
return 3, 0, 23
end
function fake_node:range(bytes)
if bytes then
return 3, 0, 23, 3, 0, 23
end
return 3, 0, 3, 0
end
return vim.treesitter.get_node_text(fake_node, 0)
]])
end)
it('support getting empty text if node range is zero width', function()
local text = [[
```lua
{}
```]]
insert(text)
local result = exec_lua([[
local fake_node = {}
function fake_node:start()
return 1, 0, 7
end
function fake_node:end_()
return 1, 0, 7
end
function fake_node:range()
return 1, 0, 1, 0
end
return vim.treesitter.get_node_text(fake_node, 0) == ''
]])
eq(true, result)
end)
it('can match special regex characters like \\ * + ( with `vim-match?`', function()
insert('char* astring = "\\n"; (1 + 1) * 2 != 2;')
local res = exec_lua([[
cquery = vim.treesitter.query.parse("c", '([_] @plus (#vim-match? @plus "^\\\\+$"))'..
'([_] @times (#vim-match? @times "^\\\\*$"))'..
'([_] @paren (#vim-match? @paren "^\\\\($"))'..
'([_] @escape (#vim-match? @escape "^\\\\\\\\n$"))'..
'([_] @string (#vim-match? @string "^\\"\\\\\\\\n\\"$"))')
parser = vim.treesitter.get_parser(0, "c")
tree = parser:parse()[1]
res = {}
for pattern, match in cquery:iter_matches(tree:root(), 0) do
-- can't transmit node over RPC. just check the name and range
local mrepr = {}
for cid,node in pairs(match) do
table.insert(mrepr, {cquery.captures[cid], node:type(), node:range()})
end
table.insert(res, {pattern, mrepr})
end
return res
]])
eq({
{ 2, { { "times", '*', 0, 4, 0, 5 } } },
{ 5, { { "string", 'string_literal', 0, 16, 0, 20 } } },
{ 4, { { "escape", 'escape_sequence', 0, 17, 0, 19 } } },
{ 3, { { "paren", '(', 0, 22, 0, 23 } } },
{ 1, { { "plus", '+', 0, 25, 0, 26 } } },
{ 2, { { "times", '*', 0, 30, 0, 31 } } },
}, res)
end)
it('supports builtin query predicate any-of?', function()
insert([[
#include <stdio.h>
int main(void) {
int i;
for(i=1; i<=100; i++) {
if(((i%3)||(i%5))== 0)
printf("number= %d FizzBuzz\n", i);
else if((i%3)==0)
printf("number= %d Fizz\n", i);
else if((i%5)==0)
printf("number= %d Buzz\n", i);
else
printf("number= %d\n",i);
}
return 0;
}
]])
exec_lua([[
function get_query_result(query_text)
cquery = vim.treesitter.query.parse("c", query_text)
parser = vim.treesitter.get_parser(0, "c")
tree = parser:parse()[1]
res = {}
for cid, node in cquery:iter_captures(tree:root(), 0) do
-- can't transmit node over RPC. just check the name, range, and text
local text = vim.treesitter.get_node_text(node, 0)
local range = {node:range()}
table.insert(res, {cquery.captures[cid], node:type(), range, text})
end
return res
end
]])
local res0 = exec_lua([[return get_query_result(...)]],
[[((primitive_type) @c-keyword (#any-of? @c-keyword "int" "float"))]])
eq({
{ "c-keyword", "primitive_type", { 2, 2, 2, 5 }, "int" },
{ "c-keyword", "primitive_type", { 3, 4, 3, 7 }, "int" },
}, res0)
local res1 = exec_lua([[return get_query_result(...)]],
[[
((string_literal) @fizzbuzz-strings (#any-of? @fizzbuzz-strings
"\"number= %d FizzBuzz\\n\""
"\"number= %d Fizz\\n\""
"\"number= %d Buzz\\n\""
))
]])
eq({
{ "fizzbuzz-strings", "string_literal", { 6, 15, 6, 38 }, "\"number= %d FizzBuzz\\n\""},
{ "fizzbuzz-strings", "string_literal", { 8, 15, 8, 34 }, "\"number= %d Fizz\\n\""},
{ "fizzbuzz-strings", "string_literal", { 10, 15, 10, 34 }, "\"number= %d Buzz\\n\""},
}, res1)
end)
it('allow loading query with escaped quotes and capture them with `lua-match?` and `vim-match?`', function()
insert('char* astring = "Hello World!";')
local res = exec_lua([[
cquery = vim.treesitter.query.parse("c", '([_] @quote (#vim-match? @quote "^\\"$")) ([_] @quote (#lua-match? @quote "^\\"$"))')
parser = vim.treesitter.get_parser(0, "c")
tree = parser:parse()[1]
res = {}
for pattern, match in cquery:iter_matches(tree:root(), 0) do
-- can't transmit node over RPC. just check the name and range
local mrepr = {}
for cid,node in pairs(match) do
table.insert(mrepr, {cquery.captures[cid], node:type(), node:range()})
end
table.insert(res, {pattern, mrepr})
end
return res
]])
eq({
{ 1, { { "quote", '"', 0, 16, 0, 17 } } },
{ 2, { { "quote", '"', 0, 16, 0, 17 } } },
{ 1, { { "quote", '"', 0, 29, 0, 30 } } },
{ 2, { { "quote", '"', 0, 29, 0, 30 } } },
}, res)
end)
it('allows to add predicates', function()
insert([[
int main(void) {
return 0;
}
]])
local custom_query = "((identifier) @main (#is-main? @main))"
local res = exec_lua([[
local query = vim.treesitter.query
local function is_main(match, pattern, bufnr, predicate)
local node = match[ predicate[2] ]
return query.get_node_text(node, bufnr)
end
local parser = vim.treesitter.get_parser(0, "c")
query.add_predicate("is-main?", is_main)
local query = query.parse("c", ...)
local nodes = {}
for _, node in query:iter_captures(parser:parse()[1]:root(), 0) do
table.insert(nodes, {node:range()})
end
return nodes
]], custom_query)
eq({{0, 4, 0, 8}}, res)
local res_list = exec_lua[[
local query = vim.treesitter.query
local list = query.list_predicates()
table.sort(list)
return list
]]
eq({ 'any-of?', 'contains?', 'eq?', 'has-ancestor?', 'has-parent?', 'is-main?', 'lua-match?', 'match?', 'vim-match?' }, res_list)
end)
it('allows to set simple ranges', function()
insert(test_text)
local res = exec_lua [[
parser = vim.treesitter.get_parser(0, "c")
return { parser:parse()[1]:root():range() }
]]
eq({0, 0, 19, 0}, res)
-- The following sets the included ranges for the current parser
-- As stated here, this only includes the function (thus the whole buffer, without the last line)
local res2 = exec_lua [[
local root = parser:parse()[1]:root()
parser:set_included_regions({{root:child(0)}})
parser:invalidate()
return { parser:parse(true)[1]:root():range() }
]]
eq({0, 0, 18, 1}, res2)
eq({ { { 0, 0, 0, 18, 1, 512 } } }, exec_lua [[ return parser:included_regions() ]])
local range_tbl = exec_lua [[
parser:set_included_regions { { { 0, 0, 17, 1 } } }
parser:parse()
return parser:included_regions()
]]
eq(range_tbl, { { { 0, 0, 0, 17, 1, 508 } } })
end)
it("allows to set complex ranges", function()
insert(test_text)
local res = exec_lua [[
parser = vim.treesitter.get_parser(0, "c")
query = vim.treesitter.query.parse("c", "(declaration) @decl")
local nodes = {}
for _, node in query:iter_captures(parser:parse()[1]:root(), 0) do
table.insert(nodes, node)
end
parser:set_included_regions({nodes})
local root = parser:parse(true)[1]:root()
local res = {}
for i=0,(root:named_child_count() - 1) do
table.insert(res, { root:named_child(i):range() })
end
return res
]]
eq({
{ 2, 2, 2, 40 },
{ 3, 2, 3, 32 },
{ 4, 7, 4, 25 },
{ 8, 2, 8, 33 },
{ 9, 7, 9, 20 },
{ 10, 4, 10, 20 },
{ 14, 9, 14, 27 } }, res)
end)
it("allows to create string parsers", function()
local ret = exec_lua [[
local parser = vim.treesitter.get_string_parser("int foo = 42;", "c")
return { parser:parse()[1]:root():range() }
]]
eq({ 0, 0, 0, 13 }, ret)
end)
it("allows to run queries with string parsers", function()
local txt = [[
int foo = 42;
int bar = 13;
]]
local ret = exec_lua([[
local str = ...
local parser = vim.treesitter.get_string_parser(str, "c")
local nodes = {}
local query = vim.treesitter.query.parse("c", '((identifier) @id (eq? @id "foo"))')
for _, node in query:iter_captures(parser:parse()[1]:root(), str) do
table.insert(nodes, { node:range() })
end
return nodes]], txt)
eq({ {0, 10, 0, 13} }, ret)
end)
it("should use node range when omitted", function()
local txt = [[
int foo = 42;
int bar = 13;
]]
local ret = exec_lua([[
local str = ...
local parser = vim.treesitter.get_string_parser(str, "c")
local nodes = {}
local query = vim.treesitter.query.parse("c", '((identifier) @foo)')
local first_child = parser:parse()[1]:root():child(1)
for _, node in query:iter_captures(first_child, str) do
table.insert(nodes, { node:range() })
end
return nodes]], txt)
eq({ {1, 10, 1, 13} }, ret)
end)
describe("when creating a language tree", function()
local function get_ranges()
return exec_lua([[
local result = {}
parser:for_each_tree(function(tree) table.insert(result, {tree:root():range()}) end)
return result
]])
end
before_each(function()
insert([[
int x = INT_MAX;
#define READ_STRING(x, y) (char *)read_string((x), (size_t)(y))
#define READ_STRING_OK(x, y) (char *)read_string((x), (size_t)(y))
#define VALUE 123
#define VALUE1 123
#define VALUE2 123
]])
end)
describe("when parsing regions independently", function()
it("should inject a language", function()
exec_lua([[
parser = vim.treesitter.get_parser(0, "c", {
injections = {
c = '(preproc_def (preproc_arg) @injection.content (#set! injection.language "c")) (preproc_function_def value: (preproc_arg) @injection.content (#set! injection.language "c"))'}})
parser:parse(true)
]])
eq("table", exec_lua("return type(parser:children().c)"))
eq(5, exec_lua("return #parser:children().c:trees()"))
eq({
{0, 0, 7, 0}, -- root tree
{3, 14, 3, 17}, -- VALUE 123
{4, 15, 4, 18}, -- VALUE1 123
{5, 15, 5, 18}, -- VALUE2 123
{1, 26, 1, 63}, -- READ_STRING(x, y) (char *)read_string((x), (size_t)(y))
{2, 29, 2, 66} -- READ_STRING_OK(x, y) (char *)read_string((x), (size_t)(y))
}, get_ranges())
helpers.feed('ggo<esc>')
eq(5, exec_lua("return #parser:children().c:trees()"))
eq({
{0, 0, 8, 0}, -- root tree
{4, 14, 4, 17}, -- VALUE 123
{5, 15, 5, 18}, -- VALUE1 123
{6, 15, 6, 18}, -- VALUE2 123
{2, 26, 2, 63}, -- READ_STRING(x, y) (char *)read_string((x), (size_t)(y))
{3, 29, 3, 66} -- READ_STRING_OK(x, y) (char *)read_string((x), (size_t)(y))
}, get_ranges())
end)
end)
describe("when parsing regions combined", function()
it("should inject a language", function()
exec_lua([[
parser = vim.treesitter.get_parser(0, "c", {
injections = {
c = '(preproc_def (preproc_arg) @injection.content (#set! injection.language "c") (#set! injection.combined)) (preproc_function_def value: (preproc_arg) @injection.content (#set! injection.language "c") (#set! injection.combined))'}})
parser:parse(true)
]])
eq("table", exec_lua("return type(parser:children().c)"))
eq(2, exec_lua("return #parser:children().c:trees()"))
eq({
{0, 0, 7, 0}, -- root tree
{3, 14, 5, 18}, -- VALUE 123
-- VALUE1 123
-- VALUE2 123
{1, 26, 2, 66} -- READ_STRING(x, y) (char *)read_string((x), (size_t)(y))
-- READ_STRING_OK(x, y) (char *)read_string((x), (size_t)(y))
}, get_ranges())
helpers.feed('ggo<esc>')
eq("table", exec_lua("return type(parser:children().c)"))
eq(2, exec_lua("return #parser:children().c:trees()"))
eq({
{0, 0, 8, 0}, -- root tree
{4, 14, 6, 18}, -- VALUE 123
-- VALUE1 123
-- VALUE2 123
{2, 26, 3, 66} -- READ_STRING(x, y) (char *)read_string((x), (size_t)(y))
-- READ_STRING_OK(x, y) (char *)read_string((x), (size_t)(y))
}, get_ranges())
helpers.feed('7ggI//<esc>')
exec_lua([[parser:parse({6, 7})]])
eq("table", exec_lua("return type(parser:children().c)"))
eq(2, exec_lua("return #parser:children().c:trees()"))
eq({
{0, 0, 8, 0}, -- root tree
{4, 14, 5, 18}, -- VALUE 123
-- VALUE1 123
{2, 26, 3, 66} -- READ_STRING(x, y) (char *)read_string((x), (size_t)(y))
-- READ_STRING_OK(x, y) (char *)read_string((x), (size_t)(y))
}, get_ranges())
end)
end)
describe("when using injection.self", function()
it("should inject the source language", function()
exec_lua([[
parser = vim.treesitter.get_parser(0, "c", {
injections = {
c = '(preproc_def (preproc_arg) @injection.content (#set! injection.self)) (preproc_function_def value: (preproc_arg) @injection.content (#set! injection.self))'}})
parser:parse(true)
]])
eq("table", exec_lua("return type(parser:children().c)"))
eq(5, exec_lua("return #parser:children().c:trees()"))
eq({
{0, 0, 7, 0}, -- root tree
{3, 14, 3, 17}, -- VALUE 123
{4, 15, 4, 18}, -- VALUE1 123
{5, 15, 5, 18}, -- VALUE2 123
{1, 26, 1, 63}, -- READ_STRING(x, y) (char *)read_string((x), (size_t)(y))
{2, 29, 2, 66} -- READ_STRING_OK(x, y) (char *)read_string((x), (size_t)(y))
}, get_ranges())
helpers.feed('ggo<esc>')
eq(5, exec_lua("return #parser:children().c:trees()"))
eq({
{0, 0, 8, 0}, -- root tree
{4, 14, 4, 17}, -- VALUE 123
{5, 15, 5, 18}, -- VALUE1 123
{6, 15, 6, 18}, -- VALUE2 123
{2, 26, 2, 63}, -- READ_STRING(x, y) (char *)read_string((x), (size_t)(y))
{3, 29, 3, 66} -- READ_STRING_OK(x, y) (char *)read_string((x), (size_t)(y))
}, get_ranges())
end)
end)
describe("when using the offset directive", function()
it("should shift the range by the directive amount", function()
exec_lua([[
parser = vim.treesitter.get_parser(0, "c", {
injections = {
c = '(preproc_def ((preproc_arg) @injection.content (#set! injection.language "c") (#offset! @injection.content 0 2 0 -1))) (preproc_function_def value: (preproc_arg) @injection.content (#set! injection.language "c"))'}})
parser:parse(true)
]])
eq("table", exec_lua("return type(parser:children().c)"))
eq({
{0, 0, 7, 0}, -- root tree
{3, 16, 3, 16}, -- VALUE 123
{4, 17, 4, 17}, -- VALUE1 123
{5, 17, 5, 17}, -- VALUE2 123
{1, 26, 1, 63}, -- READ_STRING(x, y) (char *)read_string((x), (size_t)(y))
{2, 29, 2, 66} -- READ_STRING_OK(x, y) (char *)read_string((x), (size_t)(y))
}, get_ranges())
end)
it("should list all directives", function()
local res_list = exec_lua[[
local query = vim.treesitter.query
local list = query.list_directives()
table.sort(list)
return list
]]
eq({ 'gsub!', 'offset!', 'set!', 'trim!' }, res_list)
end)
end)
end)
describe("when getting the language for a range", function()
before_each(function()
insert([[
int x = INT_MAX;
#define VALUE 123456789
]])
end)
it("should return the correct language tree", function()
local result = exec_lua([[
parser = vim.treesitter.get_parser(0, "c", {
injections = { c = '(preproc_def (preproc_arg) @injection.content (#set! injection.language "c"))'}})
parser:parse(true)
local sub_tree = parser:language_for_range({1, 18, 1, 19})
return sub_tree == parser:children().c
]])
eq(result, true)
end)
end)
describe("when getting/setting match data", function()
describe("when setting for the whole match", function()
it("should set/get the data correctly", function()
insert([[
int x = 3;
]])
local result = exec_lua([[
local result
query = vim.treesitter.query.parse("c", '((number_literal) @number (#set! "key" "value"))')
parser = vim.treesitter.get_parser(0, "c")
for pattern, match, metadata in query:iter_matches(parser:parse()[1]:root(), 0) do
result = metadata.key
end
return result
]])
eq(result, "value")
end)
describe("when setting a key on a capture", function()
it("it should create the nested table", function()
insert([[
int x = 3;
]])
local result = exec_lua([[
local query = vim.treesitter.query
local value
query = vim.treesitter.query.parse("c", '((number_literal) @number (#set! @number "key" "value"))')
parser = vim.treesitter.get_parser(0, "c")
for pattern, match, metadata in query:iter_matches(parser:parse()[1]:root(), 0) do
for _, nested_tbl in pairs(metadata) do
return nested_tbl.key
end
end
]])
eq(result, "value")
end)
it("it should not overwrite the nested table", function()
insert([[
int x = 3;
]])
local result = exec_lua([[
local query = vim.treesitter.query
local result
query = vim.treesitter.query.parse("c", '((number_literal) @number (#set! @number "key" "value") (#set! @number "key2" "value2"))')
parser = vim.treesitter.get_parser(0, "c")
for pattern, match, metadata in query:iter_matches(parser:parse()[1]:root(), 0) do
for _, nested_tbl in pairs(metadata) do
return nested_tbl
end
end
]])
local expected = {
["key"] = "value",
["key2"] = "value2",
}
eq(expected, result)
end)
end)
end)
end)
it('tracks the root range properly (#22911)', function()
insert([[
int main() {
int x = 3;
}]])
local query0 = [[
(declaration) @declaration
(function_definition) @function
]]
exec_lua([[
vim.treesitter.start(0, 'c')
]])
local function run_query()
return exec_lua([[
local query = vim.treesitter.query.parse("c", ...)
parser = vim.treesitter.get_parser()
tree = parser:parse()[1]
res = {}
for id, node in query:iter_captures(tree:root()) do
table.insert(res, {query.captures[id], node:range()})
end
return res
]], query0)
end
eq({
{ 'function', 0, 0, 2, 1 },
{ 'declaration', 1, 2, 1, 12 }
}, run_query())
helpers.command'normal ggO'
insert('int a;')
eq({
{ 'declaration', 0, 0, 0, 6 },
{ 'function', 1, 0, 3, 1 },
{ 'declaration', 2, 2, 2, 12 }
}, run_query())
end)
it('handles ranges when source is a multiline string (#20419)', function()
local source = [==[
vim.cmd[[
set number
set cmdheight=2
set lastsatus=2
]]
set query = [[;; query
((function_call
name: [
(identifier) @_cdef_identifier
(_ _ (identifier) @_cdef_identifier)
]
arguments: (arguments (string content: _ @injection.content)))
(#set! injection.language "c")
(#eq? @_cdef_identifier "cdef"))
]]
]==]
local r = exec_lua([[
local parser = vim.treesitter.get_string_parser(..., 'lua')
parser:parse(true)
local ranges = {}
parser:for_each_tree(function(tstree, tree)
ranges[tree:lang()] = { tstree:root():range(true) }
end)
return ranges
]], source)
eq({
lua = { 0, 6, 6, 16, 4, 438 },
query = { 6, 20, 113, 15, 6, 431 },
vim = { 1, 0, 16, 4, 6, 89 }
}, r)
-- The above ranges are provided directly from treesitter, however query directives may mutate
-- the ranges but only provide a Range4. Strip the byte entries from the ranges and make sure
-- add_bytes() produces the same result.
local rb = exec_lua([[
local r, source = ...
local add_bytes = require('vim.treesitter._range').add_bytes
for lang, range in pairs(r) do
r[lang] = {range[1], range[2], range[4], range[5]}
r[lang] = add_bytes(source, r[lang])
end
return r
]], r, source)
eq(rb, r)
end)
it("does not produce empty injection ranges (#23409)", function()
insert [[
Examples: >lua
local a = {}
<
]]
-- This is not a valid injection since (code) has children and include-children is not set
exec_lua [[
parser1 = require('vim.treesitter.languagetree').new(0, "vimdoc", {
injections = {
vimdoc = "((codeblock (language) @injection.language (code) @injection.content))"
}
})
parser1:parse(true)
]]
eq(0, exec_lua("return #vim.tbl_keys(parser1:children())"))
exec_lua [[
parser2 = require('vim.treesitter.languagetree').new(0, "vimdoc", {
injections = {
vimdoc = "((codeblock (language) @injection.language (code) @injection.content) (#set! injection.include-children))"
}
})
parser2:parse(true)
]]
eq(1, exec_lua("return #vim.tbl_keys(parser2:children())"))
eq( { { { 1, 0, 21, 2, 0, 42 } } }, exec_lua("return parser2:children().lua:included_regions()"))
end)
it("parsers injections incrementally", function()
insert(dedent[[
>lua
local a = {}
<
>lua
local b = {}
<
>lua
local c = {}
<
>lua
local d = {}
<
>lua
local e = {}
<
>lua
local f = {}
<
>lua
local g = {}
<
]])
exec_lua [[
parser = require('vim.treesitter.languagetree').new(0, "vimdoc", {
injections = {
vimdoc = "((codeblock (language) @injection.language (code) @injection.content) (#set! injection.include-children))"
}
})
]]
--- Do not parse injections by default
eq(0, exec_lua [[
parser:parse()
return #vim.tbl_keys(parser:children())
]])
--- Only parse injections between lines 0, 2
eq(1, exec_lua [[
parser:parse({0, 2})
return #parser:children().lua:trees()
]])
eq(2, exec_lua [[
parser:parse({2, 6})
return #parser:children().lua:trees()
]])
eq(7, exec_lua [[
parser:parse(true)
return #parser:children().lua:trees()
]])
end)
it('fails to load queries', function()
local function test(exp, cquery)
eq(exp, pcall_err(exec_lua, "vim.treesitter.query.parse('c', ...)", cquery))
end
-- Invalid node type
test(
'.../query.lua:0: Query error at 1:2. Invalid node type "dentifier":\n'..
'(dentifier) @variable\n'..
' ^',
'(dentifier) @variable')
-- Impossible pattern
test(
'.../query.lua:0: Query error at 1:13. Impossible pattern:\n'..
'(identifier (identifier) @variable)\n'..
' ^',
'(identifier (identifier) @variable)')
-- Invalid syntax
test(
'.../query.lua:0: Query error at 1:13. Invalid syntax:\n'..
'(identifier @variable\n'..
' ^',
'(identifier @variable')
-- Invalid field name
test(
'.../query.lua:0: Query error at 1:15. Invalid field name "invalid_field":\n'..
'((identifier) invalid_field: (identifier))\n'..
' ^',
'((identifier) invalid_field: (identifier))')
-- Invalid capture name
test(
'.../query.lua:0: Query error at 3:2. Invalid capture name "ok.capture":\n'..
'@ok.capture\n'..
' ^',
'((identifier) @id \n(#eq? @id\n@ok.capture\n))')
end)
describe('is_valid()', function()
before_each(function()
insert(dedent[[
Treesitter integration *treesitter*
Nvim integrates the `tree-sitter` library for incremental parsing of buffers:
https://tree-sitter.github.io/tree-sitter/
]])
feed(':set ft=help<cr>')
exec_lua [[
vim.treesitter.get_parser(0, "vimdoc", {
injections = {
vimdoc = "((codeblock (language) @injection.language (code) @injection.content) (#set! injection.include-children))"
}
})
]]
end)
it('is valid excluding, invalid including children initially', function()
eq(true, exec_lua('return vim.treesitter.get_parser():is_valid(true)'))
eq(false, exec_lua('return vim.treesitter.get_parser():is_valid()'))
end)
it('is fully valid after a full parse', function()
exec_lua('vim.treesitter.get_parser():parse(true)')
eq(true, exec_lua('return vim.treesitter.get_parser():is_valid(true)'))
eq(true, exec_lua('return vim.treesitter.get_parser():is_valid()'))
end)
it('is fully valid after a parsing a range on parsed tree', function()
exec_lua('vim.treesitter.get_parser():parse({5, 7})')
eq(true, exec_lua('return vim.treesitter.get_parser():is_valid(true)'))
eq(true, exec_lua('return vim.treesitter.get_parser():is_valid()'))
end)
describe('when adding content with injections', function()
before_each(function()
feed('G')
insert(dedent[[
>lua
local a = {}
<
]])
end)
it('is fully invalid after changes', function()
eq(false, exec_lua('return vim.treesitter.get_parser():is_valid(true)'))
eq(false, exec_lua('return vim.treesitter.get_parser():is_valid()'))
end)
it('is valid excluding, invalid including children after a rangeless parse', function()
exec_lua('vim.treesitter.get_parser():parse()')
eq(true, exec_lua('return vim.treesitter.get_parser():is_valid(true)'))
eq(false, exec_lua('return vim.treesitter.get_parser():is_valid()'))
end)
it('is fully valid after a range parse that leads to parsing not parsed injections', function()
exec_lua('vim.treesitter.get_parser():parse({5, 7})')
eq(true, exec_lua('return vim.treesitter.get_parser():is_valid(true)'))
eq(true, exec_lua('return vim.treesitter.get_parser():is_valid()'))
end)
it('is valid excluding, invalid including children after a range parse that does not lead to parsing not parsed injections', function()
exec_lua('vim.treesitter.get_parser():parse({2, 4})')
eq(true, exec_lua('return vim.treesitter.get_parser():is_valid(true)'))
eq(false, exec_lua('return vim.treesitter.get_parser():is_valid()'))
end)
end)
describe('when removing content with injections', function()
before_each(function()
feed('G')
insert(dedent[[
>lua
local a = {}
<
>lua
local a = {}
<
]])
exec_lua('vim.treesitter.get_parser():parse(true)')
feed('Gd3k')
end)
it('is fully invalid after changes', function()
eq(false, exec_lua('return vim.treesitter.get_parser():is_valid(true)'))
eq(false, exec_lua('return vim.treesitter.get_parser():is_valid()'))
end)
it('is valid excluding, invalid including children after a rangeless parse', function()
exec_lua('vim.treesitter.get_parser():parse()')
eq(true, exec_lua('return vim.treesitter.get_parser():is_valid(true)'))
eq(false, exec_lua('return vim.treesitter.get_parser():is_valid()'))
end)
it('is fully valid after a range parse that leads to parsing modified child tree', function()
exec_lua('vim.treesitter.get_parser():parse({5, 7})')
eq(true, exec_lua('return vim.treesitter.get_parser():is_valid(true)'))
eq(true, exec_lua('return vim.treesitter.get_parser():is_valid()'))
end)
it('is valid excluding, invalid including children after a range parse that does not lead to parsing modified child tree', function()
exec_lua('vim.treesitter.get_parser():parse({2, 4})')
eq(true, exec_lua('return vim.treesitter.get_parser():is_valid(true)'))
eq(false, exec_lua('return vim.treesitter.get_parser():is_valid()'))
end)
end)
end)
end)