aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua
diff options
context:
space:
mode:
authorLewis Russell <lewis6991@gmail.com>2026-09-28 11:24:21 +0100
committerLewis Russell <me@lewisr.dev>2026-09-28 15:17:58 +0100
commitd49bbcf92e5dc1e96bd09fca62fcf7152c459ba8 (patch)
treead8e8d2ec9fbe8a3e2bf165145627def30467670 /runtime/lua
parentd51a6e2d55486b8ac3e352dc48f290884577dad9 (diff)
downloadrneovim-d49bbcf92e5dc1e96bd09fca62fcf7152c459ba8.tar.gz
rneovim-d49bbcf92e5dc1e96bd09fca62fcf7152c459ba8.tar.bz2
rneovim-d49bbcf92e5dc1e96bd09fca62fcf7152c459ba8.zip
feat(treesitter): add get_captures() for positions and ranges
Problem: get_captures_at_pos() only returns captures at one position. It cannot query a whole line or range. It can also miss captures from injected languages outside the visible area. Solution: Add vim.treesitter.get_captures() with a required start and an optional exclusive stop. Accept row numbers or {row, col} positions. Without a stop, query one position or a whole line. Parse the requested range and return capture coordinates. Deprecate get_captures_at_pos() with a compatibility wrapper and migrate its internal callers. Keep get_captures_at_cursor() for capture names at a window's cursor. AI-assisted
Diffstat (limited to 'runtime/lua')
-rw-r--r--runtime/lua/vim/_comment.lua2
-rw-r--r--runtime/lua/vim/_inspector.lua2
-rw-r--r--runtime/lua/vim/treesitter.lua98
3 files changed, 79 insertions, 23 deletions
diff --git a/runtime/lua/vim/_comment.lua b/runtime/lua/vim/_comment.lua
index a3fa08150d..502a4bec64 100644
--- a/runtime/lua/vim/_comment.lua
+++ b/runtime/lua/vim/_comment.lua
@@ -20,7 +20,7 @@ local function get_commentstring(ref_position)
-- Get 'commentstring' from tree-sitter captures' metadata.
-- Traverse backwards to prefer narrower captures.
- local caps = vim.treesitter.get_captures_at_pos(0, row, col)
+ local caps = vim.treesitter.get_captures(0, { row, col })
for i = #caps, 1, -1 do
local id, metadata = caps[i].id, caps[i].metadata
local md_cms = metadata['bo.commentstring'] or metadata[id] and metadata[id]['bo.commentstring']
diff --git a/runtime/lua/vim/_inspector.lua b/runtime/lua/vim/_inspector.lua
index 49a99c4db8..e33f9c8998 100644
--- a/runtime/lua/vim/_inspector.lua
+++ b/runtime/lua/vim/_inspector.lua
@@ -93,7 +93,7 @@ function vim.inspect_pos(buf, row, col, filter)
-- treesitter
if filter.treesitter then
- for _, capture in pairs(vim.treesitter.get_captures_at_pos(buf, row, col)) do
+ for _, capture in pairs(vim.treesitter.get_captures(buf, { row, col })) do
--- @diagnostic disable-next-line: inject-field
capture.hl_group = '@' .. capture.capture .. '.' .. capture.lang
results.treesitter[#results.treesitter + 1] = resolve_hl(capture)
diff --git a/runtime/lua/vim/treesitter.lua b/runtime/lua/vim/treesitter.lua
index 53a1427970..dda51484d4 100644
--- a/runtime/lua/vim/treesitter.lua
+++ b/runtime/lua/vim/treesitter.lua
@@ -271,37 +271,86 @@ function M.node_contains(node, range)
return M._range.contains(nrange, range)
end
---- Returns a list of highlight captures at the given position
+--- @class vim.treesitter.CaptureInfo
+--- @inlinedoc
+--- @field capture string Capture name
+--- @field lang string Language name
+--- @field metadata vim.treesitter.query.TSMetadata Query metadata, e.g. `priority` or `conceal`
+--- @field id integer 1-based capture id in the query
+--- @field pattern_id integer 1-based id of the matched pattern in the query
+--- @field row integer Start row
+--- @field col integer Start column
+--- @field end_row integer End row
+--- @field end_col integer End column (excluded)
+
+---@deprecated Use vim.treesitter.get_captures() instead.
+---@param buf integer
+---@param row integer
+---@param col integer
+---@return vim.treesitter.CaptureInfo[]
+function M.get_captures_at_pos(buf, row, col)
+ vim.deprecate('vim.treesitter.get_captures_at_pos()', 'vim.treesitter.get_captures()', '0.15')
+ return M.get_captures(buf, { row, col })
+end
+
+--- Returns highlight captures at a position, on a line, or overlapping a range.
---
---- Each capture is represented by a table containing the capture name as a string, the capture's
---- language, a table of metadata (`priority`, `conceal`, ...; empty if none are defined), the id
---- of the capture, and the (0-indexed) id of the matched pattern in the query.
+--- {start} and {stop} can be a row number or a `{row, col}` pair. All rows and
+--- columns are 0-based; columns are byte offsets.
---
----@param buf integer Buffer number (0 for current buffer)
----@param row integer Position row
----@param col integer Position column
+--- Without {stop}, a row number queries the whole line and a `{row, col}` pair
+--- queries one position.
---
----@return {capture: string, lang: string, metadata: vim.treesitter.query.TSMetadata, id: integer, pattern_id: integer}[]
-function M.get_captures_at_pos(buf, row, col)
+--- With {stop}, return captures that overlap the range. The stop position is
+--- excluded; a row number excludes that row. Empty ranges return no captures.
+---
+--- Each result includes the full captured node range, which may extend outside the
+--- requested range. Returns an empty list if treesitter highlighting is not active
+--- in {buf}.
+---
+---@since 15
+---@param buf integer Buffer number (0 for current buffer)
+---@param start integer|[integer,integer] Start row or `{row, col}`
+---@param stop? integer|[integer,integer] Stop row or `{row, col}` (excluded)
+---@return vim.treesitter.CaptureInfo[]
+function M.get_captures(buf, start, stop)
buf = vim._resolve_bufnr(buf)
+
+ --- @type integer, integer, integer, integer
+ local start_row, start_col, end_row, end_col
+ if type(start) == 'number' then
+ start_row, start_col = start, 0
+ else
+ start_row, start_col = start[1], start[2]
+ end
+
+ if stop == nil then
+ end_row = type(start) == 'number' and start_row + 1 or start_row
+ end_col = type(start) == 'number' and 0 or start_col + 1
+ elseif type(stop) == 'number' then
+ end_row, end_col = stop, 0
+ else
+ end_row, end_col = stop[1], stop[2]
+ end
+
local buf_highlighter = M.highlighter.active[buf]
- if not buf_highlighter then
+ if not buf_highlighter or M._range.cmp_pos.ge(start_row, start_col, end_row, end_col) then
return {}
end
- local matches = {}
+ local matches = {} --- @type vim.treesitter.CaptureInfo[]
+ local query_range = { start_row, start_col, end_row, end_col } --- @type Range4
+ -- Highlighting only parses visible ranges, so offscreen injections may be missing.
+ buf_highlighter.tree:parse(query_range)
buf_highlighter.tree:for_each_tree(function(tstree, tree)
if not tstree then
return
end
local root = tstree:root()
- local root_start_row, _, root_end_row, _ = root:range()
-
- -- Only worry about trees within the line range
- if root_start_row > row or root_end_row < row then
+ if not M._range.intercepts({ root:range() }, query_range) then
return
end
@@ -313,21 +362,28 @@ function M.get_captures_at_pos(buf, row, col)
return
end
- local iter = query:iter_captures(root, buf_highlighter.bufnr, row, row + 1)
-
+ local iter = query:iter_captures(root, buf_highlighter.bufnr, start_row, end_row, {
+ start_col = start_col,
+ end_col = end_col,
+ })
for id, node, metadata, match in iter do
- if M.is_in_node_range(node, row, col) then
+ local nsrow, nscol, nerow, necol = node:range()
+ if M._range.intercepts({ nsrow, nscol, nerow, necol }, query_range) then
---@diagnostic disable-next-line: invisible
local capture = query.captures[id] -- name of the capture in the query
if capture ~= nil then
local _, pattern_id = match:info()
- table.insert(matches, {
+ matches[#matches + 1] = {
capture = capture,
metadata = metadata,
lang = tree:lang(),
id = id,
pattern_id = pattern_id,
- })
+ row = nsrow,
+ col = nscol,
+ end_row = nerow,
+ end_col = necol,
+ }
end
end
end
@@ -345,7 +401,7 @@ function M.get_captures_at_cursor(win)
local bufnr = api.nvim_win_get_buf(win)
local cursor = api.nvim_win_get_cursor(win)
- local data = M.get_captures_at_pos(bufnr, cursor[1] - 1, cursor[2])
+ local data = M.get_captures(bufnr, { cursor[1] - 1, cursor[2] })
local captures = {}