mirror of
				https://github.com/neovim/neovim.git
				synced 2025-10-26 12:27:24 +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) | • |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* | REMOVED FEATURES                                                 *news-removed* | ||||||
|  |  | ||||||
| @@ -480,7 +492,7 @@ release. | |||||||
|  |  | ||||||
| • `vim.loop` has been renamed to |vim.uv|. | • `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. |   - |LanguageTree:for_each_child()|	Use |LanguageTree:children()| (non-recursive) instead. | ||||||
|  |  | ||||||
| • The "term_background" UI option |ui-ext-options| is deprecated and no longer | • 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")) |             ((identifier) @variable.builtin (#eq? @variable.builtin "self")) | ||||||
|             ((node1) @left (node2) @right (#eq? @left @right)) |             ((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?* |     `match?`                                      *treesitter-predicate-match?* | ||||||
|     `vim-match?`                              *treesitter-predicate-vim-match?* |     `vim-match?`                              *treesitter-predicate-vim-match?* | ||||||
|          Match a |regexp| against the text corresponding to a node: >query |          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 |          Note: The `^` and `$` anchors will match the start and end of the | ||||||
|                node's text. |                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?* |     `lua-match?`                              *treesitter-predicate-lua-match?* | ||||||
|          Match |lua-patterns| against the text corresponding to a node, |          Match |lua-patterns| against the text corresponding to a node, | ||||||
|          similar to `match?` |          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?* |     `contains?`                                *treesitter-predicate-contains?* | ||||||
|         Match a string against parts of the text corresponding to a node: >query |         Match a string against parts of the text corresponding to a node: >query | ||||||
|             ((identifier) @foo (#contains? @foo "foo")) |             ((identifier) @foo (#contains? @foo "foo")) | ||||||
|             ((identifier) @foo-bar (#contains? @foo-bar "foo" "bar")) |             ((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?* |     `any-of?`                                    *treesitter-predicate-any-of?* | ||||||
|         Match any of the given strings against the text corresponding to |         Match any of the given strings against the text corresponding to | ||||||
|         a node: >query |         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 | Each predicate has a `not-` prefixed predicate that is just the negation of | ||||||
| the predicate. | 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()|. | Further predicates can be added via |vim.treesitter.query.add_predicate()|. | ||||||
| Use |vim.treesitter.query.list_predicates()| to list all available predicates. | 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* | Lua module: vim.treesitter.query                        *lua-treesitter-query* | ||||||
|  |  | ||||||
|                                         *vim.treesitter.query.add_directive()* |                                         *vim.treesitter.query.add_directive()* | ||||||
| add_directive({name}, {handler}, {force}) | add_directive({name}, {handler}, {opts}) | ||||||
|     Adds a new directive to be used in queries |     Adds a new directive to be used in queries | ||||||
|  |  | ||||||
|     Handlers can set match level data by setting directly on the metadata |     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 |     data by using the capture id on the metadata table | ||||||
|     `metadata[capture_id].key = value` |     `metadata[capture_id].key = value` | ||||||
|  |  | ||||||
|     Parameters: ~ |     Parameters: ~ | ||||||
|       • {name}     (`string`) Name of the directive, without leading # |       • {name}     (`string`) Name of the directive, without leading # | ||||||
|       • {handler}  (`function`) |       • {handler}  (`function`) | ||||||
|                    • match: see |treesitter-query| |                    • match: A table mapping capture IDs to a list of captured | ||||||
|                      • node-level data are accessible via `match[capture_id]` |                      nodes | ||||||
|  |                    • pattern: the index of the matching pattern in the query | ||||||
|                    • pattern: see |treesitter-query| |                      file | ||||||
|                    • predicate: list of strings containing the full directive |                    • predicate: list of strings containing the full directive | ||||||
|                      being called, e.g. `(node (#set! conceal "-"))` would get |                      being called, e.g. `(node (#set! conceal "-"))` would get | ||||||
|                      the predicate `{ "#set!", "conceal", "-" }` |                      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()* |                                         *vim.treesitter.query.add_predicate()* | ||||||
| add_predicate({name}, {handler}, {force}) | add_predicate({name}, {handler}, {opts}) | ||||||
|     Adds a new predicate to be used in queries |     Adds a new predicate to be used in queries | ||||||
|  |  | ||||||
|     Parameters: ~ |     Parameters: ~ | ||||||
| @@ -952,7 +1002,14 @@ add_predicate({name}, {handler}, {force}) | |||||||
|       • {handler}  (`function`) |       • {handler}  (`function`) | ||||||
|                    • see |vim.treesitter.query.add_directive()| for argument |                    • see |vim.treesitter.query.add_directive()| for argument | ||||||
|                      meanings |                      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()* | edit({lang})                                     *vim.treesitter.query.edit()* | ||||||
|     Opens a live editor to query the buffer you started from. |     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 |     Iterate over all matches within a {node}. The arguments are the same as | ||||||
|     for |Query:iter_captures()| but the iterated values are different: an |     for |Query:iter_captures()| but the iterated values are different: an | ||||||
|     (1-based) index of the pattern in the query, a table mapping capture |     (1-based) index of the pattern in the query, a table mapping capture | ||||||
|     indices to nodes, and metadata from any directives processing the match. |     indices to a list of nodes, and metadata from any directives processing | ||||||
|     If the query has more than one pattern, the capture table might be sparse |     the match. | ||||||
|     and e.g. `pairs()` method should be used over `ipairs`. Here is an example |  | ||||||
|     iterating over all captures in every match: >lua |     WARNING: Set `all=true` to ensure all matching nodes in a match are | ||||||
|         for pattern, match, metadata in cquery:iter_matches(tree:root(), bufnr, first, last) do |     returned, otherwise only the last node in a match is returned, breaking | ||||||
|           for id, node in pairs(match) do |     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] |             local name = query.captures[id] | ||||||
|  |             for _, node in ipairs(nodes) do | ||||||
|               -- `node` was captured by the `name` capture in the match |               -- `node` was captured by the `name` capture in the match | ||||||
|  |  | ||||||
|               local node_data = metadata[id] -- Node level metadata |               local node_data = metadata[id] -- Node level metadata | ||||||
|  |               ... use the info here ... | ||||||
|             -- ... use the info here ... |             end | ||||||
|           end |           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 |                   • max_start_depth (integer) if non-zero, sets the maximum | ||||||
|                     start depth for each match. This is used to prevent |                     start depth for each match. This is used to prevent | ||||||
|                     traversing too deep into a tree. |                     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: ~ |     Return: ~ | ||||||
|         (`fun(): integer, table<integer,TSNode>, table`) pattern id, match, |         (`fun(): integer, table<integer, TSNode[]>, table`) pattern id, match, | ||||||
|         metadata |         metadata | ||||||
|  |  | ||||||
| set({lang}, {query_name}, {text})                 *vim.treesitter.query.set()* | set({lang}, {query_name}, {text})                 *vim.treesitter.query.set()* | ||||||
|   | |||||||
| @@ -39,7 +39,7 @@ local TSNode = {} | |||||||
| ---@param start? integer | ---@param start? integer | ||||||
| ---@param end_? integer | ---@param end_? integer | ||||||
| ---@param opts? table | ---@param opts? table | ||||||
| ---@return fun(): integer, TSNode, any | ---@return fun(): integer, TSNode, TSMatch | ||||||
| function TSNode:_rawquery(query, captures, start, end_, opts) end | function TSNode:_rawquery(query, captures, start, end_, opts) end | ||||||
|  |  | ||||||
| ---@param query TSQuery | ---@param query TSQuery | ||||||
| @@ -47,7 +47,7 @@ function TSNode:_rawquery(query, captures, start, end_, opts) end | |||||||
| ---@param start? integer | ---@param start? integer | ||||||
| ---@param end_? integer | ---@param end_? integer | ||||||
| ---@param opts? table | ---@param opts? table | ||||||
| ---@return fun(): integer, any | ---@return fun(): integer, TSMatch | ||||||
| function TSNode:_rawquery(query, captures, start, end_, opts) end | function TSNode:_rawquery(query, captures, start, end_, opts) end | ||||||
|  |  | ||||||
| ---@alias TSLoggerCallback fun(logtype: 'parse'|'lex', msg: string) | ---@alias TSLoggerCallback fun(logtype: 'parse'|'lex', msg: string) | ||||||
|   | |||||||
| @@ -122,7 +122,7 @@ local parse = vim.func._memoize(hash_parse, function(node, buf, lang) | |||||||
| end) | end) | ||||||
|  |  | ||||||
| --- @param buf integer | --- @param buf integer | ||||||
| --- @param match table<integer,TSNode> | --- @param match table<integer,TSNode[]> | ||||||
| --- @param query Query | --- @param query Query | ||||||
| --- @param lang_context QueryLinterLanguageContext | --- @param lang_context QueryLinterLanguageContext | ||||||
| --- @param diagnostics Diagnostic[] | --- @param diagnostics Diagnostic[] | ||||||
| @@ -130,7 +130,8 @@ local function lint_match(buf, match, query, lang_context, diagnostics) | |||||||
|   local lang = lang_context.lang |   local lang = lang_context.lang | ||||||
|   local parser_info = lang_context.parser_info |   local parser_info = lang_context.parser_info | ||||||
|  |  | ||||||
|   for id, node in pairs(match) do |   for id, nodes in pairs(match) do | ||||||
|  |     for _, node in ipairs(nodes) do | ||||||
|       local cap_id = query.captures[id] |       local cap_id = query.captures[id] | ||||||
|  |  | ||||||
|       -- perform language-independent checks only for first lang |       -- perform language-independent checks only for first lang | ||||||
| @@ -147,6 +148,7 @@ local function lint_match(buf, match, query, lang_context, diagnostics) | |||||||
|         end |         end | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|  |   end | ||||||
| end | end | ||||||
|  |  | ||||||
| --- @private | --- @private | ||||||
|   | |||||||
| @@ -784,7 +784,7 @@ end | |||||||
| ---@private | ---@private | ||||||
| --- Extract injections according to: | --- Extract injections according to: | ||||||
| --- https://tree-sitter.github.io/tree-sitter/syntax-highlighting#language-injection | --- https://tree-sitter.github.io/tree-sitter/syntax-highlighting#language-injection | ||||||
| ---@param match table<integer,TSNode> | ---@param match table<integer,TSNode[]> | ||||||
| ---@param metadata TSMetadata | ---@param metadata TSMetadata | ||||||
| ---@return string?, boolean, Range6[] | ---@return string?, boolean, Range6[] | ||||||
| function LanguageTree:_get_injection(match, metadata) | function LanguageTree:_get_injection(match, metadata) | ||||||
| @@ -796,7 +796,8 @@ function LanguageTree:_get_injection(match, metadata) | |||||||
|     or (injection_lang and resolve_lang(injection_lang)) |     or (injection_lang and resolve_lang(injection_lang)) | ||||||
|   local include_children = metadata['injection.include-children'] ~= nil |   local include_children = metadata['injection.include-children'] ~= nil | ||||||
|  |  | ||||||
|   for id, node in pairs(match) do |   for id, nodes in pairs(match) do | ||||||
|  |     for _, node in ipairs(nodes) do | ||||||
|       local name = self._injection_query.captures[id] |       local name = self._injection_query.captures[id] | ||||||
|       -- Lang should override any other language tag |       -- Lang should override any other language tag | ||||||
|       if name == 'injection.language' then |       if name == 'injection.language' then | ||||||
| @@ -806,6 +807,7 @@ function LanguageTree:_get_injection(match, metadata) | |||||||
|         ranges = get_node_ranges(node, self._source, metadata[id], include_children) |         ranges = get_node_ranges(node, self._source, metadata[id], include_children) | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|   return lang, combined, ranges |   return lang, combined, ranges | ||||||
| end | end | ||||||
| @@ -844,7 +846,13 @@ function LanguageTree:_get_injections() | |||||||
|     local start_line, _, end_line, _ = root_node:range() |     local start_line, _, end_line, _ = root_node:range() | ||||||
|  |  | ||||||
|     for pattern, match, metadata in |     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 |     do | ||||||
|       local lang, combined, ranges = self:_get_injection(match, metadata) |       local lang, combined, ranges = self:_get_injection(match, metadata) | ||||||
|       if lang then |       if lang then | ||||||
|   | |||||||
| @@ -290,19 +290,23 @@ function M.get_node_text(...) | |||||||
|   return vim.treesitter.get_node_text(...) |   return vim.treesitter.get_node_text(...) | ||||||
| end | end | ||||||
|  |  | ||||||
| ---@alias TSMatch table<integer,TSNode> | --- Implementations of predicates that can optionally be prefixed with "any-". | ||||||
|  | --- | ||||||
| ---@alias TSPredicate fun(match: TSMatch, _, _, predicate: any[]): boolean | --- These functions contain the implementations for each predicate, correctly | ||||||
|  | --- handling the "any" vs "all" semantics. They are called from the | ||||||
| -- Predicate handler receive the following arguments | --- predicate_handlers table with the appropriate arguments for each predicate. | ||||||
| -- (match, pattern, bufnr, predicate) | local impl = { | ||||||
| ---@type table<string,TSPredicate> |   --- @param match TSMatch | ||||||
| local predicate_handlers = { |   --- @param source integer|string | ||||||
|   ['eq?'] = function(match, _, source, predicate) |   --- @param predicate any[] | ||||||
|     local node = match[predicate[2]] |   --- @param any boolean | ||||||
|     if not node then |   ['eq'] = function(match, source, predicate, any) | ||||||
|  |     local nodes = match[predicate[2]] | ||||||
|  |     if not nodes or #nodes == 0 then | ||||||
|       return true |       return true | ||||||
|     end |     end | ||||||
|  |  | ||||||
|  |     for _, node in ipairs(nodes) do | ||||||
|       local node_text = vim.treesitter.get_node_text(node, source) |       local node_text = vim.treesitter.get_node_text(node, source) | ||||||
|  |  | ||||||
|       local str ---@type string |       local str ---@type string | ||||||
| @@ -311,26 +315,46 @@ local predicate_handlers = { | |||||||
|         str = predicate[3] |         str = predicate[3] | ||||||
|       else |       else | ||||||
|         -- (#eq? @aa @bb) |         -- (#eq? @aa @bb) | ||||||
|       str = vim.treesitter.get_node_text(match[predicate[3]], source) |         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 |       end | ||||||
|  |  | ||||||
|     if node_text ~= str or str == nil then |       local res = str ~= nil and node_text == str | ||||||
|  |       if any and res then | ||||||
|  |         return true | ||||||
|  |       elseif not any and not res then | ||||||
|         return false |         return false | ||||||
|       end |       end | ||||||
|  |     end | ||||||
|  |  | ||||||
|     return true |     return not any | ||||||
|   end, |   end, | ||||||
|  |  | ||||||
|   ['lua-match?'] = function(match, _, source, predicate) |   --- @param match TSMatch | ||||||
|     local node = match[predicate[2]] |   --- @param source integer|string | ||||||
|     if not node then |   --- @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 |       return true | ||||||
|     end |     end | ||||||
|  |  | ||||||
|  |     for _, node in ipairs(nodes) do | ||||||
|       local regex = predicate[3] |       local regex = predicate[3] | ||||||
|     return string.find(vim.treesitter.get_node_text(node, source), regex) ~= nil |       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, |   end, | ||||||
|  |  | ||||||
|   ['match?'] = (function() |   ['match'] = (function() | ||||||
|     local magic_prefixes = { ['\\v'] = true, ['\\m'] = true, ['\\M'] = true, ['\\V'] = true } |     local magic_prefixes = { ['\\v'] = true, ['\\m'] = true, ['\\M'] = true, ['\\V'] = true } | ||||||
|     local function check_magic(str) |     local function check_magic(str) | ||||||
|       if string.len(str) < 2 or magic_prefixes[string.sub(str, 1, 2)] then |       if string.len(str) < 2 or magic_prefixes[string.sub(str, 1, 2)] then | ||||||
| @@ -347,92 +371,168 @@ local predicate_handlers = { | |||||||
|       end, |       end, | ||||||
|     }) |     }) | ||||||
|  |  | ||||||
|     return function(match, _, source, pred) |     --- @param match TSMatch | ||||||
|       ---@cast match TSMatch |     --- @param source integer|string | ||||||
|       local node = match[pred[2]] |     --- @param predicate any[] | ||||||
|       if not node then |     --- @param any boolean | ||||||
|  |     return function(match, source, predicate, any) | ||||||
|  |       local nodes = match[predicate[2]] | ||||||
|  |       if not nodes or #nodes == 0 then | ||||||
|         return true |         return true | ||||||
|       end |       end | ||||||
|       ---@diagnostic disable-next-line no-unknown |  | ||||||
|       local regex = compiled_vim_regexes[pred[3]] |       for _, node in ipairs(nodes) do | ||||||
|       return regex:match_str(vim.treesitter.get_node_text(node, source)) |         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 | ||||||
|   end)(), |   end)(), | ||||||
|  |  | ||||||
|   ['contains?'] = function(match, _, source, predicate) |   --- @param match TSMatch | ||||||
|     local node = match[predicate[2]] |   --- @param source integer|string | ||||||
|     if not node then |   --- @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 |       return true | ||||||
|     end |     end | ||||||
|  |  | ||||||
|  |     for _, node in ipairs(nodes) do | ||||||
|       local node_text = vim.treesitter.get_node_text(node, source) |       local node_text = vim.treesitter.get_node_text(node, source) | ||||||
|  |  | ||||||
|       for i = 3, #predicate do |       for i = 3, #predicate do | ||||||
|       if string.find(node_text, predicate[i], 1, true) then |         local res = string.find(node_text, predicate[i], 1, true) | ||||||
|  |         if any and res then | ||||||
|           return true |           return true | ||||||
|  |         elseif not any and not res then | ||||||
|  |           return false | ||||||
|  |         end | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|  |  | ||||||
|     return false |     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, |   end, | ||||||
|  |  | ||||||
|   ['any-of?'] = function(match, _, source, predicate) |   ['any-of?'] = function(match, _, source, predicate) | ||||||
|     local node = match[predicate[2]] |     local nodes = match[predicate[2]] | ||||||
|     if not node then |     if not nodes or #nodes == 0 then | ||||||
|       return true |       return true | ||||||
|     end |     end | ||||||
|  |  | ||||||
|  |     for _, node in ipairs(nodes) do | ||||||
|       local node_text = vim.treesitter.get_node_text(node, source) |       local node_text = vim.treesitter.get_node_text(node, source) | ||||||
|  |  | ||||||
|       -- Since 'predicate' will not be used by callers of this function, use it |       -- 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. |       -- to store a string set built from the list of words to check against. | ||||||
|     local string_set = predicate['string_set'] |       local string_set = predicate['string_set'] --- @type table<string, boolean> | ||||||
|       if not string_set then |       if not string_set then | ||||||
|         string_set = {} |         string_set = {} | ||||||
|         for i = 3, #predicate do |         for i = 3, #predicate do | ||||||
|         ---@diagnostic disable-next-line:no-unknown |  | ||||||
|           string_set[predicate[i]] = true |           string_set[predicate[i]] = true | ||||||
|         end |         end | ||||||
|         predicate['string_set'] = string_set |         predicate['string_set'] = string_set | ||||||
|       end |       end | ||||||
|  |  | ||||||
|     return string_set[node_text] |       if string_set[node_text] then | ||||||
|  |         return true | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     return false | ||||||
|   end, |   end, | ||||||
|  |  | ||||||
|   ['has-ancestor?'] = function(match, _, _, predicate) |   ['has-ancestor?'] = function(match, _, _, predicate) | ||||||
|     local node = match[predicate[2]] |     local nodes = match[predicate[2]] | ||||||
|     if not node then |     if not nodes or #nodes == 0 then | ||||||
|       return true |       return true | ||||||
|     end |     end | ||||||
|  |  | ||||||
|     local ancestor_types = {} |     for _, node in ipairs(nodes) do | ||||||
|  |       local ancestor_types = {} --- @type table<string, boolean> | ||||||
|       for _, type in ipairs({ unpack(predicate, 3) }) do |       for _, type in ipairs({ unpack(predicate, 3) }) do | ||||||
|         ancestor_types[type] = true |         ancestor_types[type] = true | ||||||
|       end |       end | ||||||
|  |  | ||||||
|     node = node:parent() |       local cur = node:parent() | ||||||
|     while node do |       while cur do | ||||||
|       if ancestor_types[node:type()] then |         if ancestor_types[cur:type()] then | ||||||
|           return true |           return true | ||||||
|         end |         end | ||||||
|       node = node:parent() |         cur = cur:parent() | ||||||
|  |       end | ||||||
|     end |     end | ||||||
|     return false |     return false | ||||||
|   end, |   end, | ||||||
|  |  | ||||||
|   ['has-parent?'] = function(match, _, _, predicate) |   ['has-parent?'] = function(match, _, _, predicate) | ||||||
|     local node = match[predicate[2]] |     local nodes = match[predicate[2]] | ||||||
|     if not node then |     if not nodes or #nodes == 0 then | ||||||
|       return true |       return true | ||||||
|     end |     end | ||||||
|  |  | ||||||
|  |     for _, node in ipairs(nodes) do | ||||||
|       if vim.list_contains({ unpack(predicate, 3) }, node:parent():type()) then |       if vim.list_contains({ unpack(predicate, 3) }, node:parent():type()) then | ||||||
|         return true |         return true | ||||||
|       end |       end | ||||||
|  |     end | ||||||
|     return false |     return false | ||||||
|   end, |   end, | ||||||
| } | } | ||||||
|  |  | ||||||
| -- As we provide lua-match? also expose vim-match? | -- As we provide lua-match? also expose vim-match? | ||||||
| predicate_handlers['vim-match?'] = predicate_handlers['match?'] | predicate_handlers['vim-match?'] = predicate_handlers['match?'] | ||||||
|  | predicate_handlers['any-vim-match?'] = predicate_handlers['any-match?'] | ||||||
|  |  | ||||||
| ---@class TSMetadata | ---@class TSMetadata | ||||||
| ---@field range? Range | ---@field range? Range | ||||||
| @@ -468,13 +568,17 @@ local directive_handlers = { | |||||||
|   -- Shifts the range of a node. |   -- Shifts the range of a node. | ||||||
|   -- Example: (#offset! @_node 0 1 0 -1) |   -- Example: (#offset! @_node 0 1 0 -1) | ||||||
|   ['offset!'] = function(match, _, _, pred, metadata) |   ['offset!'] = function(match, _, _, pred, metadata) | ||||||
|     ---@cast pred integer[] |     local capture_id = pred[2] --[[@as integer]] | ||||||
|     local capture_id = pred[2] |     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 |     if not metadata[capture_id] then | ||||||
|       metadata[capture_id] = {} |       metadata[capture_id] = {} | ||||||
|     end |     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_row_offset = pred[3] or 0 | ||||||
|     local start_col_offset = pred[4] or 0 |     local start_col_offset = pred[4] or 0 | ||||||
|     local end_row_offset = pred[5] or 0 |     local end_row_offset = pred[5] or 0 | ||||||
| @@ -498,7 +602,9 @@ local directive_handlers = { | |||||||
|     local id = pred[2] |     local id = pred[2] | ||||||
|     assert(type(id) == 'number') |     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 '' |     local text = vim.treesitter.get_node_text(node, bufnr, { metadata = metadata[id] }) or '' | ||||||
|  |  | ||||||
|     if not metadata[id] then |     if not metadata[id] then | ||||||
| @@ -518,10 +624,9 @@ local directive_handlers = { | |||||||
|     local capture_id = pred[2] |     local capture_id = pred[2] | ||||||
|     assert(type(capture_id) == 'number') |     assert(type(capture_id) == 'number') | ||||||
|  |  | ||||||
|     local node = match[capture_id] |     local nodes = match[capture_id] | ||||||
|     if not node then |     assert(#nodes == 1, '#trim! does not support captures on multiple nodes') | ||||||
|       return |     local node = nodes[1] | ||||||
|     end |  | ||||||
|  |  | ||||||
|     local start_row, start_col, end_row, end_col = node:range() |     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 | --- Adds a new predicate to be used in queries | ||||||
| --- | --- | ||||||
| ---@param name string Name of the predicate, without leading # | ---@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 | ---   - see |vim.treesitter.query.add_directive()| for argument meanings | ||||||
| ---@param force boolean|nil | ---@param opts table<string, any> Optional options: | ||||||
| function M.add_predicate(name, handler, force) | ---                                 - force (boolean): Override an existing | ||||||
|   if predicate_handlers[name] and not force then | ---                                   predicate of the same name | ||||||
|     error(string.format('Overriding %s', 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 |   end | ||||||
|  |  | ||||||
|  |   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 |     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 | end | ||||||
|  |  | ||||||
| --- Adds a new directive to be used in queries | --- Adds a new directive to be used in queries | ||||||
| --- | --- | ||||||
| --- Handlers can set match level data by setting directly on the | --- 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 | --- can set node level data by using the capture id on the | ||||||
| --- metadata table `metadata[capture_id].key = value` | --- metadata table `metadata[capture_id].key = value` | ||||||
| --- | --- | ||||||
| ---@param name string Name of the directive, without leading # | ---@param name string Name of the directive, without leading # | ||||||
| ---@param handler function(match:table<string,TSNode>, pattern:string, bufnr:integer, predicate:string[], metadata:table) | ---@param handler function(match: table<integer,TSNode[]>, pattern: integer, source: integer|string, predicate: any[], metadata: table) | ||||||
| ---   - match: see |treesitter-query| | ---   - match: A table mapping capture IDs to a list of captured nodes | ||||||
| ---      - node-level data are accessible via `match[capture_id]` | ---   - pattern: the index of the matching pattern in the query file | ||||||
| ---   - pattern: see |treesitter-query| |  | ||||||
| ---   - predicate: list of strings containing the full directive being called, e.g. | ---   - predicate: list of strings containing the full directive being called, e.g. | ||||||
| ---     `(node (#set! conceal "-"))` would get the predicate `{ "#set!", "conceal", "-" }` | ---     `(node (#set! conceal "-"))` would get the predicate `{ "#set!", "conceal", "-" }` | ||||||
| ---@param force boolean|nil | ---@param opts table<string, any> Optional options: | ||||||
| function M.add_directive(name, handler, force) | ---                                 - force (boolean): Override an existing | ||||||
|   if directive_handlers[name] and not force then | ---                                   predicate of the same name | ||||||
|     error(string.format('Overriding %s', 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 |   end | ||||||
|  |  | ||||||
|  |   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 |     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 | end | ||||||
|  |  | ||||||
| --- Lists the currently available directives to use in queries. | --- Lists the currently available directives to use in queries. | ||||||
| @@ -608,7 +768,7 @@ end | |||||||
|  |  | ||||||
| ---@private | ---@private | ||||||
| ---@param match TSMatch | ---@param match TSMatch | ||||||
| ---@param pattern string | ---@param pattern integer | ||||||
| ---@param source integer|string | ---@param source integer|string | ||||||
| function Query:match_preds(match, pattern, source) | function Query:match_preds(match, pattern, source) | ||||||
|   local preds = self.info.patterns[pattern] |   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, |     -- continue on the other case. This way unknown predicates will not be considered, | ||||||
|     -- which allows some testing and easier user extensibility (#12173). |     -- which allows some testing and easier user extensibility (#12173). | ||||||
|     -- Also, tree-sitter strips the leading # from predicates for us. |     -- Also, tree-sitter strips the leading # from predicates for us. | ||||||
|     local pred_name ---@type string |     local is_not = false | ||||||
|  |  | ||||||
|     local is_not ---@type boolean |  | ||||||
|  |  | ||||||
|     -- Skip over directives... they will get processed after all the predicates. |     -- Skip over directives... they will get processed after all the predicates. | ||||||
|     if not is_directive(pred[1]) then |     if not is_directive(pred[1]) then | ||||||
|       if string.sub(pred[1], 1, 4) == 'not-' then |       local pred_name = pred[1] | ||||||
|         pred_name = string.sub(pred[1], 5) |       if pred_name:match('^not%-') then | ||||||
|  |         pred_name = pred_name:sub(5) | ||||||
|         is_not = true |         is_not = true | ||||||
|       else |  | ||||||
|         pred_name = pred[1] |  | ||||||
|         is_not = false |  | ||||||
|       end |       end | ||||||
|  |  | ||||||
|       local handler = predicate_handlers[pred_name] |       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) |   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 function iter(end_line) | ||||||
|     local capture, captured_node, match = raw_iter() |     local capture, captured_node, match = raw_iter() | ||||||
|     local metadata = {} |     local metadata = {} | ||||||
| @@ -748,27 +904,34 @@ end | |||||||
|  |  | ||||||
| --- Iterates the matches of self on a given range. | --- Iterates the matches of self on a given range. | ||||||
| --- | --- | ||||||
| --- 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 | ||||||
| --- for |Query:iter_captures()| but the iterated values are different: | --- |Query:iter_captures()| but the iterated values are different: an (1-based) | ||||||
| --- an (1-based) index of the pattern in the query, a table mapping | --- index of the pattern in the query, a table mapping capture indices to a list | ||||||
| --- capture indices to nodes, and metadata from any directives processing the match. | --- of 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`. | --- WARNING: Set `all=true` to ensure all matching nodes in a match are | ||||||
| --- Here is an example iterating over all captures in every match: | --- 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 | --- ```lua | ||||||
| --- for pattern, match, metadata in cquery:iter_matches(tree:root(), bufnr, first, last) do | --- for pattern, match, metadata in cquery:iter_matches(tree:root(), bufnr, 0, -1, { all = true }) do | ||||||
| ---   for id, node in pairs(match) do | ---   for id, nodes in pairs(match) do | ||||||
| ---     local name = query.captures[id] | ---     local name = query.captures[id] | ||||||
|  | ---     for _, node in ipairs(nodes) do | ||||||
| ---       -- `node` was captured by the `name` capture in the match | ---       -- `node` was captured by the `name` capture in the match | ||||||
| --- | --- | ||||||
| ---       local node_data = metadata[id] -- Node level metadata | ---       local node_data = metadata[id] -- Node level metadata | ||||||
| --- | ---       ... use the info here ... | ||||||
| ---     -- ... use the info here ... | ---     end | ||||||
| ---   end | ---   end | ||||||
| --- end | --- end | ||||||
| --- ``` | --- ``` | ||||||
| --- | --- | ||||||
|  | --- | ||||||
| ---@param node TSNode under which the search will occur | ---@param node TSNode under which the search will occur | ||||||
| ---@param source (integer|string) Source buffer or string to search | ---@param source (integer|string) Source buffer or string to search | ||||||
| ---@param start? integer Starting line for the search. Defaults to `node:start()`. | ---@param start? integer Starting line for the search. Defaults to `node:start()`. | ||||||
| @@ -776,17 +939,20 @@ end | |||||||
| ---@param opts? table Optional keyword arguments: | ---@param opts? table Optional keyword arguments: | ||||||
| ---   - max_start_depth (integer) if non-zero, sets the maximum start depth | ---   - 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. | ---     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) | function Query:iter_matches(node, source, start, stop, opts) | ||||||
|  |   local all = opts and opts.all | ||||||
|   if type(source) == 'number' and source == 0 then |   if type(source) == 'number' and source == 0 then | ||||||
|     source = api.nvim_get_current_buf() |     source = api.nvim_get_current_buf() | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   start, stop = value_or_node_range(start, stop, node) |   start, stop = value_or_node_range(start, stop, node) | ||||||
|  |  | ||||||
|   local raw_iter = node:_rawquery(self.query, false, start, stop, opts) |   local raw_iter = node:_rawquery(self.query, false, start, stop, opts) ---@type fun(): integer, TSMatch | ||||||
|   ---@cast raw_iter fun(): string, any |  | ||||||
|   local function iter() |   local function iter() | ||||||
|     local pattern, match = raw_iter() |     local pattern, match = raw_iter() | ||||||
|     local metadata = {} |     local metadata = {} | ||||||
| @@ -799,6 +965,18 @@ function Query:iter_matches(node, source, start, stop, opts) | |||||||
|  |  | ||||||
|       self:apply_directives(match, pattern, source, metadata) |       self:apply_directives(match, pattern, source, metadata) | ||||||
|     end |     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 |     return pattern, match, metadata | ||||||
|   end |   end | ||||||
|   return iter |   return iter | ||||||
|   | |||||||
| @@ -1364,9 +1364,16 @@ static int node_equal(lua_State *L) | |||||||
| /// assumes the match table being on top of the stack | /// assumes the match table being on top of the stack | ||||||
| static void set_match(lua_State *L, TSQueryMatch *match, int nodeidx) | static void set_match(lua_State *L, TSQueryMatch *match, int nodeidx) | ||||||
| { | { | ||||||
|   for (int i = 0; i < match->capture_count; i++) { |   // [match] | ||||||
|     push_node(L, match->captures[i].node, nodeidx); |   for (size_t i = 0; i < match->capture_count; i++) { | ||||||
|     lua_rawseti(L, -2, (int)match->captures[i].index + 1); |     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; |   TSQueryMatch match; | ||||||
|   if (ts_query_cursor_next_match(cursor, &match)) { |   if (ts_query_cursor_next_match(cursor, &match)) { | ||||||
|     lua_pushinteger(L, match.pattern_index + 1);  // [index] |     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)); |     set_match(L, &match, lua_upvalueindex(2)); | ||||||
|     return 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)) { |     if (n_pred > 0 && (ud->max_match_id < (int)match.id)) { | ||||||
|       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)); |       set_match(L, &match, lua_upvalueindex(2)); | ||||||
|       lua_pushinteger(L, match.pattern_index + 1); |       lua_pushinteger(L, match.pattern_index + 1); | ||||||
|       lua_setfield(L, -2, "pattern"); |       lua_setfield(L, -2, "pattern"); | ||||||
| @@ -1431,6 +1439,10 @@ static int query_next_capture(lua_State *L) | |||||||
|         lua_pushboolean(L, false); |         lua_pushboolean(L, false); | ||||||
|         lua_setfield(L, -2, "active"); |         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 3; | ||||||
|     } |     } | ||||||
|     return 2; |     return 2; | ||||||
|   | |||||||
| @@ -731,6 +731,31 @@ describe('treesitter highlighting (C)', function() | |||||||
|     eq(3, get_hl '@foo.missing.exists.bar') |     eq(3, get_hl '@foo.missing.exists.bar') | ||||||
|     eq(nil, get_hl '@total.nonsense.but.a.lot.of.dots') |     eq(nil, get_hl '@total.nonsense.but.a.lot.of.dots') | ||||||
|   end) |   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) | end) | ||||||
|  |  | ||||||
| describe('treesitter highlighting (help)', function() | describe('treesitter highlighting (help)', function() | ||||||
|   | |||||||
| @@ -8,6 +8,8 @@ local exec_lua = helpers.exec_lua | |||||||
| local pcall_err = helpers.pcall_err | local pcall_err = helpers.pcall_err | ||||||
| local feed = helpers.feed | local feed = helpers.feed | ||||||
| local is_os = helpers.is_os | local is_os = helpers.is_os | ||||||
|  | local api = helpers.api | ||||||
|  | local fn = helpers.fn | ||||||
|  |  | ||||||
| describe('treesitter parser API', function() | describe('treesitter parser API', function() | ||||||
|   before_each(function() |   before_each(function() | ||||||
| @@ -171,7 +173,7 @@ void ui_refresh(void) | |||||||
|     assert(res_fail) |     assert(res_fail) | ||||||
|   end) |   end) | ||||||
|  |  | ||||||
|   local query = [[ |   local test_query = [[ | ||||||
|     ((call_expression function: (identifier) @minfunc (argument_list (identifier) @min_id)) (eq? @minfunc "MIN")) |     ((call_expression function: (identifier) @minfunc (argument_list (identifier) @min_id)) (eq? @minfunc "MIN")) | ||||||
|     "for" @keyword |     "for" @keyword | ||||||
|     (primitive_type) @type |     (primitive_type) @type | ||||||
| @@ -187,7 +189,7 @@ void ui_refresh(void) | |||||||
|   end) |   end) | ||||||
|  |  | ||||||
|   it('supports caching queries', function() |   it('supports caching queries', function() | ||||||
|     local long_query = query:rep(100) |     local long_query = test_query:rep(100) | ||||||
|     local function q(n) |     local function q(n) | ||||||
|       return exec_lua( |       return exec_lua( | ||||||
|         [[ |         [[ | ||||||
| @@ -230,7 +232,7 @@ void ui_refresh(void) | |||||||
|       end |       end | ||||||
|       return res |       return res | ||||||
|     ]], |     ]], | ||||||
|       query |       test_query | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     eq({ |     eq({ | ||||||
| @@ -256,17 +258,19 @@ void ui_refresh(void) | |||||||
|       parser = vim.treesitter.get_parser(0, "c") |       parser = vim.treesitter.get_parser(0, "c") | ||||||
|       tree = parser:parse()[1] |       tree = parser:parse()[1] | ||||||
|       res = {} |       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 |         -- can't transmit node over RPC. just check the name and range | ||||||
|         local mrepr = {} |         local mrepr = {} | ||||||
|         for cid,node in pairs(match) do |         for cid, nodes in pairs(match) do | ||||||
|  |           for _, node in ipairs(nodes) do | ||||||
|             table.insert(mrepr, {cquery.captures[cid], node:type(), node:range()}) |             table.insert(mrepr, {cquery.captures[cid], node:type(), node:range()}) | ||||||
|           end |           end | ||||||
|  |         end | ||||||
|         table.insert(res, {pattern, mrepr}) |         table.insert(res, {pattern, mrepr}) | ||||||
|       end |       end | ||||||
|       return res |       return res | ||||||
|     ]], |     ]], | ||||||
|       query |       test_query | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     eq({ |     eq({ | ||||||
| @@ -287,6 +291,67 @@ void ui_refresh(void) | |||||||
|     }, res) |     }, res) | ||||||
|   end) |   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() |   it('supports getting text of multiline node', function() | ||||||
|     insert(test_text) |     insert(test_text) | ||||||
|     local res = exec_lua([[ |     local res = exec_lua([[ | ||||||
| @@ -365,12 +430,14 @@ end]] | |||||||
|       parser = vim.treesitter.get_parser(0, "c") |       parser = vim.treesitter.get_parser(0, "c") | ||||||
|       tree = parser:parse()[1] |       tree = parser:parse()[1] | ||||||
|       res = {} |       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 |         -- can't transmit node over RPC. just check the name and range | ||||||
|         local mrepr = {} |         local mrepr = {} | ||||||
|         for cid,node in pairs(match) do |         for cid, nodes in pairs(match) do | ||||||
|  |           for _, node in ipairs(nodes) do | ||||||
|             table.insert(mrepr, {cquery.captures[cid], node:type(), node:range()}) |             table.insert(mrepr, {cquery.captures[cid], node:type(), node:range()}) | ||||||
|           end |           end | ||||||
|  |         end | ||||||
|         table.insert(res, {pattern, mrepr}) |         table.insert(res, {pattern, mrepr}) | ||||||
|       end |       end | ||||||
|       return res |       return res | ||||||
| @@ -457,12 +524,14 @@ end]] | |||||||
|       parser = vim.treesitter.get_parser(0, "c") |       parser = vim.treesitter.get_parser(0, "c") | ||||||
|       tree = parser:parse()[1] |       tree = parser:parse()[1] | ||||||
|       res = {} |       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 |         -- can't transmit node over RPC. just check the name and range | ||||||
|         local mrepr = {} |         local mrepr = {} | ||||||
|         for cid,node in pairs(match) do |         for cid, nodes in pairs(match) do | ||||||
|  |           for _, node in ipairs(nodes) do | ||||||
|             table.insert(mrepr, {cquery.captures[cid], node:type(), node:range()}) |             table.insert(mrepr, {cquery.captures[cid], node:type(), node:range()}) | ||||||
|           end |           end | ||||||
|  |         end | ||||||
|         table.insert(res, {pattern, mrepr}) |         table.insert(res, {pattern, mrepr}) | ||||||
|       end |       end | ||||||
|       return res |       return res | ||||||
| @@ -486,19 +555,28 @@ end]] | |||||||
|  |  | ||||||
|     local custom_query = '((identifier) @main (#is-main? @main))' |     local custom_query = '((identifier) @main (#is-main? @main))' | ||||||
|  |  | ||||||
|  |     do | ||||||
|       local res = exec_lua( |       local res = exec_lua( | ||||||
|         [[ |         [[ | ||||||
|       local query = vim.treesitter.query |       local query = vim.treesitter.query | ||||||
|  |  | ||||||
|       local function is_main(match, pattern, bufnr, predicate) |       local function is_main(match, pattern, bufnr, predicate) | ||||||
|       local node = match[ predicate[2] ] |         local nodes = match[ predicate[2] ] | ||||||
|  |         for _, node in ipairs(nodes) do | ||||||
|       return query.get_node_text(node, bufnr) |           if query.get_node_text(node, bufnr) == 'main' then | ||||||
|  |             return true | ||||||
|  |           end | ||||||
|  |         end | ||||||
|  |         return false | ||||||
|       end |       end | ||||||
|  |  | ||||||
|       local parser = vim.treesitter.get_parser(0, "c") |       local parser = vim.treesitter.get_parser(0, "c") | ||||||
|  |  | ||||||
|     query.add_predicate("is-main?", is_main) |       -- 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 query = query.parse("c", ...) | ||||||
|  |  | ||||||
| @@ -513,28 +591,212 @@ end]] | |||||||
|       ) |       ) | ||||||
|  |  | ||||||
|       eq({ { 0, 4, 0, 8 } }, res) |       eq({ { 0, 4, 0, 8 } }, res) | ||||||
|  |     end | ||||||
|  |  | ||||||
|     local res_list = exec_lua [[ |     -- Once with the old API. Remove this whole 'do' block in 0.12 | ||||||
|  |     do | ||||||
|  |       local res = exec_lua( | ||||||
|  |         [[ | ||||||
|       local query = vim.treesitter.query |       local query = vim.treesitter.query | ||||||
|  |  | ||||||
|     local list = query.list_predicates() |       local function is_main(match, pattern, bufnr, predicate) | ||||||
|  |         local node = match[ predicate[2] ] | ||||||
|  |  | ||||||
|     table.sort(list) |         return query.get_node_text(node, bufnr) == 'main' | ||||||
|  |       end | ||||||
|  |  | ||||||
|     return list |       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 | ||||||
|  |  | ||||||
|  |     do | ||||||
|  |       local res = exec_lua [[ | ||||||
|  |         local query = vim.treesitter.query | ||||||
|  |  | ||||||
|  |         local t = {} | ||||||
|  |         for _, v in ipairs(query.list_predicates()) do | ||||||
|  |           t[v] = true | ||||||
|  |         end | ||||||
|  |  | ||||||
|  |         return t | ||||||
|       ]] |       ]] | ||||||
|  |  | ||||||
|  |       eq(true, res['is-main?']) | ||||||
|  |     end | ||||||
|  |   end) | ||||||
|  |  | ||||||
|  |   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({ |     eq({ | ||||||
|       'any-of?', |       { 0, 2, 0, 12 }, | ||||||
|       'contains?', |       { 1, 2, 1, 12 }, | ||||||
|       'eq?', |       { 2, 2, 2, 12 }, | ||||||
|       'has-ancestor?', |     }, result) | ||||||
|       'has-parent?', |   end) | ||||||
|       'is-main?', |  | ||||||
|       'lua-match?', |   it('supports the old broken version of iter_matches #24738', function() | ||||||
|       'match?', |     -- Delete this test in 0.12 when iter_matches is removed | ||||||
|       'vim-match?', |     eq(0, fn.has('nvim-0.12')) | ||||||
|     }, res_list) |  | ||||||
|  |     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) |   end) | ||||||
|  |  | ||||||
|   it('allows to set simple ranges', function() |   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"))') |         query = vim.treesitter.query.parse("c", '((number_literal) @number (#set! "key" "value"))') | ||||||
|         parser = vim.treesitter.get_parser(0, "c") |         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 |           result = metadata.key | ||||||
|         end |         end | ||||||
|  |  | ||||||
| @@ -889,7 +1151,7 @@ int x = INT_MAX; | |||||||
|           query = vim.treesitter.query.parse("c", '((number_literal) @number (#set! @number "key" "value"))') |           query = vim.treesitter.query.parse("c", '((number_literal) @number (#set! @number "key" "value"))') | ||||||
|           parser = vim.treesitter.get_parser(0, "c") |           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 |             for _, nested_tbl in pairs(metadata) do | ||||||
|               return nested_tbl.key |               return nested_tbl.key | ||||||
|             end |             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"))') |           query = vim.treesitter.query.parse("c", '((number_literal) @number (#set! @number "key" "value") (#set! @number "key2" "value2"))') | ||||||
|           parser = vim.treesitter.get_parser(0, "c") |           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 |             for _, nested_tbl in pairs(metadata) do | ||||||
|               return nested_tbl |               return nested_tbl | ||||||
|             end |             end | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Thomas Vigouroux
					Thomas Vigouroux