mirror of
				https://github.com/neovim/neovim.git
				synced 2025-10-26 04:17:01 +00:00 
			
		
		
		
	fix(treesitter): correctly handle query quantifiers (#24738)
Query patterns can contain quantifiers (e.g. (foo)+ @bar), so a single capture can map to multiple nodes. The iter_matches API can not handle this situation because the match table incorrectly maps capture indices to a single node instead of to an array of nodes. The match table should be updated to map capture indices to an array of nodes. However, this is a massively breaking change, so must be done with a proper deprecation period. `iter_matches`, `add_predicate` and `add_directive` must opt-in to the correct behavior for backward compatibility. This is done with a new "all" option. This option will become the default and removed after the 0.10 release. Co-authored-by: Christian Clason <c.clason@uni-graz.at> Co-authored-by: MDeiml <matthias@deiml.net> Co-authored-by: Gregory Anders <greg@gpanders.com>
This commit is contained in:
		| @@ -427,6 +427,18 @@ The following changes to existing APIs or features add new behavior. | ||||
|  | ||||
| • |nvim_buf_call()| and |nvim_win_call()| now preserves any return value (NB: not multiple return values) | ||||
|  | ||||
| • Treesitter | ||||
|   • |Query:iter_matches()|, |vim.treesitter.query.add_predicate()|, and | ||||
|     |vim.treesitter.query.add_directive()| accept a new `all` option which | ||||
|     ensures that all matching nodes are returned as a table. The default option | ||||
|     `all=false` returns only a single node, breaking captures with quantifiers | ||||
|     like `(comment)+ @comment; it is only provided for backward compatibility | ||||
|     and will be removed after Nvim 0.10. | ||||
|   • |vim.treesitter.query.add_predicate()| and | ||||
|     |vim.treesitter.query.add_directive()| now accept an options table rather | ||||
|     than a boolean "force" argument. To force a predicate or directive to | ||||
|     override an existing predicate or directive, use `{ force = true }`. | ||||
|  | ||||
| ============================================================================== | ||||
| REMOVED FEATURES                                                 *news-removed* | ||||
|  | ||||
| @@ -480,7 +492,7 @@ release. | ||||
|  | ||||
| • `vim.loop` has been renamed to |vim.uv|. | ||||
|  | ||||
| • vim.treesitter.languagetree functions: | ||||
| • vim.treesitter functions: | ||||
|   - |LanguageTree:for_each_child()|	Use |LanguageTree:children()| (non-recursive) instead. | ||||
|  | ||||
| • The "term_background" UI option |ui-ext-options| is deprecated and no longer | ||||
|   | ||||
| @@ -223,6 +223,10 @@ The following predicates are built in: | ||||
|             ((identifier) @variable.builtin (#eq? @variable.builtin "self")) | ||||
|             ((node1) @left (node2) @right (#eq? @left @right)) | ||||
| < | ||||
|     `any-eq?`                                    *treesitter-predicate-any-eq?* | ||||
|         Like `eq?`, but for quantified patterns only one captured node must | ||||
|         match. | ||||
|  | ||||
|     `match?`                                      *treesitter-predicate-match?* | ||||
|     `vim-match?`                              *treesitter-predicate-vim-match?* | ||||
|          Match a |regexp| against the text corresponding to a node: >query | ||||
| @@ -231,15 +235,28 @@ The following predicates are built in: | ||||
|          Note: The `^` and `$` anchors will match the start and end of the | ||||
|                node's text. | ||||
|  | ||||
|     `any-match?`                              *treesitter-predicate-any-match?* | ||||
|     `any-vim-match?`                      *treesitter-predicate-any-vim-match?* | ||||
|         Like `match?`, but for quantified patterns only one captured node must | ||||
|         match. | ||||
|  | ||||
|     `lua-match?`                              *treesitter-predicate-lua-match?* | ||||
|          Match |lua-patterns| against the text corresponding to a node, | ||||
|          similar to `match?` | ||||
|  | ||||
|     `any-lua-match?`                      *treesitter-predicate-any-lua-match?* | ||||
|          Like `lua-match?`, but for quantified patterns only one captured node | ||||
|          must match. | ||||
|  | ||||
|     `contains?`                                *treesitter-predicate-contains?* | ||||
|         Match a string against parts of the text corresponding to a node: >query | ||||
|             ((identifier) @foo (#contains? @foo "foo")) | ||||
|             ((identifier) @foo-bar (#contains? @foo-bar "foo" "bar")) | ||||
| < | ||||
|     `any-contains?`                        *treesitter-predicate-any-contains?* | ||||
|         Like `contains?`, but for quantified patterns only one captured node | ||||
|         must match. | ||||
|  | ||||
|     `any-of?`                                    *treesitter-predicate-any-of?* | ||||
|         Match any of the given strings against the text corresponding to | ||||
|         a node: >query | ||||
| @@ -265,6 +282,32 @@ The following predicates are built in: | ||||
| Each predicate has a `not-` prefixed predicate that is just the negation of | ||||
| the predicate. | ||||
|  | ||||
|                                                  *lua-treesitter-all-predicate* | ||||
|                                                  *lua-treesitter-any-predicate* | ||||
| Queries can use quantifiers to capture multiple nodes. When a capture contains | ||||
| multiple nodes, predicates match only if ALL nodes contained by the capture | ||||
| match the predicate. Some predicates (`eq?`, `match?`, `lua-match?`, | ||||
| `contains?`) accept an `any-` prefix to instead match if ANY of the nodes | ||||
| contained by the capture match the predicate. | ||||
|  | ||||
| As an example, consider the following Lua code: >lua | ||||
|  | ||||
|   -- TODO: This is a | ||||
|   -- very long | ||||
|   -- comment (just imagine it) | ||||
| < | ||||
| using the following predicated query: | ||||
| >query | ||||
|     (((comment)+ @comment) | ||||
|      (#match? @comment "TODO")) | ||||
| < | ||||
| This query will not match because not all of the nodes captured by @comment | ||||
| match the predicate. Instead, use: | ||||
| >query | ||||
|     (((comment)+ @comment) | ||||
|      (#any-match? @comment "TODO")) | ||||
| < | ||||
|  | ||||
| Further predicates can be added via |vim.treesitter.query.add_predicate()|. | ||||
| Use |vim.treesitter.query.list_predicates()| to list all available predicates. | ||||
|  | ||||
| @@ -923,28 +966,35 @@ register({lang}, {filetype})              *vim.treesitter.language.register()* | ||||
| Lua module: vim.treesitter.query                        *lua-treesitter-query* | ||||
|  | ||||
|                                         *vim.treesitter.query.add_directive()* | ||||
| add_directive({name}, {handler}, {force}) | ||||
| add_directive({name}, {handler}, {opts}) | ||||
|     Adds a new directive to be used in queries | ||||
|  | ||||
|     Handlers can set match level data by setting directly on the metadata | ||||
|     object `metadata.key = value`, additionally, handlers can set node level | ||||
|     object `metadata.key = value`. Additionally, handlers can set node level | ||||
|     data by using the capture id on the metadata table | ||||
|     `metadata[capture_id].key = value` | ||||
|  | ||||
|     Parameters: ~ | ||||
|       • {name}     (`string`) Name of the directive, without leading # | ||||
|       • {handler}  (`function`) | ||||
|                    • match: see |treesitter-query| | ||||
|                      • node-level data are accessible via `match[capture_id]` | ||||
|  | ||||
|                    • pattern: see |treesitter-query| | ||||
|                    • match: A table mapping capture IDs to a list of captured | ||||
|                      nodes | ||||
|                    • pattern: the index of the matching pattern in the query | ||||
|                      file | ||||
|                    • predicate: list of strings containing the full directive | ||||
|                      being called, e.g. `(node (#set! conceal "-"))` would get | ||||
|                      the predicate `{ "#set!", "conceal", "-" }` | ||||
|       • {force}    (`boolean?`) | ||||
|       • {opts}     (`table<string, any>`) Optional options: | ||||
|                    • force (boolean): Override an existing predicate of the | ||||
|                      same name | ||||
|                    • all (boolean): Use the correct implementation of the | ||||
|                      match table where capture IDs map to a list of nodes | ||||
|                      instead of a single node. Defaults to false (for backward | ||||
|                      compatibility). This option will eventually become the | ||||
|                      default and removed. | ||||
|  | ||||
|                                         *vim.treesitter.query.add_predicate()* | ||||
| add_predicate({name}, {handler}, {force}) | ||||
| add_predicate({name}, {handler}, {opts}) | ||||
|     Adds a new predicate to be used in queries | ||||
|  | ||||
|     Parameters: ~ | ||||
| @@ -952,7 +1002,14 @@ add_predicate({name}, {handler}, {force}) | ||||
|       • {handler}  (`function`) | ||||
|                    • see |vim.treesitter.query.add_directive()| for argument | ||||
|                      meanings | ||||
|       • {force}    (`boolean?`) | ||||
|       • {opts}     (`table<string, any>`) Optional options: | ||||
|                    • force (boolean): Override an existing predicate of the | ||||
|                      same name | ||||
|                    • all (boolean): Use the correct implementation of the | ||||
|                      match table where capture IDs map to a list of nodes | ||||
|                      instead of a single node. Defaults to false (for backward | ||||
|                      compatibility). This option will eventually become the | ||||
|                      default and removed. | ||||
|  | ||||
| edit({lang})                                     *vim.treesitter.query.edit()* | ||||
|     Opens a live editor to query the buffer you started from. | ||||
| @@ -1102,18 +1159,25 @@ Query:iter_matches({node}, {source}, {start}, {stop}, {opts}) | ||||
|     Iterate over all matches within a {node}. The arguments are the same as | ||||
|     for |Query:iter_captures()| but the iterated values are different: an | ||||
|     (1-based) index of the pattern in the query, a table mapping capture | ||||
|     indices to nodes, and metadata from any directives processing the match. | ||||
|     If the query has more than one pattern, the capture table might be sparse | ||||
|     and e.g. `pairs()` method should be used over `ipairs`. Here is an example | ||||
|     iterating over all captures in every match: >lua | ||||
|         for pattern, match, metadata in cquery:iter_matches(tree:root(), bufnr, first, last) do | ||||
|           for id, node in pairs(match) do | ||||
|     indices to a list of nodes, and metadata from any directives processing | ||||
|     the match. | ||||
|  | ||||
|     WARNING: Set `all=true` to ensure all matching nodes in a match are | ||||
|     returned, otherwise only the last node in a match is returned, breaking | ||||
|     captures involving quantifiers such as `(comment)+ @comment`. The default | ||||
|     option `all=false` is only provided for backward compatibility and will be | ||||
|     removed after Nvim 0.10. | ||||
|  | ||||
|     Example: >lua | ||||
|         for pattern, match, metadata in cquery:iter_matches(tree:root(), bufnr, 0, -1, { all = true }) do | ||||
|           for id, nodes in pairs(match) do | ||||
|             local name = query.captures[id] | ||||
|             -- `node` was captured by the `name` capture in the match | ||||
|             for _, node in ipairs(nodes) do | ||||
|               -- `node` was captured by the `name` capture in the match | ||||
|  | ||||
|             local node_data = metadata[id] -- Node level metadata | ||||
|  | ||||
|             -- ... use the info here ... | ||||
|               local node_data = metadata[id] -- Node level metadata | ||||
|               ... use the info here ... | ||||
|             end | ||||
|           end | ||||
|         end | ||||
| < | ||||
| @@ -1129,9 +1193,14 @@ Query:iter_matches({node}, {source}, {start}, {stop}, {opts}) | ||||
|                   • max_start_depth (integer) if non-zero, sets the maximum | ||||
|                     start depth for each match. This is used to prevent | ||||
|                     traversing too deep into a tree. | ||||
|                   • all (boolean) When set, the returned match table maps | ||||
|                     capture IDs to a list of nodes. Older versions of | ||||
|                     iter_matches incorrectly mapped capture IDs to a single | ||||
|                     node, which is incorrect behavior. This option will | ||||
|                     eventually become the default and removed. | ||||
|  | ||||
|     Return: ~ | ||||
|         (`fun(): integer, table<integer,TSNode>, table`) pattern id, match, | ||||
|         (`fun(): integer, table<integer, TSNode[]>, table`) pattern id, match, | ||||
|         metadata | ||||
|  | ||||
| set({lang}, {query_name}, {text})                 *vim.treesitter.query.set()* | ||||
|   | ||||
| @@ -39,7 +39,7 @@ local TSNode = {} | ||||
| ---@param start? integer | ||||
| ---@param end_? integer | ||||
| ---@param opts? table | ||||
| ---@return fun(): integer, TSNode, any | ||||
| ---@return fun(): integer, TSNode, TSMatch | ||||
| function TSNode:_rawquery(query, captures, start, end_, opts) end | ||||
|  | ||||
| ---@param query TSQuery | ||||
| @@ -47,7 +47,7 @@ function TSNode:_rawquery(query, captures, start, end_, opts) end | ||||
| ---@param start? integer | ||||
| ---@param end_? integer | ||||
| ---@param opts? table | ||||
| ---@return fun(): integer, any | ||||
| ---@return fun(): integer, TSMatch | ||||
| function TSNode:_rawquery(query, captures, start, end_, opts) end | ||||
|  | ||||
| ---@alias TSLoggerCallback fun(logtype: 'parse'|'lex', msg: string) | ||||
|   | ||||
| @@ -122,7 +122,7 @@ local parse = vim.func._memoize(hash_parse, function(node, buf, lang) | ||||
| end) | ||||
|  | ||||
| --- @param buf integer | ||||
| --- @param match table<integer,TSNode> | ||||
| --- @param match table<integer,TSNode[]> | ||||
| --- @param query Query | ||||
| --- @param lang_context QueryLinterLanguageContext | ||||
| --- @param diagnostics Diagnostic[] | ||||
| @@ -130,20 +130,22 @@ local function lint_match(buf, match, query, lang_context, diagnostics) | ||||
|   local lang = lang_context.lang | ||||
|   local parser_info = lang_context.parser_info | ||||
|  | ||||
|   for id, node in pairs(match) do | ||||
|     local cap_id = query.captures[id] | ||||
|   for id, nodes in pairs(match) do | ||||
|     for _, node in ipairs(nodes) do | ||||
|       local cap_id = query.captures[id] | ||||
|  | ||||
|     -- perform language-independent checks only for first lang | ||||
|     if lang_context.is_first_lang and cap_id == 'error' then | ||||
|       local node_text = vim.treesitter.get_node_text(node, buf):gsub('\n', ' ') | ||||
|       add_lint_for_node(diagnostics, { node:range() }, 'Syntax error: ' .. node_text) | ||||
|     end | ||||
|       -- perform language-independent checks only for first lang | ||||
|       if lang_context.is_first_lang and cap_id == 'error' then | ||||
|         local node_text = vim.treesitter.get_node_text(node, buf):gsub('\n', ' ') | ||||
|         add_lint_for_node(diagnostics, { node:range() }, 'Syntax error: ' .. node_text) | ||||
|       end | ||||
|  | ||||
|     -- other checks rely on Neovim parser introspection | ||||
|     if lang and parser_info and cap_id == 'toplevel' then | ||||
|       local err = parse(node, buf, lang) | ||||
|       if err then | ||||
|         add_lint_for_node(diagnostics, err.range, err.msg, lang) | ||||
|       -- other checks rely on Neovim parser introspection | ||||
|       if lang and parser_info and cap_id == 'toplevel' then | ||||
|         local err = parse(node, buf, lang) | ||||
|         if err then | ||||
|           add_lint_for_node(diagnostics, err.range, err.msg, lang) | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|   | ||||
| @@ -784,7 +784,7 @@ end | ||||
| ---@private | ||||
| --- Extract injections according to: | ||||
| --- https://tree-sitter.github.io/tree-sitter/syntax-highlighting#language-injection | ||||
| ---@param match table<integer,TSNode> | ||||
| ---@param match table<integer,TSNode[]> | ||||
| ---@param metadata TSMetadata | ||||
| ---@return string?, boolean, Range6[] | ||||
| function LanguageTree:_get_injection(match, metadata) | ||||
| @@ -796,14 +796,16 @@ function LanguageTree:_get_injection(match, metadata) | ||||
|     or (injection_lang and resolve_lang(injection_lang)) | ||||
|   local include_children = metadata['injection.include-children'] ~= nil | ||||
|  | ||||
|   for id, node in pairs(match) do | ||||
|     local name = self._injection_query.captures[id] | ||||
|     -- Lang should override any other language tag | ||||
|     if name == 'injection.language' then | ||||
|       local text = vim.treesitter.get_node_text(node, self._source, { metadata = metadata[id] }) | ||||
|       lang = resolve_lang(text) | ||||
|     elseif name == 'injection.content' then | ||||
|       ranges = get_node_ranges(node, self._source, metadata[id], include_children) | ||||
|   for id, nodes in pairs(match) do | ||||
|     for _, node in ipairs(nodes) do | ||||
|       local name = self._injection_query.captures[id] | ||||
|       -- Lang should override any other language tag | ||||
|       if name == 'injection.language' then | ||||
|         local text = vim.treesitter.get_node_text(node, self._source, { metadata = metadata[id] }) | ||||
|         lang = resolve_lang(text) | ||||
|       elseif name == 'injection.content' then | ||||
|         ranges = get_node_ranges(node, self._source, metadata[id], include_children) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | ||||
| @@ -844,7 +846,13 @@ function LanguageTree:_get_injections() | ||||
|     local start_line, _, end_line, _ = root_node:range() | ||||
|  | ||||
|     for pattern, match, metadata in | ||||
|       self._injection_query:iter_matches(root_node, self._source, start_line, end_line + 1) | ||||
|       self._injection_query:iter_matches( | ||||
|         root_node, | ||||
|         self._source, | ||||
|         start_line, | ||||
|         end_line + 1, | ||||
|         { all = true } | ||||
|       ) | ||||
|     do | ||||
|       local lang, combined, ranges = self:_get_injection(match, metadata) | ||||
|       if lang then | ||||
|   | ||||
| @@ -290,47 +290,71 @@ function M.get_node_text(...) | ||||
|   return vim.treesitter.get_node_text(...) | ||||
| end | ||||
|  | ||||
| ---@alias TSMatch table<integer,TSNode> | ||||
|  | ||||
| ---@alias TSPredicate fun(match: TSMatch, _, _, predicate: any[]): boolean | ||||
|  | ||||
| -- Predicate handler receive the following arguments | ||||
| -- (match, pattern, bufnr, predicate) | ||||
| ---@type table<string,TSPredicate> | ||||
| local predicate_handlers = { | ||||
|   ['eq?'] = function(match, _, source, predicate) | ||||
|     local node = match[predicate[2]] | ||||
|     if not node then | ||||
| --- Implementations of predicates that can optionally be prefixed with "any-". | ||||
| --- | ||||
| --- These functions contain the implementations for each predicate, correctly | ||||
| --- handling the "any" vs "all" semantics. They are called from the | ||||
| --- predicate_handlers table with the appropriate arguments for each predicate. | ||||
| local impl = { | ||||
|   --- @param match TSMatch | ||||
|   --- @param source integer|string | ||||
|   --- @param predicate any[] | ||||
|   --- @param any boolean | ||||
|   ['eq'] = function(match, source, predicate, any) | ||||
|     local nodes = match[predicate[2]] | ||||
|     if not nodes or #nodes == 0 then | ||||
|       return true | ||||
|     end | ||||
|     local node_text = vim.treesitter.get_node_text(node, source) | ||||
|  | ||||
|     local str ---@type string | ||||
|     if type(predicate[3]) == 'string' then | ||||
|       -- (#eq? @aa "foo") | ||||
|       str = predicate[3] | ||||
|     else | ||||
|       -- (#eq? @aa @bb) | ||||
|       str = vim.treesitter.get_node_text(match[predicate[3]], source) | ||||
|     for _, node in ipairs(nodes) do | ||||
|       local node_text = vim.treesitter.get_node_text(node, source) | ||||
|  | ||||
|       local str ---@type string | ||||
|       if type(predicate[3]) == 'string' then | ||||
|         -- (#eq? @aa "foo") | ||||
|         str = predicate[3] | ||||
|       else | ||||
|         -- (#eq? @aa @bb) | ||||
|         local other = assert(match[predicate[3]]) | ||||
|         assert(#other == 1, '#eq? does not support comparison with captures on multiple nodes') | ||||
|         str = vim.treesitter.get_node_text(other[1], source) | ||||
|       end | ||||
|  | ||||
|       local res = str ~= nil and node_text == str | ||||
|       if any and res then | ||||
|         return true | ||||
|       elseif not any and not res then | ||||
|         return false | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     if node_text ~= str or str == nil then | ||||
|       return false | ||||
|     end | ||||
|  | ||||
|     return true | ||||
|     return not any | ||||
|   end, | ||||
|  | ||||
|   ['lua-match?'] = function(match, _, source, predicate) | ||||
|     local node = match[predicate[2]] | ||||
|     if not node then | ||||
|   --- @param match TSMatch | ||||
|   --- @param source integer|string | ||||
|   --- @param predicate any[] | ||||
|   --- @param any boolean | ||||
|   ['lua-match'] = function(match, source, predicate, any) | ||||
|     local nodes = match[predicate[2]] | ||||
|     if not nodes or #nodes == 0 then | ||||
|       return true | ||||
|     end | ||||
|     local regex = predicate[3] | ||||
|     return string.find(vim.treesitter.get_node_text(node, source), regex) ~= nil | ||||
|  | ||||
|     for _, node in ipairs(nodes) do | ||||
|       local regex = predicate[3] | ||||
|       local res = string.find(vim.treesitter.get_node_text(node, source), regex) ~= nil | ||||
|       if any and res then | ||||
|         return true | ||||
|       elseif not any and not res then | ||||
|         return false | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     return not any | ||||
|   end, | ||||
|  | ||||
|   ['match?'] = (function() | ||||
|   ['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 | ||||
| @@ -347,27 +371,120 @@ local predicate_handlers = { | ||||
|       end, | ||||
|     }) | ||||
|  | ||||
|     return function(match, _, source, pred) | ||||
|       ---@cast match TSMatch | ||||
|       local node = match[pred[2]] | ||||
|       if not node then | ||||
|     --- @param match TSMatch | ||||
|     --- @param source integer|string | ||||
|     --- @param predicate any[] | ||||
|     --- @param any boolean | ||||
|     return function(match, source, predicate, any) | ||||
|       local nodes = match[predicate[2]] | ||||
|       if not nodes or #nodes == 0 then | ||||
|         return true | ||||
|       end | ||||
|       ---@diagnostic disable-next-line no-unknown | ||||
|       local regex = compiled_vim_regexes[pred[3]] | ||||
|       return regex:match_str(vim.treesitter.get_node_text(node, source)) | ||||
|  | ||||
|       for _, node in ipairs(nodes) do | ||||
|         local regex = compiled_vim_regexes[predicate[3]] ---@type vim.regex | ||||
|         local res = regex:match_str(vim.treesitter.get_node_text(node, source)) | ||||
|         if any and res then | ||||
|           return true | ||||
|         elseif not any and not res then | ||||
|           return false | ||||
|         end | ||||
|       end | ||||
|       return not any | ||||
|     end | ||||
|   end)(), | ||||
|  | ||||
|   ['contains?'] = function(match, _, source, predicate) | ||||
|     local node = match[predicate[2]] | ||||
|     if not node then | ||||
|   --- @param match TSMatch | ||||
|   --- @param source integer|string | ||||
|   --- @param predicate any[] | ||||
|   --- @param any boolean | ||||
|   ['contains'] = function(match, source, predicate, any) | ||||
|     local nodes = match[predicate[2]] | ||||
|     if not nodes or #nodes == 0 then | ||||
|       return true | ||||
|     end | ||||
|     local node_text = vim.treesitter.get_node_text(node, source) | ||||
|  | ||||
|     for i = 3, #predicate do | ||||
|       if string.find(node_text, predicate[i], 1, true) then | ||||
|     for _, node in ipairs(nodes) do | ||||
|       local node_text = vim.treesitter.get_node_text(node, source) | ||||
|  | ||||
|       for i = 3, #predicate do | ||||
|         local res = string.find(node_text, predicate[i], 1, true) | ||||
|         if any and res then | ||||
|           return true | ||||
|         elseif not any and not res then | ||||
|           return false | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     return not any | ||||
|   end, | ||||
| } | ||||
|  | ||||
| ---@class TSMatch | ||||
| ---@field pattern? integer | ||||
| ---@field active? boolean | ||||
| ---@field [integer] TSNode[] | ||||
|  | ||||
| ---@alias TSPredicate fun(match: TSMatch, pattern: integer, source: integer|string, predicate: any[]): boolean | ||||
|  | ||||
| -- Predicate handler receive the following arguments | ||||
| -- (match, pattern, bufnr, predicate) | ||||
| ---@type table<string,TSPredicate> | ||||
| local predicate_handlers = { | ||||
|   ['eq?'] = function(match, _, source, predicate) | ||||
|     return impl['eq'](match, source, predicate, false) | ||||
|   end, | ||||
|  | ||||
|   ['any-eq?'] = function(match, _, source, predicate) | ||||
|     return impl['eq'](match, source, predicate, true) | ||||
|   end, | ||||
|  | ||||
|   ['lua-match?'] = function(match, _, source, predicate) | ||||
|     return impl['lua-match'](match, source, predicate, false) | ||||
|   end, | ||||
|  | ||||
|   ['any-lua-match?'] = function(match, _, source, predicate) | ||||
|     return impl['lua-match'](match, source, predicate, true) | ||||
|   end, | ||||
|  | ||||
|   ['match?'] = function(match, _, source, predicate) | ||||
|     return impl['match'](match, source, predicate, false) | ||||
|   end, | ||||
|  | ||||
|   ['any-match?'] = function(match, _, source, predicate) | ||||
|     return impl['match'](match, source, predicate, true) | ||||
|   end, | ||||
|  | ||||
|   ['contains?'] = function(match, _, source, predicate) | ||||
|     return impl['contains'](match, source, predicate, false) | ||||
|   end, | ||||
|  | ||||
|   ['any-contains?'] = function(match, _, source, predicate) | ||||
|     return impl['contains'](match, source, predicate, true) | ||||
|   end, | ||||
|  | ||||
|   ['any-of?'] = function(match, _, source, predicate) | ||||
|     local nodes = match[predicate[2]] | ||||
|     if not nodes or #nodes == 0 then | ||||
|       return true | ||||
|     end | ||||
|  | ||||
|     for _, node in ipairs(nodes) do | ||||
|       local node_text = vim.treesitter.get_node_text(node, source) | ||||
|  | ||||
|       -- Since 'predicate' will not be used by callers of this function, use it | ||||
|       -- to store a string set built from the list of words to check against. | ||||
|       local string_set = predicate['string_set'] --- @type table<string, boolean> | ||||
|       if not string_set then | ||||
|         string_set = {} | ||||
|         for i = 3, #predicate do | ||||
|           string_set[predicate[i]] = true | ||||
|         end | ||||
|         predicate['string_set'] = string_set | ||||
|       end | ||||
|  | ||||
|       if string_set[node_text] then | ||||
|         return true | ||||
|       end | ||||
|     end | ||||
| @@ -375,57 +492,39 @@ local predicate_handlers = { | ||||
|     return false | ||||
|   end, | ||||
|  | ||||
|   ['any-of?'] = function(match, _, source, predicate) | ||||
|     local node = match[predicate[2]] | ||||
|     if not node then | ||||
|       return true | ||||
|     end | ||||
|     local node_text = vim.treesitter.get_node_text(node, source) | ||||
|  | ||||
|     -- Since 'predicate' will not be used by callers of this function, use it | ||||
|     -- to store a string set built from the list of words to check against. | ||||
|     local string_set = predicate['string_set'] | ||||
|     if not string_set then | ||||
|       string_set = {} | ||||
|       for i = 3, #predicate do | ||||
|         ---@diagnostic disable-next-line:no-unknown | ||||
|         string_set[predicate[i]] = true | ||||
|       end | ||||
|       predicate['string_set'] = string_set | ||||
|     end | ||||
|  | ||||
|     return string_set[node_text] | ||||
|   end, | ||||
|  | ||||
|   ['has-ancestor?'] = function(match, _, _, predicate) | ||||
|     local node = match[predicate[2]] | ||||
|     if not node then | ||||
|     local nodes = match[predicate[2]] | ||||
|     if not nodes or #nodes == 0 then | ||||
|       return true | ||||
|     end | ||||
|  | ||||
|     local ancestor_types = {} | ||||
|     for _, type in ipairs({ unpack(predicate, 3) }) do | ||||
|       ancestor_types[type] = true | ||||
|     end | ||||
|  | ||||
|     node = node:parent() | ||||
|     while node do | ||||
|       if ancestor_types[node:type()] then | ||||
|         return true | ||||
|     for _, node in ipairs(nodes) do | ||||
|       local ancestor_types = {} --- @type table<string, boolean> | ||||
|       for _, type in ipairs({ unpack(predicate, 3) }) do | ||||
|         ancestor_types[type] = true | ||||
|       end | ||||
|  | ||||
|       local cur = node:parent() | ||||
|       while cur do | ||||
|         if ancestor_types[cur:type()] then | ||||
|           return true | ||||
|         end | ||||
|         cur = cur:parent() | ||||
|       end | ||||
|       node = node:parent() | ||||
|     end | ||||
|     return false | ||||
|   end, | ||||
|  | ||||
|   ['has-parent?'] = function(match, _, _, predicate) | ||||
|     local node = match[predicate[2]] | ||||
|     if not node then | ||||
|     local nodes = match[predicate[2]] | ||||
|     if not nodes or #nodes == 0 then | ||||
|       return true | ||||
|     end | ||||
|  | ||||
|     if vim.list_contains({ unpack(predicate, 3) }, node:parent():type()) then | ||||
|       return true | ||||
|     for _, node in ipairs(nodes) do | ||||
|       if vim.list_contains({ unpack(predicate, 3) }, node:parent():type()) then | ||||
|         return true | ||||
|       end | ||||
|     end | ||||
|     return false | ||||
|   end, | ||||
| @@ -433,6 +532,7 @@ local predicate_handlers = { | ||||
|  | ||||
| -- As we provide lua-match? also expose vim-match? | ||||
| predicate_handlers['vim-match?'] = predicate_handlers['match?'] | ||||
| predicate_handlers['any-vim-match?'] = predicate_handlers['any-match?'] | ||||
|  | ||||
| ---@class TSMetadata | ||||
| ---@field range? Range | ||||
| @@ -468,13 +568,17 @@ local directive_handlers = { | ||||
|   -- Shifts the range of a node. | ||||
|   -- Example: (#offset! @_node 0 1 0 -1) | ||||
|   ['offset!'] = function(match, _, _, pred, metadata) | ||||
|     ---@cast pred integer[] | ||||
|     local capture_id = pred[2] | ||||
|     local capture_id = pred[2] --[[@as integer]] | ||||
|     local nodes = match[capture_id] | ||||
|     assert(#nodes == 1, '#offset! does not support captures on multiple nodes') | ||||
|  | ||||
|     local node = nodes[1] | ||||
|  | ||||
|     if not metadata[capture_id] then | ||||
|       metadata[capture_id] = {} | ||||
|     end | ||||
|  | ||||
|     local range = metadata[capture_id].range or { match[capture_id]:range() } | ||||
|     local range = metadata[capture_id].range or { node:range() } | ||||
|     local start_row_offset = pred[3] or 0 | ||||
|     local start_col_offset = pred[4] or 0 | ||||
|     local end_row_offset = pred[5] or 0 | ||||
| @@ -498,7 +602,9 @@ local directive_handlers = { | ||||
|     local id = pred[2] | ||||
|     assert(type(id) == 'number') | ||||
|  | ||||
|     local node = match[id] | ||||
|     local nodes = match[id] | ||||
|     assert(#nodes == 1, '#gsub! does not support captures on multiple nodes') | ||||
|     local node = nodes[1] | ||||
|     local text = vim.treesitter.get_node_text(node, bufnr, { metadata = metadata[id] }) or '' | ||||
|  | ||||
|     if not metadata[id] then | ||||
| @@ -518,10 +624,9 @@ local directive_handlers = { | ||||
|     local capture_id = pred[2] | ||||
|     assert(type(capture_id) == 'number') | ||||
|  | ||||
|     local node = match[capture_id] | ||||
|     if not node then | ||||
|       return | ||||
|     end | ||||
|     local nodes = match[capture_id] | ||||
|     assert(#nodes == 1, '#trim! does not support captures on multiple nodes') | ||||
|     local node = nodes[1] | ||||
|  | ||||
|     local start_row, start_col, end_row, end_col = node:range() | ||||
|  | ||||
| @@ -552,38 +657,93 @@ local directive_handlers = { | ||||
| --- Adds a new predicate to be used in queries | ||||
| --- | ||||
| ---@param name string Name of the predicate, without leading # | ||||
| ---@param handler function(match:table<string,TSNode>, pattern:string, bufnr:integer, predicate:string[]) | ||||
| ---@param handler function(match: table<integer,TSNode[]>, pattern: integer, source: integer|string, predicate: any[], metadata: table) | ||||
| ---   - see |vim.treesitter.query.add_directive()| for argument meanings | ||||
| ---@param force boolean|nil | ||||
| function M.add_predicate(name, handler, force) | ||||
|   if predicate_handlers[name] and not force then | ||||
|     error(string.format('Overriding %s', name)) | ||||
| ---@param opts table<string, any> Optional options: | ||||
| ---                                 - force (boolean): Override an existing | ||||
| ---                                   predicate of the same name | ||||
| ---                                 - all (boolean): Use the correct | ||||
| ---                                   implementation of the match table where | ||||
| ---                                   capture IDs map to a list of nodes instead | ||||
| ---                                   of a single node. Defaults to false (for | ||||
| ---                                   backward compatibility). This option will | ||||
| ---                                   eventually become the default and removed. | ||||
| function M.add_predicate(name, handler, opts) | ||||
|   -- Backward compatibility: old signature had "force" as boolean argument | ||||
|   if type(opts) == 'boolean' then | ||||
|     opts = { force = opts } | ||||
|   end | ||||
|  | ||||
|   predicate_handlers[name] = handler | ||||
|   opts = opts or {} | ||||
|  | ||||
|   if predicate_handlers[name] and not opts.force then | ||||
|     error(string.format('Overriding existing predicate %s', name)) | ||||
|   end | ||||
|  | ||||
|   if opts.all then | ||||
|     predicate_handlers[name] = handler | ||||
|   else | ||||
|     --- @param match table<integer, TSNode[]> | ||||
|     local function wrapper(match, ...) | ||||
|       local m = {} ---@type table<integer, TSNode> | ||||
|       for k, v in pairs(match) do | ||||
|         if type(k) == 'number' then | ||||
|           m[k] = v[#v] | ||||
|         end | ||||
|       end | ||||
|       return handler(m, ...) | ||||
|     end | ||||
|     predicate_handlers[name] = wrapper | ||||
|   end | ||||
| end | ||||
|  | ||||
| --- Adds a new directive to be used in queries | ||||
| --- | ||||
| --- Handlers can set match level data by setting directly on the | ||||
| --- metadata object `metadata.key = value`, additionally, handlers | ||||
| --- metadata object `metadata.key = value`. Additionally, handlers | ||||
| --- can set node level data by using the capture id on the | ||||
| --- metadata table `metadata[capture_id].key = value` | ||||
| --- | ||||
| ---@param name string Name of the directive, without leading # | ||||
| ---@param handler function(match:table<string,TSNode>, pattern:string, bufnr:integer, predicate:string[], metadata:table) | ||||
| ---   - match: see |treesitter-query| | ||||
| ---      - node-level data are accessible via `match[capture_id]` | ||||
| ---   - pattern: see |treesitter-query| | ||||
| ---@param handler function(match: table<integer,TSNode[]>, pattern: integer, source: integer|string, predicate: any[], metadata: table) | ||||
| ---   - match: A table mapping capture IDs to a list of captured nodes | ||||
| ---   - pattern: the index of the matching pattern in the query file | ||||
| ---   - predicate: list of strings containing the full directive being called, e.g. | ||||
| ---     `(node (#set! conceal "-"))` would get the predicate `{ "#set!", "conceal", "-" }` | ||||
| ---@param force boolean|nil | ||||
| function M.add_directive(name, handler, force) | ||||
|   if directive_handlers[name] and not force then | ||||
|     error(string.format('Overriding %s', name)) | ||||
| ---@param opts table<string, any> Optional options: | ||||
| ---                                 - force (boolean): Override an existing | ||||
| ---                                   predicate of the same name | ||||
| ---                                 - all (boolean): Use the correct | ||||
| ---                                   implementation of the match table where | ||||
| ---                                   capture IDs map to a list of nodes instead | ||||
| ---                                   of a single node. Defaults to false (for | ||||
| ---                                   backward compatibility). This option will | ||||
| ---                                   eventually become the default and removed. | ||||
| function M.add_directive(name, handler, opts) | ||||
|   -- Backward compatibility: old signature had "force" as boolean argument | ||||
|   if type(opts) == 'boolean' then | ||||
|     opts = { force = opts } | ||||
|   end | ||||
|  | ||||
|   directive_handlers[name] = handler | ||||
|   opts = opts or {} | ||||
|  | ||||
|   if directive_handlers[name] and not opts.force then | ||||
|     error(string.format('Overriding existing directive %s', name)) | ||||
|   end | ||||
|  | ||||
|   if opts.all then | ||||
|     directive_handlers[name] = handler | ||||
|   else | ||||
|     --- @param match table<integer, TSNode[]> | ||||
|     local function wrapper(match, ...) | ||||
|       local m = {} ---@type table<integer, TSNode> | ||||
|       for k, v in pairs(match) do | ||||
|         m[k] = v[#v] | ||||
|       end | ||||
|       handler(m, ...) | ||||
|     end | ||||
|     directive_handlers[name] = wrapper | ||||
|   end | ||||
| end | ||||
|  | ||||
| --- Lists the currently available directives to use in queries. | ||||
| @@ -608,7 +768,7 @@ end | ||||
|  | ||||
| ---@private | ||||
| ---@param match TSMatch | ||||
| ---@param pattern string | ||||
| ---@param pattern integer | ||||
| ---@param source integer|string | ||||
| function Query:match_preds(match, pattern, source) | ||||
|   local preds = self.info.patterns[pattern] | ||||
| @@ -618,18 +778,14 @@ function Query:match_preds(match, pattern, source) | ||||
|     -- 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. | ||||
|     local pred_name ---@type string | ||||
|  | ||||
|     local is_not ---@type boolean | ||||
|     local is_not = false | ||||
|  | ||||
|     -- Skip over directives... they will get processed after all the predicates. | ||||
|     if not is_directive(pred[1]) then | ||||
|       if string.sub(pred[1], 1, 4) == 'not-' then | ||||
|         pred_name = string.sub(pred[1], 5) | ||||
|       local pred_name = pred[1] | ||||
|       if pred_name:match('^not%-') then | ||||
|         pred_name = pred_name:sub(5) | ||||
|         is_not = true | ||||
|       else | ||||
|         pred_name = pred[1] | ||||
|         is_not = false | ||||
|       end | ||||
|  | ||||
|       local handler = predicate_handlers[pred_name] | ||||
| @@ -724,7 +880,7 @@ function Query:iter_captures(node, source, start, stop) | ||||
|  | ||||
|   start, stop = value_or_node_range(start, stop, node) | ||||
|  | ||||
|   local raw_iter = node:_rawquery(self.query, true, start, stop) | ||||
|   local raw_iter = node:_rawquery(self.query, true, start, stop) ---@type fun(): integer, TSNode, TSMatch | ||||
|   local function iter(end_line) | ||||
|     local capture, captured_node, match = raw_iter() | ||||
|     local metadata = {} | ||||
| @@ -748,27 +904,34 @@ end | ||||
|  | ||||
| --- Iterates the matches of self on a given range. | ||||
| --- | ||||
| --- Iterate over all matches within a {node}. The arguments are the same as | ||||
| --- for |Query:iter_captures()| but the iterated values are different: | ||||
| --- an (1-based) index of the pattern in the query, a table mapping | ||||
| --- capture indices to nodes, and metadata from any directives processing the match. | ||||
| --- If the query has more than one pattern, the capture table might be sparse | ||||
| --- and e.g. `pairs()` method should be used over `ipairs`. | ||||
| --- Here is an example iterating over all captures in every match: | ||||
| --- Iterate over all matches within a {node}. The arguments are the same as for | ||||
| --- |Query:iter_captures()| but the iterated values are different: an (1-based) | ||||
| --- index of the pattern in the query, a table mapping capture indices to a list | ||||
| --- of nodes, and metadata from any directives processing the match. | ||||
| --- | ||||
| --- WARNING: Set `all=true` to ensure all matching nodes in a match are | ||||
| --- returned, otherwise only the last node in a match is returned, breaking captures | ||||
| --- involving quantifiers such as `(comment)+ @comment`. The default option | ||||
| --- `all=false` is only provided for backward compatibility and will be removed | ||||
| --- after Nvim 0.10. | ||||
| --- | ||||
| --- Example: | ||||
| --- | ||||
| --- ```lua | ||||
| --- for pattern, match, metadata in cquery:iter_matches(tree:root(), bufnr, first, last) do | ||||
| ---   for id, node in pairs(match) do | ||||
| --- for pattern, match, metadata in cquery:iter_matches(tree:root(), bufnr, 0, -1, { all = true }) do | ||||
| ---   for id, nodes in pairs(match) do | ||||
| ---     local name = query.captures[id] | ||||
| ---     -- `node` was captured by the `name` capture in the match | ||||
| ---     for _, node in ipairs(nodes) do | ||||
| ---       -- `node` was captured by the `name` capture in the match | ||||
| --- | ||||
| ---     local node_data = metadata[id] -- Node level metadata | ||||
| --- | ||||
| ---     -- ... use the info here ... | ||||
| ---       local node_data = metadata[id] -- Node level metadata | ||||
| ---       ... use the info here ... | ||||
| ---     end | ||||
| ---   end | ||||
| --- end | ||||
| --- ``` | ||||
| --- | ||||
| --- | ||||
| ---@param node TSNode under which the search will occur | ||||
| ---@param source (integer|string) Source buffer or string to search | ||||
| ---@param start? integer Starting line for the search. Defaults to `node:start()`. | ||||
| @@ -776,17 +939,20 @@ end | ||||
| ---@param opts? table Optional keyword arguments: | ||||
| ---   - max_start_depth (integer) if non-zero, sets the maximum start depth | ||||
| ---     for each match. This is used to prevent traversing too deep into a tree. | ||||
| ---   - all (boolean) When set, the returned match table maps capture IDs to a list of nodes. | ||||
| ---     Older versions of iter_matches incorrectly mapped capture IDs to a single node, which is | ||||
| ---     incorrect behavior. This option will eventually become the default and removed. | ||||
| --- | ||||
| ---@return (fun(): integer, table<integer,TSNode>, table): pattern id, match, metadata | ||||
| ---@return (fun(): integer, table<integer, TSNode[]>, table): pattern id, match, metadata | ||||
| function Query:iter_matches(node, source, start, stop, opts) | ||||
|   local all = opts and opts.all | ||||
|   if type(source) == 'number' and source == 0 then | ||||
|     source = api.nvim_get_current_buf() | ||||
|   end | ||||
|  | ||||
|   start, stop = value_or_node_range(start, stop, node) | ||||
|  | ||||
|   local raw_iter = node:_rawquery(self.query, false, start, stop, opts) | ||||
|   ---@cast raw_iter fun(): string, any | ||||
|   local raw_iter = node:_rawquery(self.query, false, start, stop, opts) ---@type fun(): integer, TSMatch | ||||
|   local function iter() | ||||
|     local pattern, match = raw_iter() | ||||
|     local metadata = {} | ||||
| @@ -799,6 +965,18 @@ function Query:iter_matches(node, source, start, stop, opts) | ||||
|  | ||||
|       self:apply_directives(match, pattern, source, metadata) | ||||
|     end | ||||
|  | ||||
|     if not all then | ||||
|       -- Convert the match table into the old buggy version for backward | ||||
|       -- compatibility. This is slow. Plugin authors, if you're reading this, set the "all" | ||||
|       -- option! | ||||
|       local old_match = {} ---@type table<integer, TSNode> | ||||
|       for k, v in pairs(match or {}) do | ||||
|         old_match[k] = v[#v] | ||||
|       end | ||||
|       return pattern, old_match, metadata | ||||
|     end | ||||
|  | ||||
|     return pattern, match, metadata | ||||
|   end | ||||
|   return iter | ||||
|   | ||||
| @@ -1364,9 +1364,16 @@ static int node_equal(lua_State *L) | ||||
| /// assumes the match table being on top of the stack | ||||
| static void set_match(lua_State *L, TSQueryMatch *match, int nodeidx) | ||||
| { | ||||
|   for (int i = 0; i < match->capture_count; i++) { | ||||
|     push_node(L, match->captures[i].node, nodeidx); | ||||
|     lua_rawseti(L, -2, (int)match->captures[i].index + 1); | ||||
|   // [match] | ||||
|   for (size_t i = 0; i < match->capture_count; i++) { | ||||
|     lua_rawgeti(L, -1, (int)match->captures[i].index + 1);  // [match, captures] | ||||
|     if (lua_isnil(L, -1)) {  // [match, nil] | ||||
|       lua_pop(L, 1);  // [match] | ||||
|       lua_createtable(L, 1, 0);  // [match, captures] | ||||
|     } | ||||
|     push_node(L, match->captures[i].node, nodeidx);  // [match, captures, node] | ||||
|     lua_rawseti(L, -2, (int)lua_objlen(L, -2) + 1);  // [match, captures] | ||||
|     lua_rawseti(L, -2, (int)match->captures[i].index + 1);  // [match] | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -1379,7 +1386,7 @@ static int query_next_match(lua_State *L) | ||||
|   TSQueryMatch match; | ||||
|   if (ts_query_cursor_next_match(cursor, &match)) { | ||||
|     lua_pushinteger(L, match.pattern_index + 1);  // [index] | ||||
|     lua_createtable(L, (int)ts_query_capture_count(query), 2);  // [index, match] | ||||
|     lua_createtable(L, (int)ts_query_capture_count(query), 0);  // [index, match] | ||||
|     set_match(L, &match, lua_upvalueindex(2)); | ||||
|     return 2; | ||||
|   } | ||||
| @@ -1421,7 +1428,8 @@ static int query_next_capture(lua_State *L) | ||||
|     if (n_pred > 0 && (ud->max_match_id < (int)match.id)) { | ||||
|       ud->max_match_id = (int)match.id; | ||||
|  | ||||
|       lua_pushvalue(L, lua_upvalueindex(4));  // [index, node, match] | ||||
|       // Create a new cleared match table | ||||
|       lua_createtable(L, (int)ts_query_capture_count(query), 2);  // [index, node, match] | ||||
|       set_match(L, &match, lua_upvalueindex(2)); | ||||
|       lua_pushinteger(L, match.pattern_index + 1); | ||||
|       lua_setfield(L, -2, "pattern"); | ||||
| @@ -1431,6 +1439,10 @@ static int query_next_capture(lua_State *L) | ||||
|         lua_pushboolean(L, false); | ||||
|         lua_setfield(L, -2, "active"); | ||||
|       } | ||||
|  | ||||
|       // Set current_match to the new match | ||||
|       lua_replace(L, lua_upvalueindex(4));  // [index, node] | ||||
|       lua_pushvalue(L, lua_upvalueindex(4));  // [index, node, match] | ||||
|       return 3; | ||||
|     } | ||||
|     return 2; | ||||
|   | ||||
| @@ -731,6 +731,31 @@ describe('treesitter highlighting (C)', function() | ||||
|     eq(3, get_hl '@foo.missing.exists.bar') | ||||
|     eq(nil, get_hl '@total.nonsense.but.a.lot.of.dots') | ||||
|   end) | ||||
|  | ||||
|   it('supports multiple nodes assigned to the same capture #17060', function() | ||||
|     insert([[ | ||||
|       int x = 4; | ||||
|       int y = 5; | ||||
|       int z = 6; | ||||
|     ]]) | ||||
|  | ||||
|     exec_lua([[ | ||||
|       local query = '((declaration)+ @string)' | ||||
|       vim.treesitter.query.set('c', 'highlights', query) | ||||
|       vim.treesitter.highlighter.new(vim.treesitter.get_parser(0, 'c')) | ||||
|     ]]) | ||||
|  | ||||
|     screen:expect { | ||||
|       grid = [[ | ||||
|         {5:int x = 4;}                                                     | | ||||
|         {5:int y = 5;}                                                     | | ||||
|         {5:int z = 6;}                                                     | | ||||
|       ^                                                                 | | ||||
|       {1:~                                                                }|*13 | ||||
|                                                                        | | ||||
|     ]], | ||||
|     } | ||||
|   end) | ||||
| end) | ||||
|  | ||||
| describe('treesitter highlighting (help)', function() | ||||
|   | ||||
| @@ -8,6 +8,8 @@ local exec_lua = helpers.exec_lua | ||||
| local pcall_err = helpers.pcall_err | ||||
| local feed = helpers.feed | ||||
| local is_os = helpers.is_os | ||||
| local api = helpers.api | ||||
| local fn = helpers.fn | ||||
|  | ||||
| describe('treesitter parser API', function() | ||||
|   before_each(function() | ||||
| @@ -171,7 +173,7 @@ void ui_refresh(void) | ||||
|     assert(res_fail) | ||||
|   end) | ||||
|  | ||||
|   local query = [[ | ||||
|   local test_query = [[ | ||||
|     ((call_expression function: (identifier) @minfunc (argument_list (identifier) @min_id)) (eq? @minfunc "MIN")) | ||||
|     "for" @keyword | ||||
|     (primitive_type) @type | ||||
| @@ -187,7 +189,7 @@ void ui_refresh(void) | ||||
|   end) | ||||
|  | ||||
|   it('supports caching queries', function() | ||||
|     local long_query = query:rep(100) | ||||
|     local long_query = test_query:rep(100) | ||||
|     local function q(n) | ||||
|       return exec_lua( | ||||
|         [[ | ||||
| @@ -230,7 +232,7 @@ void ui_refresh(void) | ||||
|       end | ||||
|       return res | ||||
|     ]], | ||||
|       query | ||||
|       test_query | ||||
|     ) | ||||
|  | ||||
|     eq({ | ||||
| @@ -256,17 +258,19 @@ void ui_refresh(void) | ||||
|       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 | ||||
|       for pattern, match in cquery:iter_matches(tree:root(), 0, 7, 14, { all = true }) 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()}) | ||||
|         for cid, nodes in pairs(match) do | ||||
|           for _, node in ipairs(nodes) do | ||||
|             table.insert(mrepr, {cquery.captures[cid], node:type(), node:range()}) | ||||
|           end | ||||
|         end | ||||
|         table.insert(res, {pattern, mrepr}) | ||||
|       end | ||||
|       return res | ||||
|     ]], | ||||
|       query | ||||
|       test_query | ||||
|     ) | ||||
|  | ||||
|     eq({ | ||||
| @@ -287,6 +291,67 @@ void ui_refresh(void) | ||||
|     }, res) | ||||
|   end) | ||||
|  | ||||
|   it('support query and iter by capture for quantifiers', 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 | ||||
|     ]], | ||||
|       '(expression_statement (assignment_expression (call_expression)))+ @funccall' | ||||
|     ) | ||||
|  | ||||
|     eq({ | ||||
|       { 'funccall', 'expression_statement', 11, 4, 11, 34 }, | ||||
|       { 'funccall', 'expression_statement', 12, 4, 12, 37 }, | ||||
|       { 'funccall', 'expression_statement', 13, 4, 13, 34 }, | ||||
|     }, res) | ||||
|   end) | ||||
|  | ||||
|   it('support query and iter by match for quantifiers', 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, { all = true }) do | ||||
|         -- can't transmit node over RPC. just check the name and range | ||||
|         local mrepr = {} | ||||
|         for cid, nodes in pairs(match) do | ||||
|           for _, node in ipairs(nodes) do | ||||
|             table.insert(mrepr, {cquery.captures[cid], node:type(), node:range()}) | ||||
|           end | ||||
|         end | ||||
|         table.insert(res, {pattern, mrepr}) | ||||
|       end | ||||
|       return res | ||||
|     ]], | ||||
|       '(expression_statement (assignment_expression (call_expression)))+ @funccall' | ||||
|     ) | ||||
|  | ||||
|     eq({ | ||||
|       { | ||||
|         1, | ||||
|         { | ||||
|           { 'funccall', 'expression_statement', 11, 4, 11, 34 }, | ||||
|           { 'funccall', 'expression_statement', 12, 4, 12, 37 }, | ||||
|           { 'funccall', 'expression_statement', 13, 4, 13, 34 }, | ||||
|         }, | ||||
|       }, | ||||
|     }, res) | ||||
|   end) | ||||
|  | ||||
|   it('supports getting text of multiline node', function() | ||||
|     insert(test_text) | ||||
|     local res = exec_lua([[ | ||||
| @@ -365,11 +430,13 @@ end]] | ||||
|       parser = vim.treesitter.get_parser(0, "c") | ||||
|       tree = parser:parse()[1] | ||||
|       res = {} | ||||
|       for pattern, match in cquery:iter_matches(tree:root(), 0) do | ||||
|       for pattern, match in cquery:iter_matches(tree:root(), 0, 0, -1, { all = true }) 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()}) | ||||
|         for cid, nodes in pairs(match) do | ||||
|           for _, node in ipairs(nodes) do | ||||
|             table.insert(mrepr, {cquery.captures[cid], node:type(), node:range()}) | ||||
|           end | ||||
|         end | ||||
|         table.insert(res, {pattern, mrepr}) | ||||
|       end | ||||
| @@ -457,11 +524,13 @@ end]] | ||||
|       parser = vim.treesitter.get_parser(0, "c") | ||||
|       tree = parser:parse()[1] | ||||
|       res = {} | ||||
|       for pattern, match in cquery:iter_matches(tree:root(), 0) do | ||||
|       for pattern, match in cquery:iter_matches(tree:root(), 0, 0, -1, { all = true }) 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()}) | ||||
|         for cid, nodes in pairs(match) do | ||||
|           for _, node in ipairs(nodes) do | ||||
|             table.insert(mrepr, {cquery.captures[cid], node:type(), node:range()}) | ||||
|           end | ||||
|         end | ||||
|         table.insert(res, {pattern, mrepr}) | ||||
|       end | ||||
| @@ -486,55 +555,248 @@ end]] | ||||
|  | ||||
|     local custom_query = '((identifier) @main (#is-main? @main))' | ||||
|  | ||||
|     local res = exec_lua( | ||||
|       [[ | ||||
|     local query = vim.treesitter.query | ||||
|     do | ||||
|       local res = exec_lua( | ||||
|         [[ | ||||
|       local query = vim.treesitter.query | ||||
|  | ||||
|     local function is_main(match, pattern, bufnr, predicate) | ||||
|       local node = match[ predicate[2] ] | ||||
|       local function is_main(match, pattern, bufnr, predicate) | ||||
|         local nodes = match[ predicate[2] ] | ||||
|         for _, node in ipairs(nodes) do | ||||
|           if query.get_node_text(node, bufnr) == 'main' then | ||||
|             return true | ||||
|           end | ||||
|         end | ||||
|         return false | ||||
|       end | ||||
|  | ||||
|       return query.get_node_text(node, bufnr) | ||||
|       local parser = vim.treesitter.get_parser(0, "c") | ||||
|  | ||||
|       -- Time bomb: update this in 0.12 | ||||
|       if vim.fn.has('nvim-0.12') == 1 then | ||||
|         return 'Update this test to remove this message and { all = true } from add_predicate' | ||||
|       end | ||||
|       query.add_predicate("is-main?", is_main, { all = true }) | ||||
|  | ||||
|       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) | ||||
|     end | ||||
|  | ||||
|     local parser = vim.treesitter.get_parser(0, "c") | ||||
|     -- Once with the old API. Remove this whole 'do' block in 0.12 | ||||
|     do | ||||
|       local res = exec_lua( | ||||
|         [[ | ||||
|       local query = vim.treesitter.query | ||||
|  | ||||
|     query.add_predicate("is-main?", is_main) | ||||
|       local function is_main(match, pattern, bufnr, predicate) | ||||
|         local node = match[ predicate[2] ] | ||||
|  | ||||
|     local query = query.parse("c", ...) | ||||
|         return query.get_node_text(node, bufnr) == 'main' | ||||
|       end | ||||
|  | ||||
|     local nodes = {} | ||||
|     for _, node in query:iter_captures(parser:parse()[1]:root(), 0) do | ||||
|       table.insert(nodes, {node:range()}) | ||||
|       local parser = vim.treesitter.get_parser(0, "c") | ||||
|  | ||||
|       query.add_predicate("is-main?", is_main, true) | ||||
|  | ||||
|       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 | ||||
|       ) | ||||
|  | ||||
|       -- Remove this 'do' block in 0.12 | ||||
|       eq(0, fn.has('nvim-0.12')) | ||||
|       eq({ { 0, 4, 0, 8 } }, res) | ||||
|     end | ||||
|  | ||||
|     return nodes | ||||
|     ]], | ||||
|       custom_query | ||||
|     ) | ||||
|     do | ||||
|       local res = exec_lua [[ | ||||
|         local query = vim.treesitter.query | ||||
|  | ||||
|     eq({ { 0, 4, 0, 8 } }, res) | ||||
|         local t = {} | ||||
|         for _, v in ipairs(query.list_predicates()) do | ||||
|           t[v] = true | ||||
|         end | ||||
|  | ||||
|     local res_list = exec_lua [[ | ||||
|     local query = vim.treesitter.query | ||||
|         return t | ||||
|       ]] | ||||
|  | ||||
|     local list = query.list_predicates() | ||||
|       eq(true, res['is-main?']) | ||||
|     end | ||||
|   end) | ||||
|  | ||||
|     table.sort(list) | ||||
|  | ||||
|     return list | ||||
|   it('supports "all" and "any" semantics for predicates on quantified captures #24738', function() | ||||
|     local query_all = [[ | ||||
|       (((comment (comment_content))+) @bar | ||||
|         (#lua-match? @bar "Yes")) | ||||
|     ]] | ||||
|  | ||||
|     local query_any = [[ | ||||
|       (((comment (comment_content))+) @bar | ||||
|         (#any-lua-match? @bar "Yes")) | ||||
|     ]] | ||||
|  | ||||
|     local function test(input, query) | ||||
|       api.nvim_buf_set_lines(0, 0, -1, true, vim.split(dedent(input), '\n')) | ||||
|       return exec_lua( | ||||
|         [[ | ||||
|         local parser = vim.treesitter.get_parser(0, "lua") | ||||
|         local query = vim.treesitter.query.parse("lua", ...) | ||||
|         local nodes = {} | ||||
|         for _, node in query:iter_captures(parser:parse()[1]:root(), 0) do | ||||
|           nodes[#nodes+1] = { node:range() } | ||||
|         end | ||||
|         return nodes | ||||
|       ]], | ||||
|         query | ||||
|       ) | ||||
|     end | ||||
|  | ||||
|     eq( | ||||
|       {}, | ||||
|       test( | ||||
|         [[ | ||||
|       -- Yes | ||||
|       -- No | ||||
|       -- Yes | ||||
|     ]], | ||||
|         query_all | ||||
|       ) | ||||
|     ) | ||||
|  | ||||
|     eq( | ||||
|       { | ||||
|         { 0, 2, 0, 8 }, | ||||
|         { 1, 2, 1, 8 }, | ||||
|         { 2, 2, 2, 8 }, | ||||
|       }, | ||||
|       test( | ||||
|         [[ | ||||
|       -- Yes | ||||
|       -- Yes | ||||
|       -- Yes | ||||
|     ]], | ||||
|         query_all | ||||
|       ) | ||||
|     ) | ||||
|  | ||||
|     eq( | ||||
|       {}, | ||||
|       test( | ||||
|         [[ | ||||
|       -- No | ||||
|       -- No | ||||
|       -- No | ||||
|     ]], | ||||
|         query_any | ||||
|       ) | ||||
|     ) | ||||
|  | ||||
|     eq( | ||||
|       { | ||||
|         { 0, 2, 0, 7 }, | ||||
|         { 1, 2, 1, 8 }, | ||||
|         { 2, 2, 2, 7 }, | ||||
|       }, | ||||
|       test( | ||||
|         [[ | ||||
|       -- No | ||||
|       -- Yes | ||||
|       -- No | ||||
|     ]], | ||||
|         query_any | ||||
|       ) | ||||
|     ) | ||||
|   end) | ||||
|  | ||||
|   it('supports any- prefix to match any capture when using quantifiers #24738', function() | ||||
|     insert([[ | ||||
|       -- Comment | ||||
|       -- Comment | ||||
|       -- Comment | ||||
|     ]]) | ||||
|  | ||||
|     local query = [[ | ||||
|       (((comment (comment_content))+) @bar | ||||
|         (#lua-match? @bar "Comment")) | ||||
|     ]] | ||||
|  | ||||
|     local result = exec_lua( | ||||
|       [[ | ||||
|       local parser = vim.treesitter.get_parser(0, "lua") | ||||
|       local query = vim.treesitter.query.parse("lua", ...) | ||||
|       local nodes = {} | ||||
|       for _, node in query:iter_captures(parser:parse()[1]:root(), 0) do | ||||
|         nodes[#nodes+1] = { node:range() } | ||||
|       end | ||||
|       return nodes | ||||
|     ]], | ||||
|       query | ||||
|     ) | ||||
|  | ||||
|     eq({ | ||||
|       'any-of?', | ||||
|       'contains?', | ||||
|       'eq?', | ||||
|       'has-ancestor?', | ||||
|       'has-parent?', | ||||
|       'is-main?', | ||||
|       'lua-match?', | ||||
|       'match?', | ||||
|       'vim-match?', | ||||
|     }, res_list) | ||||
|       { 0, 2, 0, 12 }, | ||||
|       { 1, 2, 1, 12 }, | ||||
|       { 2, 2, 2, 12 }, | ||||
|     }, result) | ||||
|   end) | ||||
|  | ||||
|   it('supports the old broken version of iter_matches #24738', function() | ||||
|     -- Delete this test in 0.12 when iter_matches is removed | ||||
|     eq(0, fn.has('nvim-0.12')) | ||||
|  | ||||
|     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 | ||||
|         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 | ||||
|     ]], | ||||
|       test_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('allows to set simple ranges', function() | ||||
| @@ -866,7 +1128,7 @@ int x = INT_MAX; | ||||
|         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 | ||||
|         for pattern, match, metadata in query:iter_matches(parser:parse()[1]:root(), 0, 0, -1, { all = true }) do | ||||
|           result = metadata.key | ||||
|         end | ||||
|  | ||||
| @@ -889,7 +1151,7 @@ int x = INT_MAX; | ||||
|           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 pattern, match, metadata in query:iter_matches(parser:parse()[1]:root(), 0, 0, -1, { all = true }) do | ||||
|             for _, nested_tbl in pairs(metadata) do | ||||
|               return nested_tbl.key | ||||
|             end | ||||
| @@ -911,7 +1173,7 @@ int x = INT_MAX; | ||||
|           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 pattern, match, metadata in query:iter_matches(parser:parse()[1]:root(), 0, 0, -1, { all = true }) do | ||||
|             for _, nested_tbl in pairs(metadata) do | ||||
|               return nested_tbl | ||||
|             end | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Thomas Vigouroux
					Thomas Vigouroux