Merge pull request #12739 from vigoux/ts-refactor-predicates

treesitter: refactor
This commit is contained in:
TJ DeVries 2020-08-14 08:33:50 -04:00 committed by GitHub
commit aa48c1c724
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 474 additions and 214 deletions

View File

@ -574,6 +574,14 @@ retained for the lifetime of a buffer but this is subject to change. A plugin
should keep a reference to the parser object as long as it wants incremental should keep a reference to the parser object as long as it wants incremental
updates. updates.
Parser files *treesitter-parsers*
Parsers are the heart of tree-sitter. They are libraries that tree-sitter will
search for in the `parsers` runtime directory.
For a parser to be available for a given language, there must be a file named
`{lang}.so` within the parser directory.
Parser methods *lua-treesitter-parser* Parser methods *lua-treesitter-parser*
tsparser:parse() *tsparser:parse()* tsparser:parse() *tsparser:parse()*
@ -593,9 +601,9 @@ shouldn't be done directly in the change callback anyway as they will be very
frequent. Rather a plugin that does any kind of analysis on a tree should use frequent. Rather a plugin that does any kind of analysis on a tree should use
a timer to throttle too frequent updates. a timer to throttle too frequent updates.
tsparser:set_included_ranges(ranges) *tsparser:set_included_ranges()* tsparser:set_included_ranges({ranges}) *tsparser:set_included_ranges()*
Changes the ranges the parser should consider. This is used for Changes the ranges the parser should consider. This is used for
language injection. `ranges` should be of the form (all zero-based): > language injection. {ranges} should be of the form (all zero-based): >
{ {
{start_node, end_node}, {start_node, end_node},
... ...
@ -617,15 +625,15 @@ tsnode:parent() *tsnode:parent()*
tsnode:child_count() *tsnode:child_count()* tsnode:child_count() *tsnode:child_count()*
Get the node's number of children. Get the node's number of children.
tsnode:child(N) *tsnode:child()* tsnode:child({index}) *tsnode:child()*
Get the node's child at the given index, where zero represents the Get the node's child at the given {index}, where zero represents the
first child. first child.
tsnode:named_child_count() *tsnode:named_child_count()* tsnode:named_child_count() *tsnode:named_child_count()*
Get the node's number of named children. Get the node's number of named children.
tsnode:named_child(N) *tsnode:named_child()* tsnode:named_child({index}) *tsnode:named_child()*
Get the node's named child at the given index, where zero represents Get the node's named child at the given {index}, where zero represents
the first named child. the first named child.
tsnode:start() *tsnode:start()* tsnode:start() *tsnode:start()*
@ -661,12 +669,12 @@ tsnode:has_error() *tsnode:has_error()*
tsnode:sexpr() *tsnode:sexpr()* tsnode:sexpr() *tsnode:sexpr()*
Get an S-expression representing the node as a string. Get an S-expression representing the node as a string.
tsnode:descendant_for_range(start_row, start_col, end_row, end_col) tsnode:descendant_for_range({start_row}, {start_col}, {end_row}, {end_col})
*tsnode:descendant_for_range()* *tsnode:descendant_for_range()*
Get the smallest node within this node that spans the given range of Get the smallest node within this node that spans the given range of
(row, column) positions (row, column) positions
tsnode:named_descendant_for_range(start_row, start_col, end_row, end_col) tsnode:named_descendant_for_range({start_row}, {start_col}, {end_row}, {end_col})
*tsnode:named_descendant_for_range()* *tsnode:named_descendant_for_range()*
Get the smallest named node within this node that spans the given Get the smallest named node within this node that spans the given
range of (row, column) positions range of (row, column) positions
@ -677,17 +685,17 @@ Tree-sitter queries are supported, with some limitations. Currently, the only
supported match predicate is `eq?` (both comparing a capture against a string supported match predicate is `eq?` (both comparing a capture against a string
and two captures against each other). and two captures against each other).
vim.treesitter.parse_query(lang, query) vim.treesitter.parse_query({lang}, {query})
*vim.treesitter.parse_query(()* *vim.treesitter.parse_query()*
Parse the query as a string. (If the query is in a file, the caller Parse {query} as a string. (If the query is in a file, the caller
should read the contents into a string before calling). should read the contents into a string before calling).
query:iter_captures(node, bufnr, start_row, end_row) query:iter_captures({node}, {bufnr}, {start_row}, {end_row})
*query:iter_captures()* *query:iter_captures()*
Iterate over all captures from all matches inside a `node`. Iterate over all captures from all matches inside {node}.
`bufnr` is needed if the query contains predicates, then the caller {bufnr} is needed if the query contains predicates, then the caller
must ensure to use a freshly parsed tree consistent with the current must ensure to use a freshly parsed tree consistent with the current
text of the buffer. `start_row` and `end_row` can be used to limit text of the buffer. {start_row} and {end_row} can be used to limit
matches inside a row range (this is typically used with root node matches inside a row range (this is typically used with root node
as the node, i e to get syntax highlight matches in the current as the node, i e to get syntax highlight matches in the current
viewport) viewport)
@ -704,7 +712,7 @@ query:iter_captures(node, bufnr, start_row, end_row)
... use the info here ... ... use the info here ...
end end
< <
query:iter_matches(node, bufnr, start_row, end_row) query:iter_matches({node}, {bufnr}, {start_row}, {end_row})
*query:iter_matches()* *query:iter_matches()*
Iterate over all matches within a node. The arguments are the same as Iterate over all matches within a node. The arguments are the same as
for |query:iter_captures()| but the iterated values are different: for |query:iter_captures()| but the iterated values are different:
@ -721,8 +729,52 @@ query:iter_matches(node, bufnr, start_row, end_row)
... use the info here ... ... use the info here ...
end end
end end
>
Treesitter syntax highlighting (WIP) *lua-treesitter-highlight* Treesitter Query Predicates *lua-treesitter-predicates*
When writing queries for treesitter, one might use `predicates`, that is,
special scheme nodes that are evaluted to verify things on a captured node for
example, the |eq?| predicate : >
((identifier) @foo (#eq? @foo "foo"))
This will only match identifier corresponding to the `"foo"` text.
Here is a list of built-in predicates :
`eq?` *ts-predicate-eq?*
This predicate will check text correspondance between nodes or
strings : >
((identifier) @foo (#eq? @foo "foo"))
((node1) @left (node2) @right (#eq? @left @right))
<
`match?` *ts-predicate-match?*
This will match if the provived lua regex matches the text
corresponding to a node : >
((idenfitier) @constant (#match? @constant "^[A-Z_]+$"))
< Note: the `^` and `$` anchors will respectively match the
start and end of the node's text.
`vim-match?` *ts-predicate-vim-match?*
This will match the same way than |match?| but using vim
regexes.
`contains?` *ts-predicate-contains?*
Will check if any of the following arguments appears in the
text corresponding to the node : >
((identifier) @foo (#contains? @foo "foo"))
((identifier) @foo-bar (#contains @foo-bar "foo" "bar"))
<
*lua-treesitter-not-predicate*
Each predicate has a `not-` prefixed predicate that is just the negation of
the predicate.
*vim.treesitter.query.add_predicate()*
vim.treesitter.query.add_predicate({name}, {handler})
This adds a predicate with the name {name} to be used in queries.
{handler} should be a function whose signature will be : >
handler(match, pattern, bufnr, predicate)
Treesitter syntax highlighting (WIP) *lua-treesitter-highlight*
NOTE: This is a partially implemented feature, and not usable as a default NOTE: This is a partially implemented feature, and not usable as a default
solution yet. What is documented here is a temporary interface indented solution yet. What is documented here is a temporary interface indented

View File

@ -1,4 +1,6 @@
local a = vim.api local a = vim.api
local query = require'vim.treesitter.query'
local language = require'vim.treesitter.language'
-- TODO(bfredl): currently we retain parsers for the lifetime of the buffer. -- TODO(bfredl): currently we retain parsers for the lifetime of the buffer.
-- Consider use weak references to release parser if all plugins are done with -- Consider use weak references to release parser if all plugins are done with
@ -8,6 +10,12 @@ local parsers = {}
local Parser = {} local Parser = {}
Parser.__index = Parser Parser.__index = Parser
--- Parses the buffer if needed and returns a tree.
--
-- Calling this will call the on_changedtree callbacks if the tree has changed.
--
-- @returns An up to date tree
-- @returns If the tree changed with this call, the changed ranges
function Parser:parse() function Parser:parse()
if self.valid then if self.valid then
return self.tree return self.tree
@ -38,48 +46,39 @@ function Parser:_on_lines(bufnr, changed_tick, start_row, old_stop_row, stop_row
end end
end end
--- Sets the included ranges for the current parser
--
-- @param ranges A table of nodes that will be used as the ranges the parser should include.
function Parser:set_included_ranges(ranges) function Parser:set_included_ranges(ranges)
self._parser:set_included_ranges(ranges) self._parser:set_included_ranges(ranges)
-- The buffer will need to be parsed again later -- The buffer will need to be parsed again later
self.valid = false self.valid = false
end end
local M = { local M = vim.tbl_extend("error", query, language)
parse_query = vim._ts_parse_query,
}
setmetatable(M, { setmetatable(M, {
__index = function (t, k) __index = function (t, k)
if k == "TSHighlighter" then if k == "TSHighlighter" then
t[k] = require'vim.tshighlighter' a.nvim_err_writeln("vim.TSHighlighter is deprecated, please use vim.treesitter.highlighter")
t[k] = require'vim.treesitter.highlighter'
return t[k]
elseif k == "highlighter" then
t[k] = require'vim.treesitter.highlighter'
return t[k] return t[k]
end end
end end
}) })
function M.require_language(lang, path) --- Creates a new parser.
if vim._ts_has_language(lang) then --
return true -- It is not recommended to use this, use vim.treesitter.get_parser() instead.
end --
if path == nil then -- @param bufnr The buffer the parser will be tied to
local fname = 'parser/' .. lang .. '.*' -- @param lang The language of the parser.
local paths = a.nvim_get_runtime_file(fname, false) -- @param id The id the parser will have
if #paths == 0 then function M._create_parser(bufnr, lang, id)
-- TODO(bfredl): help tag? language.require_language(lang)
error("no parser for '"..lang.."' language")
end
path = paths[1]
end
vim._ts_add_language(path, lang)
end
function M.inspect_language(lang)
M.require_language(lang)
return vim._ts_inspect_language(lang)
end
function M.create_parser(bufnr, lang, id)
M.require_language(lang)
if bufnr == 0 then if bufnr == 0 then
bufnr = a.nvim_get_current_buf() bufnr = a.nvim_get_current_buf()
end end
@ -91,8 +90,8 @@ function M.create_parser(bufnr, lang, id)
self.changedtree_cbs = {} self.changedtree_cbs = {}
self.lines_cbs = {} self.lines_cbs = {}
self:parse() self:parse()
-- TODO(bfredl): use weakref to self, so that the parser is free'd is no plugin is -- TODO(bfredl): use weakref to self, so that the parser is free'd is no plugin is
-- using it. -- using it.
local function lines_cb(_, ...) local function lines_cb(_, ...)
return self:_on_lines(...) return self:_on_lines(...)
end end
@ -108,17 +107,31 @@ function M.create_parser(bufnr, lang, id)
return self return self
end end
function M.get_parser(bufnr, ft, buf_attach_cbs) --- Gets the parser for this bufnr / ft combination.
--
-- If needed this will create the parser.
-- Unconditionnally attach the provided callback
--
-- @param bufnr The buffer the parser should be tied to
-- @param ft The filetype of this parser
-- @param buf_attach_cbs An `nvim_buf_attach`-like table argument with the following keys :
-- `on_lines` : see `nvim_buf_attach`, but this will be called _after_ the parsers callback.
-- `on_changedtree` : a callback that will be called everytime the tree has syntactical changes.
-- it will only be passed one argument, that is a table of the ranges (as node ranges) that
-- changed.
--
-- @returns The parser
function M.get_parser(bufnr, lang, buf_attach_cbs)
if bufnr == nil or bufnr == 0 then if bufnr == nil or bufnr == 0 then
bufnr = a.nvim_get_current_buf() bufnr = a.nvim_get_current_buf()
end end
if ft == nil then if lang == nil then
ft = a.nvim_buf_get_option(bufnr, "filetype") lang = a.nvim_buf_get_option(bufnr, "filetype")
end end
local id = tostring(bufnr)..'_'..ft local id = tostring(bufnr)..'_'..lang
if parsers[id] == nil then if parsers[id] == nil then
parsers[id] = M.create_parser(bufnr, ft, id) parsers[id] = M._create_parser(bufnr, lang, id)
end end
if buf_attach_cbs and buf_attach_cbs.on_changedtree then if buf_attach_cbs and buf_attach_cbs.on_changedtree then
@ -132,129 +145,4 @@ function M.get_parser(bufnr, ft, buf_attach_cbs)
return parsers[id] return parsers[id]
end end
-- query: pattern matching on trees
-- predicate matching is implemented in lua
local Query = {}
Query.__index = Query
local magic_prefixes = {['\\v']=true, ['\\m']=true, ['\\M']=true, ['\\V']=true}
local function check_magic(str)
if string.len(str) < 2 or magic_prefixes[string.sub(str,1,2)] then
return str
end
return '\\v'..str
end
function M.parse_query(lang, query)
M.require_language(lang)
local self = setmetatable({}, Query)
self.query = vim._ts_parse_query(lang, vim.fn.escape(query,'\\'))
self.info = self.query:inspect()
self.captures = self.info.captures
self.regexes = {}
for id,preds in pairs(self.info.patterns) do
local regexes = {}
for i, pred in ipairs(preds) do
if (pred[1] == "match?" and type(pred[2]) == "number"
and type(pred[3]) == "string") then
regexes[i] = vim.regex(check_magic(pred[3]))
end
end
if next(regexes) then
self.regexes[id] = regexes
end
end
return self
end
local function get_node_text(node, bufnr)
local start_row, start_col, end_row, end_col = node:range()
if start_row ~= end_row then
return nil
end
local line = a.nvim_buf_get_lines(bufnr, start_row, start_row+1, true)[1]
return string.sub(line, start_col+1, end_col)
end
function Query:match_preds(match, pattern, bufnr)
local preds = self.info.patterns[pattern]
if not preds then
return true
end
local regexes = self.regexes[pattern]
for i, pred in pairs(preds) do
-- Here we only want to return if a predicate DOES NOT match, and
-- continue on the other case. This way unknown predicates will not be considered,
-- which allows some testing and easier user extensibility (#12173).
-- Also, tree-sitter strips the leading # from predicates for us.
if pred[1] == "eq?" then
local node = match[pred[2]]
local node_text = get_node_text(node, bufnr)
local str
if type(pred[3]) == "string" then
-- (#eq? @aa "foo")
str = pred[3]
else
-- (#eq? @aa @bb)
str = get_node_text(match[pred[3]], bufnr)
end
if node_text ~= str or str == nil then
return false
end
elseif pred[1] == "match?" then
if not regexes or not regexes[i] then
return false
end
local node = match[pred[2]]
local start_row, start_col, end_row, end_col = node:range()
if start_row ~= end_row then
return false
end
if not regexes[i]:match_line(bufnr, start_row, start_col, end_col) then
return false
end
end
end
return true
end
function Query:iter_captures(node, bufnr, start, stop)
if bufnr == 0 then
bufnr = vim.api.nvim_get_current_buf()
end
local raw_iter = node:_rawquery(self.query,true,start,stop)
local function iter()
local capture, captured_node, match = raw_iter()
if match ~= nil then
local active = self:match_preds(match, match.pattern, bufnr)
match.active = active
if not active then
return iter() -- tail call: try next match
end
end
return capture, captured_node
end
return iter
end
function Query:iter_matches(node, bufnr, start, stop)
if bufnr == 0 then
bufnr = vim.api.nvim_get_current_buf()
end
local raw_iter = node:_rawquery(self.query,false,start,stop)
local function iter()
local pattern, match = raw_iter()
if match ~= nil then
local active = self:match_preds(match, pattern, bufnr)
if not active then
return iter() -- tail call: try next match
end
end
return pattern, match
end
return iter
end
return M return M

View File

@ -7,20 +7,50 @@ local ts_hs_ns = a.nvim_create_namespace("treesitter_hl")
-- These are conventions defined by tree-sitter, though it -- These are conventions defined by tree-sitter, though it
-- needs to be user extensible also. -- needs to be user extensible also.
-- TODO(bfredl): this is very much incomplete, we will need to
-- go through a few tree-sitter provided queries and decide
-- on translations that makes the most sense.
TSHighlighter.hl_map = { TSHighlighter.hl_map = {
keyword="Keyword", ["error"] = "Error",
string="String",
type="Type", -- Miscs
comment="Comment", ["comment"] = "Comment",
constant="Constant", ["punctuation.delimiter"] = "Delimiter",
operator="Operator", ["punctuation.bracket"] = "Delimiter",
number="Number", ["punctuation.special"] = "Delimiter",
label="Label",
["function"]="Function", -- Constants
["function.special"]="Function", ["constant"] = "Constant",
["constant.builtin"] = "Special",
["constant.macro"] = "Define",
["string"] = "String",
["string.regex"] = "String",
["string.escape"] = "SpecialChar",
["character"] = "Character",
["number"] = "Number",
["boolean"] = "Boolean",
["float"] = "Float",
-- Functions
["function"] = "Function",
["function.special"] = "Function",
["function.builtin"] = "Special",
["function.macro"] = "Macro",
["parameter"] = "Identifier",
["method"] = "Function",
["field"] = "Identifier",
["property"] = "Identifier",
["constructor"] = "Special",
-- Keywords
["conditional"] = "Conditional",
["repeat"] = "Repeat",
["label"] = "Label",
["operator"] = "Operator",
["keyword"] = "Keyword",
["exception"] = "Exception",
["type"] = "Type",
["type.builtin"] = "Type",
["structure"] = "Structure",
["include"] = "Include",
} }
function TSHighlighter.new(query, bufnr, ft) function TSHighlighter.new(query, bufnr, ft)
@ -75,7 +105,15 @@ end
function TSHighlighter:set_query(query) function TSHighlighter:set_query(query)
if type(query) == "string" then if type(query) == "string" then
query = vim.treesitter.parse_query(self.parser.lang, query) query = vim.treesitter.parse_query(self.parser.lang, query)
elseif query == nil then
query = vim.treesitter.get_query(self.parser.lang, 'highlights')
if query == nil then
a.nvim_err_writeln("No highlights.scm query found for " .. self.parser.lang)
query = vim.treesitter.parse_query(self.parser.lang, "")
end
end end
self.query = query self.query = query
self.hl_cache = setmetatable({}, { self.hl_cache = setmetatable({}, {

View File

@ -0,0 +1,37 @@
local a = vim.api
local M = {}
--- Asserts that the provided language is installed, and optionnaly provide a path for the parser
--
-- Parsers are searched in the `parser` runtime directory.
--
-- @param lang The language the parser should parse
-- @param path Optionnal path the parser is located at
function M.require_language(lang, path)
if vim._ts_has_language(lang) then
return true
end
if path == nil then
local fname = 'parser/' .. lang .. '.*'
local paths = a.nvim_get_runtime_file(fname, false)
if #paths == 0 then
-- TODO(bfredl): help tag?
error("no parser for '"..lang.."' language, see :help treesitter-parsers")
end
path = paths[1]
end
vim._ts_add_language(path, lang)
end
--- Inspects the provided language.
--
-- Inspecting provides some useful informations on the language like node names, ...
--
-- @param lang The language.
function M.inspect_language(lang)
M.require_language(lang)
return vim._ts_inspect_language(lang)
end
return M

View File

@ -0,0 +1,210 @@
local a = vim.api
local language = require'vim.treesitter.language'
-- query: pattern matching on trees
-- predicate matching is implemented in lua
local Query = {}
Query.__index = Query
local M = {}
--- Parses a query.
--
-- @param language The language
-- @param query A string containing the query (s-expr syntax)
--
-- @returns The query
function M.parse_query(lang, query)
language.require_language(lang)
local self = setmetatable({}, Query)
self.query = vim._ts_parse_query(lang, vim.fn.escape(query,'\\'))
self.info = self.query:inspect()
self.captures = self.info.captures
return self
end
-- TODO(vigoux): support multiline nodes too
--- Gets the text corresponding to a given node
-- @param node the node
-- @param bufnr the buffer from which the node in extracted.
function M.get_node_text(node, bufnr)
local start_row, start_col, end_row, end_col = node:range()
if start_row ~= end_row then
return nil
end
local line = a.nvim_buf_get_lines(bufnr, start_row, start_row+1, true)[1]
return string.sub(line, start_col+1, end_col)
end
-- Predicate handler receive the following arguments
-- (match, pattern, bufnr, predicate)
local predicate_handlers = {
["eq?"] = function(match, _, bufnr, predicate)
local node = match[predicate[2]]
local node_text = M.get_node_text(node, bufnr)
local str
if type(predicate[3]) == "string" then
-- (#eq? @aa "foo")
str = predicate[3]
else
-- (#eq? @aa @bb)
str = M.get_node_text(match[predicate[3]], bufnr)
end
if node_text ~= str or str == nil then
return false
end
return true
end,
["match?"] = function(match, _, bufnr, predicate)
local node = match[predicate[2]]
local regex = predicate[3]
local start_row, _, end_row, _ = node:range()
if start_row ~= end_row then
return false
end
return string.find(M.get_node_text(node, bufnr), regex)
end,
["vim-match?"] = (function()
local magic_prefixes = {['\\v']=true, ['\\m']=true, ['\\M']=true, ['\\V']=true}
local function check_magic(str)
if string.len(str) < 2 or magic_prefixes[string.sub(str,1,2)] then
return str
end
return '\\v'..str
end
local compiled_vim_regexes = setmetatable({}, {
__index = function(t, pattern)
local res = vim.regex(check_magic(pattern))
rawset(t, pattern, res)
return res
end
})
return function(match, _, bufnr, pred)
local node = match[pred[2]]
local start_row, start_col, end_row, end_col = node:range()
if start_row ~= end_row then
return false
end
local regex = compiled_vim_regexes[pred[3]]
return regex:match_line(bufnr, start_row, start_col, end_col)
end
end)(),
["contains?"] = function(match, _, bufnr, predicate)
local node = match[predicate[2]]
local node_text = M.get_node_text(node, bufnr)
for i=3,#predicate do
if string.find(node_text, predicate[i], 1, true) then
return true
end
end
return false
end
}
--- Adds a new predicates to be used in queries
--
-- @param name the name of the predicate, without leading #
-- @param handler the handler function to be used
-- signature will be (match, pattern, bufnr, predicate)
function M.add_predicate(name, handler, force)
if predicate_handlers[name] and not force then
a.nvim_err_writeln(string.format("Overriding %s", name))
end
predicate_handlers[name] = handler
end
function Query:match_preds(match, pattern, bufnr)
local preds = self.info.patterns[pattern]
if not preds then
return true
end
for _, pred in pairs(preds) do
-- Here we only want to return if a predicate DOES NOT match, and
-- continue on the other case. This way unknown predicates will not be considered,
-- which allows some testing and easier user extensibility (#12173).
-- Also, tree-sitter strips the leading # from predicates for us.
if string.sub(pred[1], 1, 4) == "not-" then
local pred_name = string.sub(pred[1], 5)
if predicate_handlers[pred_name] and
predicate_handlers[pred_name](match, pattern, bufnr, pred) then
return false
end
elseif predicate_handlers[pred[1]] and
not predicate_handlers[pred[1]](match, pattern, bufnr, pred) then
return false
end
end
return true
end
--- Iterates of the captures of self on a given range.
--
-- @param node The node under witch the search will occur
-- @param buffer The source buffer to search
-- @param start The starting line of the search
-- @param stop The stoping line of the search (end-exclusive)
--
-- @returns The matching capture id
-- @returns The captured node
function Query:iter_captures(node, bufnr, start, stop)
if bufnr == 0 then
bufnr = vim.api.nvim_get_current_buf()
end
local raw_iter = node:_rawquery(self.query, true, start, stop)
local function iter()
local capture, captured_node, match = raw_iter()
if match ~= nil then
local active = self:match_preds(match, match.pattern, bufnr)
match.active = active
if not active then
return iter() -- tail call: try next match
end
end
return capture, captured_node
end
return iter
end
--- Iterates of the matches of self on a given range.
--
-- @param node The node under witch the search will occur
-- @param buffer The source buffer to search
-- @param start The starting line of the search
-- @param stop The stoping line of the search (end-exclusive)
--
-- @returns The matching pattern id
-- @returns The matching match
function Query:iter_matches(node, bufnr, start, stop)
if bufnr == 0 then
bufnr = vim.api.nvim_get_current_buf()
end
local raw_iter = node:_rawquery(self.query, false, start, stop)
local function iter()
local pattern, match = raw_iter()
if match ~= nil then
local active = self:match_preds(match, pattern, bufnr)
if not active then
return iter() -- tail call: try next match
end
end
return pattern, match
end
return iter
end
return M

View File

@ -15,14 +15,14 @@ before_each(clear)
describe('treesitter API', function() describe('treesitter API', function()
-- error tests not requiring a parser library -- error tests not requiring a parser library
it('handles missing language', function() it('handles missing language', function()
eq("Error executing lua: .../treesitter.lua: no parser for 'borklang' language", eq("Error executing lua: .../language.lua: no parser for 'borklang' language, see :help treesitter-parsers",
pcall_err(exec_lua, "parser = vim.treesitter.create_parser(0, 'borklang')")) pcall_err(exec_lua, "parser = vim.treesitter.get_parser(0, 'borklang')"))
-- actual message depends on platform -- actual message depends on platform
matches("Error executing lua: Failed to load parser: uv_dlopen: .+", matches("Error executing lua: Failed to load parser: uv_dlopen: .+",
pcall_err(exec_lua, "parser = vim.treesitter.require_language('borklang', 'borkbork.so')")) pcall_err(exec_lua, "parser = vim.treesitter.require_language('borklang', 'borkbork.so')"))
eq("Error executing lua: .../treesitter.lua: no parser for 'borklang' language", eq("Error executing lua: .../language.lua: no parser for 'borklang' language, see :help treesitter-parsers",
pcall_err(exec_lua, "parser = vim.treesitter.inspect_language('borklang')")) pcall_err(exec_lua, "parser = vim.treesitter.inspect_language('borklang')"))
end) end)
@ -198,6 +198,41 @@ void ui_refresh(void)
}, res) }, res)
end) 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 = require"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_query("c", ...)
local nodes = {}
for _, node in query:iter_captures(parser:parse():root(), 0, 0, 19) do
table.insert(nodes, {node:range()})
end
return nodes
]], custom_query)
eq({{0, 4, 0, 8}}, res)
end)
it('supports highlighting', function() it('supports highlighting', function()
if not check_parser() then return end if not check_parser() then return end
@ -243,10 +278,10 @@ static int nlua_schedule(lua_State *const lstate)
(primitive_type) @type (primitive_type) @type
(sized_type_specifier) @type (sized_type_specifier) @type
; defaults to very magic syntax, for best compatibility ; Use lua regexes
((identifier) @Identifier (#match? @Identifier "^l(u)a_")) ((identifier) @Identifier (#contains? @Identifier "lua_"))
; still support \M etc prefixes ((identifier) @Constant (#match? @Constant "^[A-Z_]+$"))
((identifier) @Constant (#match? @Constant "\M^\[A-Z_]\+$")) ((identifier) @Normal (#vim-match? @Constant "^lstate$"))
((binary_expression left: (identifier) @WarningMsg.left right: (identifier) @WarningMsg.right) (#eq? @WarningMsg.left @WarningMsg.right)) ((binary_expression left: (identifier) @WarningMsg.left right: (identifier) @WarningMsg.right) (#eq? @WarningMsg.left @WarningMsg.right))
@ -292,13 +327,13 @@ static int nlua_schedule(lua_State *const lstate)
]]} ]]}
exec_lua([[ exec_lua([[
local TSHighlighter = vim.treesitter.TSHighlighter local highlighter = vim.treesitter.highlighter
local query = ... local query = ...
test_hl = TSHighlighter.new(query, 0, "c") test_hl = highlighter.new(query, 0, "c")
]], hl_query) ]], hl_query)
screen:expect{grid=[[ screen:expect{grid=[[
{2:/// Schedule Lua callback on main loop's event queue} | {2:/// Schedule Lua callback on main loop's event queue} |
{3:static} {3:int} nlua_schedule({3:lua_State} *{3:const} lstate) | {3:static} {3:int} {11:nlua_schedule}({3:lua_State} *{3:const} lstate) |
{ | { |
{4:if} ({11:lua_type}(lstate, {5:1}) != {5:LUA_TFUNCTION} | {4:if} ({11:lua_type}(lstate, {5:1}) != {5:LUA_TFUNCTION} |
|| {6:lstate} != {6:lstate}) { | || {6:lstate} != {6:lstate}) { |
@ -306,9 +341,9 @@ static int nlua_schedule(lua_State *const lstate)
{4:return} {11:lua_error}(lstate); | {4:return} {11:lua_error}(lstate); |
} | } |
| |
{7:LuaRef} cb = nlua_ref(lstate, {5:1}); | {7:LuaRef} cb = {11:nlua_ref}(lstate, {5:1}); |
| |
multiqueue_put(main_loop.events, nlua_schedule_event, | multiqueue_put(main_loop.events, {11:nlua_schedule_event}, |
{5:1}, ({3:void} *)({3:ptrdiff_t})cb); | {5:1}, ({3:void} *)({3:ptrdiff_t})cb); |
{4:return} {5:0}; | {4:return} {5:0}; |
^} | ^} |
@ -320,7 +355,7 @@ static int nlua_schedule(lua_State *const lstate)
feed('7Go*/<esc>') feed('7Go*/<esc>')
screen:expect{grid=[[ screen:expect{grid=[[
{2:/// Schedule Lua callback on main loop's event queue} | {2:/// Schedule Lua callback on main loop's event queue} |
{3:static} {3:int} nlua_schedule({3:lua_State} *{3:const} lstate) | {3:static} {3:int} {11:nlua_schedule}({3:lua_State} *{3:const} lstate) |
{ | { |
{4:if} ({11:lua_type}(lstate, {5:1}) != {5:LUA_TFUNCTION} | {4:if} ({11:lua_type}(lstate, {5:1}) != {5:LUA_TFUNCTION} |
|| {6:lstate} != {6:lstate}) { | || {6:lstate} != {6:lstate}) { |
@ -329,9 +364,9 @@ static int nlua_schedule(lua_State *const lstate)
{8:*^/} | {8:*^/} |
} | } |
| |
{7:LuaRef} cb = nlua_ref(lstate, {5:1}); | {7:LuaRef} cb = {11:nlua_ref}(lstate, {5:1}); |
| |
multiqueue_put(main_loop.events, nlua_schedule_event, | multiqueue_put(main_loop.events, {11:nlua_schedule_event}, |
{5:1}, ({3:void} *)({3:ptrdiff_t})cb); | {5:1}, ({3:void} *)({3:ptrdiff_t})cb); |
{4:return} {5:0}; | {4:return} {5:0}; |
} | } |
@ -342,7 +377,7 @@ static int nlua_schedule(lua_State *const lstate)
feed('3Go/*<esc>') feed('3Go/*<esc>')
screen:expect{grid=[[ screen:expect{grid=[[
{2:/// Schedule Lua callback on main loop's event queue} | {2:/// Schedule Lua callback on main loop's event queue} |
{3:static} {3:int} nlua_schedule({3:lua_State} *{3:const} lstate) | {3:static} {3:int} {11:nlua_schedule}({3:lua_State} *{3:const} lstate) |
{ | { |
{2:/^*} | {2:/^*} |
{2: if (lua_type(lstate, 1) != LUA_TFUNCTION} | {2: if (lua_type(lstate, 1) != LUA_TFUNCTION} |
@ -352,9 +387,9 @@ static int nlua_schedule(lua_State *const lstate)
{2:*/} | {2:*/} |
} | } |
| |
{7:LuaRef} cb = nlua_ref(lstate, {5:1}); | {7:LuaRef} cb = {11:nlua_ref}(lstate, {5:1}); |
| |
multiqueue_put(main_loop.events, nlua_schedule_event, | multiqueue_put(main_loop.events, {11:nlua_schedule_event}, |
{5:1}, ({3:void} *)({3:ptrdiff_t})cb); | {5:1}, ({3:void} *)({3:ptrdiff_t})cb); |
{4:return} {5:0}; | {4:return} {5:0}; |
{8:}} | {8:}} |
@ -365,7 +400,7 @@ static int nlua_schedule(lua_State *const lstate)
feed("~") feed("~")
screen:expect{grid=[[ screen:expect{grid=[[
{2:/// Schedule Lua callback on main loop's event queu^E} | {2:/// Schedule Lua callback on main loop's event queu^E} |
{3:static} {3:int} nlua_schedule({3:lua_State} *{3:const} lstate) | {3:static} {3:int} {11:nlua_schedule}({3:lua_State} *{3:const} lstate) |
{ | { |
{2:/*} | {2:/*} |
{2: if (lua_type(lstate, 1) != LUA_TFUNCTION} | {2: if (lua_type(lstate, 1) != LUA_TFUNCTION} |
@ -375,9 +410,9 @@ static int nlua_schedule(lua_State *const lstate)
{2:*/} | {2:*/} |
} | } |
| |
{7:LuaRef} cb = nlua_ref(lstate, {5:1}); | {7:LuaRef} cb = {11:nlua_ref}(lstate, {5:1}); |
| |
multiqueue_put(main_loop.events, nlua_schedule_event, | multiqueue_put(main_loop.events, {11:nlua_schedule_event}, |
{5:1}, ({3:void} *)({3:ptrdiff_t})cb); | {5:1}, ({3:void} *)({3:ptrdiff_t})cb); |
{4:return} {5:0}; | {4:return} {5:0}; |
{8:}} | {8:}} |
@ -388,7 +423,7 @@ static int nlua_schedule(lua_State *const lstate)
feed("re") feed("re")
screen:expect{grid=[[ screen:expect{grid=[[
{2:/// Schedule Lua callback on main loop's event queu^e} | {2:/// Schedule Lua callback on main loop's event queu^e} |
{3:static} {3:int} nlua_schedule({3:lua_State} *{3:const} lstate) | {3:static} {3:int} {11:nlua_schedule}({3:lua_State} *{3:const} lstate) |
{ | { |
{2:/*} | {2:/*} |
{2: if (lua_type(lstate, 1) != LUA_TFUNCTION} | {2: if (lua_type(lstate, 1) != LUA_TFUNCTION} |
@ -398,9 +433,9 @@ static int nlua_schedule(lua_State *const lstate)
{2:*/} | {2:*/} |
} | } |
| |
{7:LuaRef} cb = nlua_ref(lstate, {5:1}); | {7:LuaRef} cb = {11:nlua_ref}(lstate, {5:1}); |
| |
multiqueue_put(main_loop.events, nlua_schedule_event, | multiqueue_put(main_loop.events, {11:nlua_schedule_event}, |
{5:1}, ({3:void} *)({3:ptrdiff_t})cb); | {5:1}, ({3:void} *)({3:ptrdiff_t})cb); |
{4:return} {5:0}; | {4:return} {5:0}; |
{8:}} | {8:}} |