Repository: neovim/neovim
Pull request #32790: feat(treesitter): disable captures and patterns on TSQuery
Author: login: ian-h-chamberlain
Labels: name: treesitter
Created: 2025-03-08T17:37:32Z | Merged: 2025-03-11T13:45:01Z

Part 1 of #32729 

These APIs are already exposed by tree-sitter API, so this is just a
simple binding to expose them to Lua. Since they are pretty low-level
and not reversible, leave it mostly undocumented for now.

This also adds the pattern indices to the output of `:Inspect!` so that it's actually feasible to identify a given pattern and disable it. 


Diff:
diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt
index 5a3e35a3ddaf98..bf6ec2af6ade56 100644
--- a/runtime/doc/news.txt
+++ b/runtime/doc/news.txt
@@ -427,6 +427,10 @@ TREESITTER
   code block fence lines vertically.
 • |vim.treesitter.language.inspect()| shows additional information, including
   parser version for ABI 15 parsers.
+• |TSQuery:disable_pattern()| and |TSQuery:disable_capture()| to turn off
+  a specific pattern or capture in a query.
+• |vim.treesitter.get_captures_at_pos()| returns the `pattern_id` of the
+  pattern used to match each capture.
 
 TUI
 
diff --git a/runtime/doc/treesitter.txt b/runtime/doc/treesitter.txt
index 3918188f1b2899..b5f64921f67726 100644
--- a/runtime/doc/treesitter.txt
+++ b/runtime/doc/treesitter.txt
@@ -1240,6 +1240,26 @@ This Lua |treesitter-query| interface allows you to create queries and use
 them to parse text. See |vim.treesitter.query.parse()| for a working example.
 
 
+*vim.treesitter.Query*
+    Parsed query, see |vim.treesitter.query.parse()|
+
+    Fields: ~
+      • {lang}                     (`string`) parser language name
+      • {captures}                 (`string[]`) list of (unique) capture names
+                                   defined in query
+      • {info}                     (`vim.treesitter.QueryInfo`) query context
+                                   (e.g. captures, predicates, directives)
+      • {has_conceal_line}         (`boolean`) whether the query sets
+                                   conceal_lines metadata
+      • {has_combined_injections}  (`boolean`) whether the query contains
+                                   combined injections
+      • {query}                    (`TSQuery`) userdata query object
+      • {iter_captures}            (`fun(self: vim.treesitter.Query, node: TSNode, source: integer|string, start: integer?, stop: integer?): fun(end_line: integer?): integer, TSNode, vim.treesitter.query.TSMetadata, TSQueryMatch, TSTree`)
+                                   See |Query:iter_captures()|.
+      • {iter_matches}             (`fun(self: vim.treesitter.Query, node: TSNode, source: integer|string, start: integer?, stop: integer?, opts: table?): fun(): integer, table<integer, TSNode[]>, vim.treesitter.query.TSMetadata, TSTree`)
+                                   See |Query:iter_matches()|.
+
+
                                         *vim.treesitter.query.add_directive()*
 add_directive({name}, {handler}, {opts})
     Adds a new directive to be used in queries
@@ -1307,7 +1327,7 @@ get({lang}, {query_name})                         *vim.treesitter.query.get()*
 
     Return: ~
         (`vim.treesitter.Query?`) Parsed query. `nil` if no query files are
-        found.
+        found. See |vim.treesitter.Query|.
 
                                             *vim.treesitter.query.get_files()*
 get_files({lang}, {query_name}, {is_included})
@@ -1373,10 +1393,12 @@ parse({lang}, {query})                          *vim.treesitter.query.parse()*
     Parses a {query} string and returns a `Query` object
     (|lua-treesitter-query|), which can be used to search the tree for the
     query patterns (via |Query:iter_captures()|, |Query:iter_matches()|), or
-    inspect the query via these fields:
+    inspect/modify the query via these fields:
     • `captures`: a list of unique capture names defined in the query (alias:
       `info.captures`).
     • `info.patterns`: information about predicates.
+    • `query`: the underlying |TSQuery| which can be used to disable patterns
+      or captures.
 
     Example: >lua
         local query = vim.treesitter.query.parse('vimdoc', [[
@@ -1396,7 +1418,7 @@ parse({lang}, {query})                          *vim.treesitter.query.parse()*
       • {query}  (`string`) Query text, in s-expr syntax
 
     Return: ~
-        (`vim.treesitter.Query`) Parsed query
+        (`vim.treesitter.Query`) Parsed query . See |vim.treesitter.Query|.
 
     See also: ~
       • |vim.treesitter.query.get()|
@@ -1514,6 +1536,56 @@ set({lang}, {query_name}, {text})                 *vim.treesitter.query.set()*
       • {text}        (`string`) Query text (unparsed).
 
 
+
+
+*TSQuery*
+    Extends: |userdata|
+
+    Reference to an object held by the treesitter library that is used as a
+    component of the |vim.treesitter.Query| for language feature support. See
+    |treesitter-query| for more about queries or
+    |vim.treesitter.query.parse()| for an example of how to obtain a query
+    object.
+
+    Fields: ~
+      • {disable_capture}  (`fun(self: TSQuery, capture_name: string)`) See
+                           |TSQuery:disable_capture()|.
+      • {disable_pattern}  (`fun(self: TSQuery, pattern_index: integer)`) See
+                           |TSQuery:disable_pattern()|.
+
+
+TSQuery:disable_capture({capture_name})            *TSQuery:disable_capture()*
+    Disable a specific capture in this query; once disabled the capture cannot
+    be re-enabled. {capture_name} should not include a leading "@".
+
+    Example: To disable the `@variable.parameter` capture from the vimdoc
+    highlights query: >lua
+        local query = vim.treesitter.query.get('vimdoc', 'highlights')
+        query.query:disable_capture("variable.parameter")
+        vim.treesitter.get_parser():parse()
+<
+
+    Parameters: ~
+      • {capture_name}  (`string`)
+
+TSQuery:disable_pattern({pattern_index})           *TSQuery:disable_pattern()*
+    Disable a specific pattern in this query; once disabled the pattern cannot
+    be re-enabled. The {pattern_index} for a particular match can be obtained
+    with |:Inspect!|, or by reading the source of the query (i.e. from
+    |vim.treesitter.query.get_files()|).
+
+    Example: To disable `|` links in vimdoc but keep other `@markup.link`s
+    highlighted: >lua
+        local link_pattern = 9 -- from :Inspect!
+        local query = vim.treesitter.query.get('vimdoc', 'highlights')
+        query.query:disable_pattern(link_pattern)
+        local tree = vim.treesitter.get_parser():parse()[1]
+<
+
+    Parameters: ~
+      • {pattern_index}  (`integer`)
+
+
 ==============================================================================
 Lua module: vim.treesitter.languagetree              *treesitter-languagetree*
 
diff --git a/runtime/lua/vim/treesitter.lua b/runtime/lua/vim/treesitter.lua
index a5362202fbb382..c2df96d9b44427 100644
--- a/runtime/lua/vim/treesitter.lua
+++ b/runtime/lua/vim/treesitter.lua
@@ -288,15 +288,19 @@ function M.get_captures_at_pos(bufnr, row, col)
 
     local iter = q:query():iter_captures(root, buf_highlighter.bufnr, row, row + 1)
 
-    for id, node, metadata in iter do
+    for id, node, metadata, match in iter do
       if M.is_in_node_range(node, row, col) then
         ---@diagnostic disable-next-line: invisible
         local capture = q._query.captures[id] -- name of the capture in the query
         if capture ~= nil then
-          table.insert(
-            matches,
-            { capture = capture, metadata = metadata, lang = tree:lang(), id = id }
-          )
+          local _, pattern_id = match:info()
+          table.insert(matches, {
+            capture = capture,
+            metadata = metadata,
+            lang = tree:lang(),
+            id = id,
+            pattern_id = pattern_id,
+          })
         end
       end
     end
diff --git a/runtime/lua/vim/treesitter/_meta/misc.lua b/runtime/lua/vim/treesitter/_meta/misc.lua
index 99267bb36e1795..07a1c921c744c3 100644
--- a/runtime/lua/vim/treesitter/_meta/misc.lua
+++ b/runtime/lua/vim/treesitter/_meta/misc.lua
@@ -14,9 +14,6 @@ error('Cannot require a meta file')
 ---@field _set_logger fun(self: TSParser, lex: boolean, parse: boolean, cb: TSLoggerCallback)
 ---@field _logger fun(self: TSParser): TSLoggerCallback
 
----@class TSQuery: userdata
----@field inspect fun(self: TSQuery): TSQueryInfo
-
 ---@class (exact) TSQueryInfo
 ---@field captures string[]
 ---@field patterns table<integer, (integer|string)[][]>
diff --git a/runtime/lua/vim/treesitter/_meta/tsquery.lua b/runtime/lua/vim/treesitter/_meta/tsquery.lua
new file mode 100644
index 00000000000000..14b1fc30590636
--- /dev/null
+++ b/runtime/lua/vim/treesitter/_meta/tsquery.lua
@@ -0,0 +1,45 @@
+---@meta
+-- luacheck: no unused args
+error('Cannot require a meta file')
+
+-- This could be documented as a module @brief like tsnode/tstree, but without
+-- its own section header documenting it as a class ensures it still gets a helptag.
+
+--- Reference to an object held by the treesitter library that is used as a
+--- component of the |vim.treesitter.Query| for language feature support.
+--- See |treesitter-query| for more about queries or |vim.treesitter.query.parse()|
+--- for an example of how to obtain a query object.
+---
+---@class TSQuery: userdata
+local TSQuery = {} -- luacheck: no unused
+
+--- Get information about the query's patterns and captures.
+---@nodoc
+---@return TSQueryInfo
+function TSQuery:inspect() end
+
+--- Disable a specific capture in this query; once disabled the capture cannot be re-enabled.
+--- {capture_name} should not include a leading "@".
+---
+--- Example: To disable the `@variable.parameter` capture from the vimdoc highlights query:
+--- ```lua
+--- local query = vim.treesitter.query.get('vimdoc', 'highlights')
+--- query.query:disable_capture("variable.parameter")
+--- vim.treesitter.get_parser():parse()
+--- ```
+---@param capture_name string
+function TSQuery:disable_capture(capture_name) end
+
+--- Disable a specific pattern in this query; once disabled the pattern cannot be re-enabled.
+--- The {pattern_index} for a particular match can be obtained with |:Inspect!|, or by reading
+--- the source of the query (i.e. from |vim.treesitter.query.get_files()|).
+---
+--- Example: To disable `|` links in vimdoc but keep other `@markup.link`s highlighted:
+--- ```lua
+--- local link_pattern = 9 -- from :Inspect!
+--- local query = vim.treesitter.query.get('vimdoc', 'highlights')
+--- query.query:disable_pattern(link_pattern)
+--- local tree = vim.treesitter.get_parser():parse()[1]
+--- ```
+---@param pattern_index integer
+function TSQuery:disable_pattern(pattern_index) end
diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua
index 17088ac0eb3c34..5830cc12e04882 100644
--- a/runtime/lua/vim/treesitter/query.lua
+++ b/runtime/lua/vim/treesitter/query.lua
@@ -10,7 +10,6 @@ local EXTENDS_FORMAT = '^;+%s*extends%s*$'
 
 local M = {}
 
----@nodoc
 ---Parsed query, see |vim.treesitter.query.parse()|
 ---
 ---@class vim.treesitter.Query
@@ -344,9 +343,10 @@ api.nvim_create_autocmd('OptionSet', {
 
 --- Parses a {query} string and returns a `Query` object (|lua-treesitter-query|), which can be used
 --- to search the tree for the query patterns (via |Query:iter_captures()|, |Query:iter_matches()|),
---- or inspect the query via these fields:
+--- or inspect/modify the query via these fields:
 ---   - `captures`: a list of unique capture names defined in the query (alias: `info.captures`).
 ---   - `info.patterns`: information about predicates.
+---   - `query`: the underlying |TSQuery| which can be used to disable patterns or captures.
 ---
 --- Example:
 --- ```lua
diff --git a/src/gen/gen_vimdoc.lua b/src/gen/gen_vimdoc.lua
index 2fe7224ea522ed..68912cf0b52499 100755
--- a/src/gen/gen_vimdoc.lua
+++ b/src/gen/gen_vimdoc.lua
@@ -328,10 +328,12 @@ local config = {
       'treesitter.lua',
       'language.lua',
       'query.lua',
+      'tsquery.lua',
       'highlighter.lua',
       'languagetree.lua',
       'dev.lua',
     },
+    append_only = { 'tsquery.lua' },
     files = {
       'runtime/lua/vim/treesitter/_meta/',
       'runtime/lua/vim/treesitter.lua',
diff --git a/src/nvim/lua/treesitter.c b/src/nvim/lua/treesitter.c
index fa5cf1118d61c8..a346bf59637fa0 100644
--- a/src/nvim/lua/treesitter.c
+++ b/src/nvim/lua/treesitter.c
@@ -1491,6 +1491,8 @@ static struct luaL_Reg query_meta[] = {
   { "__gc", query_gc },
   { "__tostring", query_tostring },
   { "inspect", query_inspect },
+  { "disable_capture", query_disable_capture },
+  { "disable_pattern", query_disable_pattern },
   { NULL, NULL }
 };
 
@@ -1689,6 +1691,23 @@ static int query_inspect(lua_State *L)
   return 1;
 }
 
+static int query_disable_capture(lua_State *L)
+{
+  TSQuery *query = query_check(L, 1);
+  size_t name_len;
+  const char *name = luaL_checklstring(L, 2, &name_len);
+  ts_query_disable_capture(query, name, (uint32_t)name_len);
+  return 0;
+}
+
+static int query_disable_pattern(lua_State *L)
+{
+  TSQuery *query = query_check(L, 1);
+  const uint32_t pattern_index = (uint32_t)luaL_checkinteger(L, 2);
+  ts_query_disable_pattern(query, pattern_index - 1);
+  return 0;
+}
+
 // Library init
 
 static void build_meta(lua_State *L, const char *tname, const luaL_Reg *meta)
diff --git a/test/functional/treesitter/highlight_spec.lua b/test/functional/treesitter/highlight_spec.lua
index f3a321aa8836ea..5b9a060fb5b81e 100644
--- a/test/functional/treesitter/highlight_spec.lua
+++ b/test/functional/treesitter/highlight_spec.lua
@@ -640,8 +640,14 @@ describe('treesitter highlighting (C)', function()
     }
 
     eq({
-      { capture = 'constant', metadata = { priority = '101' }, lang = 'c', id = 14 },
-      { capture = 'type', metadata = {}, lang = 'c', id = 3 },
+      {
+        capture = 'constant',
+        metadata = { priority = '101' },
+        lang = 'c',
+        id = 14,
+        pattern_id = 23,
+      },
+      { capture = 'type', metadata = {}, lang = 'c', id = 3, pattern_id = 16 },
     }, exec_lua [[ return vim.treesitter.get_captures_at_pos(0, 0, 2) ]])
   end)
 
diff --git a/test/functional/treesitter/query_spec.lua b/test/functional/treesitter/query_spec.lua
index be9c60b8ad8e00..b9bf5bb6e870a5 100644
--- a/test/functional/treesitter/query_spec.lua
+++ b/test/functional/treesitter/query_spec.lua
@@ -912,4 +912,52 @@ void ui_refresh(void)
       eq({ 2, { 1, 1, 2, 2 } }, result)
     end)
   end)
+
+  describe('TSQuery', function()
+    local source = [[
+      void foo(int x, int y);
+    ]]
+
+    local query_text = [[
+      ((identifier) @func
+        (#eq? @func "foo"))
+      ((identifier) @param
+        (#eq? @param "x"))
+      ((identifier) @param
+        (#eq? @param "y"))
+    ]]
+
+    ---@param query string
+    ---@param disabled { capture: string?, pattern: integer? }
+    local function get_patterns(query, disabled)
+      local q = vim.treesitter.query.parse('c', query)
+      if disabled.capture then
+        q.query:disable_capture(disabled.capture)
+      end
+      if disabled.pattern then
+        q.query:disable_pattern(disabled.pattern)
+      end
+
+      local parser = vim.treesitter.get_parser(0, 'c')
+      local root = parser:parse()[1]:root()
+      local captures = {} ---@type {id: number, pattern: number}[]
+      for id, _, _, match in q:iter_captures(root, 0) do
+        local _, pattern = match:info()
+        captures[#captures + 1] = { id = id, pattern = pattern }
+      end
+      return captures
+    end
+
+    it('supports disabling patterns', function()
+      insert(source)
+      local result = exec_lua(get_patterns, query_text, { pattern = 2 })
+      eq({ { id = 1, pattern = 1 }, { id = 2, pattern = 3 } }, result)
+    end)
+
+    it('supports disabling captures', function()
+      insert(source)
+      local result = exec_lua(get_patterns, query_text, { capture = 'param' })
+      eq({ { id = 1, pattern = 1 } }, result)
+    end)
+  end)
 end)


Reviews:
user: login: clason
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-09T10:47:56Z
user: login: clason
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-09T11:25:53Z
user: login: clason
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-09T11:26:05Z
user: login: clason
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-09T11:26:24Z
user: login: justinmk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-09T13:43:22Z
user: login: justinmk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-09T14:01:52Z
user: login: ian-h-chamberlain
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-09T19:39:58Z
user: login: ian-h-chamberlain
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-09T20:33:30Z
user: login: ian-h-chamberlain
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T04:48:49Z
user: login: justinmk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-10T14:56:09Z
user: login: justinmk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-10T15:03:40Z
user: login: justinmk
body: Looks great. Will wait for @clason to merge
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-10T15:08:07Z

Review comments:
diff_hunk: @@ -288,15 +288,19 @@ function M.get_captures_at_pos(bufnr, row, col)
 
     local iter = q:query():iter_captures(root, buf_highlighter.bufnr, row, row + 1)
 
-    for id, node, metadata in iter do
+    for id, node, metadata, match in iter do
       if M.is_in_node_range(node, row, col) then
         ---@diagnostic disable-next-line: invisible
         local capture = q._query.captures[id] -- name of the capture in the query
         if capture ~= nil then
-          table.insert(
-            matches,
-            { capture = capture, metadata = metadata, lang = tree:lang(), id = id }
-          )
+          local _, pattern_i = match:info()
path: runtime/lua/vim/treesitter.lua
user: login: clason
body: ```suggestion
          local _, pattern_id = match:info()
```
created_at: 2025-03-09T10:47:56Z
author_association: MEMBER
diff_hunk: @@ -417,6 +417,10 @@ TREESITTER
   code block fence lines vertically.
 • |vim.treesitter.language.inspect()| shows additional information, including
   parser version for ABI 15 parsers.
+• New |TSQuery:disable_pattern()| and |TSQuery:disable_capture()| to turn off
path: runtime/doc/news.txt
user: login: clason
body: ```suggestion
• |TSQuery:disable_pattern()| and |TSQuery:disable_capture()| to turn off
```
created_at: 2025-03-09T11:25:53Z
author_association: MEMBER
diff_hunk: @@ -417,6 +417,10 @@ TREESITTER
   code block fence lines vertically.
 • |vim.treesitter.language.inspect()| shows additional information, including
   parser version for ABI 15 parsers.
+• New |TSQuery:disable_pattern()| and |TSQuery:disable_capture()| to turn off
+  a specific pattern or capture in a query.
+• |vim.treesitter.get_captures_at_pos()| now returns the `pattern_id` of the
path: runtime/doc/news.txt
user: login: clason
body: ```suggestion
• |vim.treesitter.get_captures_at_pos()| returns the `pattern_id` of the
```


created_at: 2025-03-09T11:26:05Z
author_association: MEMBER
diff_hunk: @@ -417,6 +417,10 @@ TREESITTER
   code block fence lines vertically.
 • |vim.treesitter.language.inspect()| shows additional information, including
   parser version for ABI 15 parsers.
+• New |TSQuery:disable_pattern()| and |TSQuery:disable_capture()| to turn off
path: runtime/doc/news.txt
user: login: clason
body: Those links won't work without actual documentation...
created_at: 2025-03-09T11:26:24Z
author_association: MEMBER
diff_hunk: @@ -911,5 +911,65 @@ void ui_refresh(void)
 
       eq({ 2, { 1, 1, 2, 2 } }, result)
     end)
+
+    it('supports disabling patterns', function()
+      insert([[
+        void foo(int x, int y);
+      ]])
+      local query = [[
+        ((identifier) @func
+          (#eq? @func "foo"))
+        ((identifier) @param
+          (#eq? @param "x"))
+        ((identifier) @param
+          (#eq? @param "y"))
+      ]]
+
+      local result = exec_lua(function()
+        local query0 = vim.treesitter.query.parse('c', query)
+        query0.query:disable_pattern(2)
+
+        local parser = vim.treesitter.get_parser(0, 'c')
+        local root = parser:parse()[1]:root()
+        local captures = {}
+        for id, _, _, match in query0:iter_captures(root, 0) do
+          local _, pattern = match:info()
+          captures[#captures + 1] = { id = id, pattern = pattern }
+        end
+        return captures
+      end)
+
+      eq({ { id = 1, pattern = 1 }, { id = 2, pattern = 3 } }, result)
+    end)
+
+    it('supports disabling captures', function()
path: test/functional/treesitter/query_spec.lua
user: login: justinmk
body: this code is 99% identical to the above, please factor out into a local function.
created_at: 2025-03-09T13:43:22Z
author_association: MEMBER
diff_hunk: @@ -417,6 +417,10 @@ TREESITTER
   code block fence lines vertically.
 • |vim.treesitter.language.inspect()| shows additional information, including
   parser version for ABI 15 parsers.
+• New |TSQuery:disable_pattern()| and |TSQuery:disable_capture()| to turn off
path: runtime/doc/news.txt
user: login: justinmk
body: Just noticed that docs validation isn't working. Working on a fix
created_at: 2025-03-09T14:01:52Z
author_association: MEMBER
diff_hunk: @@ -417,6 +417,10 @@ TREESITTER
   code block fence lines vertically.
 • |vim.treesitter.language.inspect()| shows additional information, including
   parser version for ABI 15 parsers.
+• New |TSQuery:disable_pattern()| and |TSQuery:disable_capture()| to turn off
path: runtime/doc/news.txt
user: login: ian-h-chamberlain
body: Ah, I thought it might just fall back to highlighting if there weren't actual docs to link to and I only really added a `news.txt` entry since the check failed on my first push.

Currently it appears there is no documentation at all for `TSQuery` (although it's referenced in some docs), so I'm not sure what the best way to document these new methods would be – would I add a new class type in `query.lua`, or break out a new file in `runtime/lua/vim/treesitter/_meta` or something?

I guess this is related also to #26721 (exposing more of the `TS*` classes in documentation)?

Edit, also:
> A small example snippet would also go a long way.

Makes sense, I guess this would probably make the most sense as part of `lua-treesitter-query` docs?
created_at: 2025-03-09T19:39:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -288,15 +288,19 @@ function M.get_captures_at_pos(bufnr, row, col)
 
     local iter = q:query():iter_captures(root, buf_highlighter.bufnr, row, row + 1)
 
-    for id, node, metadata in iter do
+    for id, node, metadata, match in iter do
       if M.is_in_node_range(node, row, col) then
         ---@diagnostic disable-next-line: invisible
         local capture = q._query.captures[id] -- name of the capture in the query
         if capture ~= nil then
-          table.insert(
-            matches,
-            { capture = capture, metadata = metadata, lang = tree:lang(), id = id }
-          )
+          local _, pattern_i = match:info()
path: runtime/lua/vim/treesitter.lua
user: login: ian-h-chamberlain
body: (replying here for threading purposes)

> (Are the pattern ids stable?)

This is a good question and one I also had when I started implementing this... I haven't really found any specific documentation that indicates "stability" of these but from what I understand:

1. This pattern ID is really just an index, so given a query like `(foo) (bar) (baz)` you get IDs of `0`, `1`, `2`, respectively
	- Because nvim may concatenate queries together (e.g. `;extends`, `;inherits`), any changes in query file load order or similar mechanisms has the potential to change the indices
2. Pattern IDs are used in a number of public tree-sitter APIs (like `TSQueryMatch` and `ts_query_disable_pattern`), so I think they can probably be considered as stable as the rest of the API?
created_at: 2025-03-09T20:33:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -417,6 +417,10 @@ TREESITTER
   code block fence lines vertically.
 • |vim.treesitter.language.inspect()| shows additional information, including
   parser version for ABI 15 parsers.
+• New |TSQuery:disable_pattern()| and |TSQuery:disable_capture()| to turn off
path: runtime/doc/news.txt
user: login: ian-h-chamberlain
body: Pushed an update which adds `TSQuery` docs to the `lua-treesitter-query` section; it didn't seem to make sense to follow exactly the format already used by `TSNode`/`TSTree` because they have their own sections and there is already a (non-generated) `TREESITTER QUERIES` section, but please let me know if you think it deserves its own section or any wording changes.
created_at: 2025-03-10T04:48:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -427,6 +427,10 @@ TREESITTER
   code block fence lines vertically.
 • |vim.treesitter.language.inspect()| shows additional information, including
   parser version for ABI 15 parsers.
+• |TSQuery:disable_pattern()| and |TSQuery:disable_capture()| to turn off
+  a specific pattern or capture in a query.
+• |vim.treesitter.get_captures_at_pos()| returns the `pattern_id` of the
+  pattern used to match each capture.
path: runtime/doc/news.txt
user: login: justinmk
body: add to the existing item: https://github.com/neovim/neovim/blob/37786ccfcf07706c7524cdeef321bb4defba45b2/runtime/doc/news.txt#L416
created_at: 2025-03-10T14:56:08Z
author_association: MEMBER
diff_hunk: @@ -1514,6 +1536,56 @@ set({lang}, {query_name}, {text})                 *vim.treesitter.query.set()*
       • {text}        (`string`) Query text (unparsed).
 
 
+
+
+*TSQuery*
+    Extends: |userdata|
+
+    Reference to an object held by the treesitter library that is used as a
+    component of the |vim.treesitter.Query| for language feature support. See
+    |treesitter-query| for more about queries or
+    |vim.treesitter.query.parse()| for an example of how to obtain a query
+    object.
+
+    Fields: ~
+      • {disable_capture}  (`fun(self: TSQuery, capture_name: string)`) See
+                           |TSQuery:disable_capture()|.
+      • {disable_pattern}  (`fun(self: TSQuery, pattern_index: integer)`) See
+                           |TSQuery:disable_pattern()|.
+
+
+TSQuery:disable_capture({capture_name})            *TSQuery:disable_capture()*
+    Disable a specific capture in this query; once disabled the capture cannot
+    be re-enabled. {capture_name} should not include a leading "@".
+
+    Example: To disable the `@variable.parameter` capture from the vimdoc
+    highlights query: >lua
+        local query = vim.treesitter.query.get('vimdoc', 'highlights')
+        query.query:disable_capture("variable.parameter")
+        vim.treesitter.get_parser():parse()
+<
+
+    Parameters: ~
+      • {capture_name}  (`string`)
+
+TSQuery:disable_pattern({pattern_index})           *TSQuery:disable_pattern()*
+    Disable a specific pattern in this query; once disabled the pattern cannot
+    be re-enabled. The {pattern_index} for a particular match can be obtained
+    with |:Inspect!|, or by reading the source of the query (i.e. from
+    |vim.treesitter.query.get_files()|).
+
+    Example: To disable `|` links in vimdoc but keep other `@markup.link`s
+    highlighted: >lua
path: runtime/doc/treesitter.txt
user: login: justinmk
body: Nice!
created_at: 2025-03-10T15:03:40Z
author_association: MEMBER

PR comments:
user: login: clason
created_at: 2025-03-09T11:24:52Z
body: I think the exposed functions _should_ be documented, especially if you link them from `news.txt`. A small example snippet would also go a long way. (Are the pattern ids stable?)
author_association: MEMBER
user: login: ian-h-chamberlain
created_at: 2025-03-11T16:32:54Z
body: Thanks for the quick reviews and merge!
author_association: CONTRIBUTOR