aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim
diff options
context:
space:
mode:
Diffstat (limited to 'runtime/lua/vim')
-rw-r--r--runtime/lua/vim/_meta/api_keysets.lua6
-rw-r--r--runtime/lua/vim/_meta/vimfn.lua14
-rw-r--r--runtime/lua/vim/_meta/vvars.lua12
-rw-r--r--runtime/lua/vim/_options.lua6
-rw-r--r--runtime/lua/vim/filetype.lua3
-rw-r--r--runtime/lua/vim/filetype/detect.lua15
-rw-r--r--runtime/lua/vim/fs.lua4
-rw-r--r--runtime/lua/vim/func.lua14
-rw-r--r--runtime/lua/vim/func/_memoize.lua56
-rw-r--r--runtime/lua/vim/health.lua52
-rw-r--r--runtime/lua/vim/lsp/util.lua10
-rw-r--r--runtime/lua/vim/treesitter/_fold.lua34
-rw-r--r--runtime/lua/vim/treesitter/highlighter.lua4
-rw-r--r--runtime/lua/vim/treesitter/languagetree.lua2
-rw-r--r--runtime/lua/vim/treesitter/query.lua221
15 files changed, 308 insertions, 145 deletions
diff --git a/runtime/lua/vim/_meta/api_keysets.lua b/runtime/lua/vim/_meta/api_keysets.lua
index e11dddb2d3..c08ab0663b 100644
--- a/runtime/lua/vim/_meta/api_keysets.lua
+++ b/runtime/lua/vim/_meta/api_keysets.lua
@@ -227,10 +227,10 @@ error('Cannot require a meta file')
--- @field do_source? boolean
--- @class vim.api.keyset.set_decoration_provider
---- @field on_start? fun(_: "start", tick: integer)
+--- @field on_start? fun(_: "start", tick: integer): boolean?
--- @field on_buf? fun(_: "buf", bufnr: integer, tick: integer)
---- @field on_win? fun(_: "win", winid: integer, bufnr: integer, toprow: integer, botrow: integer)
---- @field on_line? fun(_: "line", winid: integer, bufnr: integer, row: integer)
+--- @field on_win? fun(_: "win", winid: integer, bufnr: integer, toprow: integer, botrow: integer): boolean?
+--- @field on_line? fun(_: "line", winid: integer, bufnr: integer, row: integer): boolean?
--- @field on_end? fun(_: "end", tick: integer)
--- @field _on_hl_def? fun(_: "hl_def")
--- @field _on_spell_nav? fun(_: "spell_nav")
diff --git a/runtime/lua/vim/_meta/vimfn.lua b/runtime/lua/vim/_meta/vimfn.lua
index 6662fca84f..031b109b38 100644
--- a/runtime/lua/vim/_meta/vimfn.lua
+++ b/runtime/lua/vim/_meta/vimfn.lua
@@ -3770,6 +3770,20 @@ function vim.fn.getregtype(regname) end
--- @return vim.fn.getscriptinfo.ret[]
function vim.fn.getscriptinfo(opts) end
+--- Returns the current stack trace of Vim scripts.
+--- Stack trace is a |List|, of which each item is a |Dictionary|
+--- with the following items:
+--- funcref The funcref if the stack is at a function,
+--- otherwise this item is omitted.
+--- event The string of the event description if the
+--- stack is at an autocmd event, otherwise this
+--- item is omitted.
+--- lnum The line number in the script on the stack.
+--- filepath The file path of the script on the stack.
+---
+--- @return table[]
+function vim.fn.getstacktrace() end
+
--- If {tabnr} is not specified, then information about all the
--- tab pages is returned as a |List|. Each List item is a
--- |Dictionary|. Otherwise, {tabnr} specifies the tab page
diff --git a/runtime/lua/vim/_meta/vvars.lua b/runtime/lua/vim/_meta/vvars.lua
index 445da4e02f..c1b8695bbf 100644
--- a/runtime/lua/vim/_meta/vvars.lua
+++ b/runtime/lua/vim/_meta/vvars.lua
@@ -203,7 +203,8 @@ vim.v.errors = ...
vim.v.event = ...
--- The value of the exception most recently caught and not
---- finished. See also `v:throwpoint` and `throw-variables`.
+--- finished. See also `v:stacktrace`, `v:throwpoint`, and
+--- `throw-variables`.
--- Example:
---
--- ```vim
@@ -616,6 +617,13 @@ vim.v.servername = ...
--- @type integer
vim.v.shell_error = ...
+--- The stack trace of the exception most recently caught and
+--- not finished. Refer to `getstacktrace()` for the structure of
+--- stack trace. See also `v:exception`, `v:throwpoint`, and
+--- `throw-variables`.
+--- @type table[]
+vim.v.stacktrace = ...
+
--- Last given status message.
--- Modifiable (can be set).
--- @type string
@@ -718,7 +726,7 @@ vim.v.this_session = ...
--- The point where the exception most recently caught and not
--- finished was thrown. Not set when commands are typed. See
---- also `v:exception` and `throw-variables`.
+--- also `v:exception`, `v:stacktrace`, and `throw-variables`.
--- Example:
---
--- ```vim
diff --git a/runtime/lua/vim/_options.lua b/runtime/lua/vim/_options.lua
index 77d7054626..dc37595578 100644
--- a/runtime/lua/vim/_options.lua
+++ b/runtime/lua/vim/_options.lua
@@ -229,10 +229,8 @@ end
--- global value of a |global-local| option, see |:setglobal|.
--- </pre>
---- Get or set |options|. Like `:set`. Invalid key is an error.
----
---- Note: this works on both buffer-scoped and window-scoped options using the
---- current buffer and window.
+--- Get or set |options|. Works like `:set`, so buffer/window-scoped options target the current
+--- buffer/window. Invalid key is an error.
---
--- Example:
---
diff --git a/runtime/lua/vim/filetype.lua b/runtime/lua/vim/filetype.lua
index b6d6906589..dee1bd88ca 100644
--- a/runtime/lua/vim/filetype.lua
+++ b/runtime/lua/vim/filetype.lua
@@ -1064,11 +1064,11 @@ local extension = {
builder = 'ruby',
rake = 'ruby',
rs = 'rust',
+ sa = detect.sa,
sage = 'sage',
sls = 'salt',
sas = 'sas',
sass = 'sass',
- sa = 'sather',
sbt = 'sbt',
scala = 'scala',
ss = 'scheme',
@@ -1097,6 +1097,7 @@ local extension = {
la = 'sh',
lai = 'sh',
mdd = 'sh',
+ slang = 'shaderslang',
sieve = 'sieve',
siv = 'sieve',
sig = detect.sig,
diff --git a/runtime/lua/vim/filetype/detect.lua b/runtime/lua/vim/filetype/detect.lua
index a1c17bc1af..2d989fdbac 100644
--- a/runtime/lua/vim/filetype/detect.lua
+++ b/runtime/lua/vim/filetype/detect.lua
@@ -34,6 +34,12 @@ local matchregex = vim.filetype._matchregex
-- can be detected from the first five lines of the file.
--- @type vim.filetype.mapfn
function M.asm(path, bufnr)
+ -- tiasm uses `* commment`
+ local lines = table.concat(getlines(bufnr, 1, 10), '\n')
+ if findany(lines, { '^%*', '\n%*', 'Texas Instruments Incorporated' }) then
+ return 'tiasm'
+ end
+
local syntax = vim.b[bufnr].asmsyntax
if not syntax or syntax == '' then
syntax = M.asm_syntax(path, bufnr)
@@ -1424,6 +1430,15 @@ function M.sig(_, bufnr)
end
end
+--- @type vim.filetype.mapfn
+function M.sa(_, bufnr)
+ local lines = table.concat(getlines(bufnr, 1, 4), '\n')
+ if findany(lines, { '^;', '\n;' }) then
+ return 'tiasm'
+ end
+ return 'sather'
+end
+
-- This function checks the first 25 lines of file extension "sc" to resolve
-- detection between scala and SuperCollider
--- @type vim.filetype.mapfn
diff --git a/runtime/lua/vim/fs.lua b/runtime/lua/vim/fs.lua
index ee713e4b58..04a6e43db1 100644
--- a/runtime/lua/vim/fs.lua
+++ b/runtime/lua/vim/fs.lua
@@ -629,8 +629,8 @@ function M.normalize(path, opts)
return prefix .. path
end
- -- Remove extraneous slashes from the prefix
- prefix = prefix:gsub('/+', '/')
+ -- Ensure capital drive and remove extraneous slashes from the prefix
+ prefix = prefix:gsub('^%a:', string.upper):gsub('/+', '/')
end
if not opts._fast then
diff --git a/runtime/lua/vim/func.lua b/runtime/lua/vim/func.lua
index f71659ffb4..fc8fa62c71 100644
--- a/runtime/lua/vim/func.lua
+++ b/runtime/lua/vim/func.lua
@@ -3,9 +3,6 @@ local M = {}
-- TODO(lewis6991): Private for now until:
-- - There are other places in the codebase that could benefit from this
-- (e.g. LSP), but might require other changes to accommodate.
--- - Invalidation of the cache needs to be controllable. Using weak tables
--- is an acceptable invalidation policy, but it shouldn't be the only
--- one.
-- - I don't think the story around `hash` is completely thought out. We
-- may be able to have a good default hash by hashing each argument,
-- so basically a better 'concat'.
@@ -17,6 +14,10 @@ local M = {}
--- Internally uses a |lua-weaktable| to cache the results of {fn} meaning the
--- cache will be invalidated whenever Lua does garbage collection.
---
+--- The cache can also be manually invalidated by calling `:clear()` on the returned object.
+--- Calling this function with no arguments clears the entire cache; otherwise, the arguments will
+--- be interpreted as function inputs, and only the cache entry at their hash will be cleared.
+---
--- The memoized function returns shared references so be wary about
--- mutating return values.
---
@@ -32,11 +33,12 @@ local M = {}
--- first n arguments passed to {fn}.
---
--- @param fn F Function to memoize.
---- @param strong? boolean Do not use a weak table
+--- @param weak? boolean Use a weak table (default `true`)
--- @return F # Memoized version of {fn}
--- @nodoc
-function M._memoize(hash, fn, strong)
- return require('vim.func._memoize')(hash, fn, strong)
+function M._memoize(hash, fn, weak)
+ -- this is wrapped in a function to lazily require the module
+ return require('vim.func._memoize')(hash, fn, weak)
end
return M
diff --git a/runtime/lua/vim/func/_memoize.lua b/runtime/lua/vim/func/_memoize.lua
index 6e557905a7..c46f878067 100644
--- a/runtime/lua/vim/func/_memoize.lua
+++ b/runtime/lua/vim/func/_memoize.lua
@@ -1,5 +1,7 @@
--- Module for private utility functions
+--- @alias vim.func.MemoObj { _hash: (fun(...): any), _weak: boolean?, _cache: table<any> }
+
--- @param argc integer?
--- @return fun(...): any
local function concat_hash(argc)
@@ -33,29 +35,49 @@ local function resolve_hash(hash)
return hash
end
+--- @param weak boolean?
+--- @return table
+local create_cache = function(weak)
+ return setmetatable({}, {
+ __mode = weak ~= false and 'kv',
+ })
+end
+
--- @generic F: function
--- @param hash integer|string|fun(...): any
--- @param fn F
---- @param strong? boolean
+--- @param weak? boolean
--- @return F
-return function(hash, fn, strong)
+return function(hash, fn, weak)
vim.validate('hash', hash, { 'number', 'string', 'function' })
vim.validate('fn', fn, 'function')
+ vim.validate('weak', weak, 'boolean', true)
- ---@type table<any,table<any,any>>
- local cache = {}
- if not strong then
- setmetatable(cache, { __mode = 'kv' })
- end
-
- hash = resolve_hash(hash)
+ --- @type vim.func.MemoObj
+ local obj = {
+ _cache = create_cache(weak),
+ _hash = resolve_hash(hash),
+ _weak = weak,
+ --- @param self vim.func.MemoObj
+ clear = function(self, ...)
+ if select('#', ...) == 0 then
+ self._cache = create_cache(self._weak)
+ return
+ end
+ local key = self._hash(...)
+ self._cache[key] = nil
+ end,
+ }
- return function(...)
- local key = hash(...)
- if cache[key] == nil then
- cache[key] = vim.F.pack_len(fn(...))
- end
-
- return vim.F.unpack_len(cache[key])
- end
+ return setmetatable(obj, {
+ --- @param self vim.func.MemoObj
+ __call = function(self, ...)
+ local key = self._hash(...)
+ local cache = self._cache
+ if cache[key] == nil then
+ cache[key] = vim.F.pack_len(fn(...))
+ end
+ return vim.F.unpack_len(cache[key])
+ end,
+ })
end
diff --git a/runtime/lua/vim/health.lua b/runtime/lua/vim/health.lua
index 52a7a13966..6dc902489f 100644
--- a/runtime/lua/vim/health.lua
+++ b/runtime/lua/vim/health.lua
@@ -11,6 +11,17 @@
--- <
--- Plugin authors are encouraged to write new healthchecks. |health-dev|
---
+--- *g:health*
+--- g:health This global variable controls the behavior and appearance of the
+--- `health` floating window. It should be a dictionary containing the
+--- following optional keys:
+--- - `style`: string? Determines the display style of the `health` window.
+--- Set to `'float'` to enable a floating window. Other
+--- styles are not currently supported.
+---
+--- Example: >lua
+--- vim.g.health = { style = 'float' }
+---
--- Commands *health-commands*
---
--- *:che* *:checkhealth*
@@ -331,13 +342,31 @@ function M._check(mods, plugin_names)
local emptybuf = vim.fn.bufnr('$') == 1 and vim.fn.getline(1) == '' and 1 == vim.fn.line('$')
- -- When no command modifiers are used:
- -- - If the current buffer is empty, open healthcheck directly.
- -- - If not specified otherwise open healthcheck in a tab.
- local buf_cmd = #mods > 0 and (mods .. ' sbuffer') or emptybuf and 'buffer' or 'tab sbuffer'
-
local bufnr = vim.api.nvim_create_buf(true, true)
- vim.cmd(buf_cmd .. ' ' .. bufnr)
+ if
+ vim.g.health
+ and type(vim.g.health) == 'table'
+ and vim.tbl_get(vim.g.health, 'style') == 'float'
+ then
+ local max_height = math.floor(vim.o.lines * 0.8)
+ local max_width = 80
+ local float_bufnr, float_winid = vim.lsp.util.open_floating_preview({}, '', {
+ height = max_height,
+ width = max_width,
+ offset_x = math.floor((vim.o.columns - max_width) / 2),
+ offset_y = math.floor((vim.o.lines - max_height) / 2) - 1,
+ relative = 'editor',
+ })
+ vim.api.nvim_set_current_win(float_winid)
+ vim.bo[float_bufnr].modifiable = true
+ vim.wo[float_winid].list = false
+ else
+ -- When no command modifiers are used:
+ -- - If the current buffer is empty, open healthcheck directly.
+ -- - If not specified otherwise open healthcheck in a tab.
+ local buf_cmd = #mods > 0 and (mods .. ' sbuffer') or emptybuf and 'buffer' or 'tab sbuffer'
+ vim.cmd(buf_cmd .. ' ' .. bufnr)
+ end
if vim.fn.bufexists('health://') == 1 then
vim.cmd.bwipe('health://')
@@ -407,6 +436,17 @@ function M._check(mods, plugin_names)
-- Clear the 'Running healthchecks...' message.
vim.cmd.redraw()
vim.print('')
+
+ -- Quit with 'q' inside healthcheck buffers.
+ vim.keymap.set('n', 'q', function()
+ local ok, _ = pcall(vim.cmd.close)
+ if not ok then
+ vim.cmd.bdelete()
+ end
+ end, { buffer = bufnr, silent = true, noremap = true, nowait = true })
+
+ -- Once we're done writing checks, set nomodifiable.
+ vim.bo[bufnr].modifiable = false
end
return M
diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua
index 6bee5bc31f..5cccb3321f 100644
--- a/runtime/lua/vim/lsp/util.lua
+++ b/runtime/lua/vim/lsp/util.lua
@@ -875,15 +875,17 @@ function M.make_floating_popup_options(width, height, opts)
return {
anchor = anchor,
+ row = row + (opts.offset_y or 0),
col = col + (opts.offset_x or 0),
height = height,
focusable = opts.focusable,
- relative = opts.relative == 'mouse' and 'mouse' or 'cursor',
- row = row + (opts.offset_y or 0),
+ relative = opts.relative == 'mouse' and 'mouse'
+ or opts.relative == 'editor' and 'editor'
+ or 'cursor',
style = 'minimal',
width = width,
border = opts.border or default_border,
- zindex = opts.zindex or 50,
+ zindex = opts.zindex or (api.nvim_win_get_config(0).zindex or 49) + 1,
title = title,
title_pos = title_pos,
}
@@ -1494,7 +1496,7 @@ end
--- @field title_pos? 'left'|'center'|'right'
---
--- (default: `'cursor'`)
---- @field relative? 'mouse'|'cursor'
+--- @field relative? 'mouse'|'cursor'|'editor'
---
--- - "auto": place window based on which side of the cursor has more lines
--- - "above": place the window above the cursor unless there are not enough lines
diff --git a/runtime/lua/vim/treesitter/_fold.lua b/runtime/lua/vim/treesitter/_fold.lua
index 207ac1ab67..d16013eca2 100644
--- a/runtime/lua/vim/treesitter/_fold.lua
+++ b/runtime/lua/vim/treesitter/_fold.lua
@@ -19,14 +19,19 @@ local api = vim.api
---The range on which to evaluate foldexpr.
---When in insert mode, the evaluation is deferred to InsertLeave.
---@field foldupdate_range? Range2
+---
+---The treesitter parser associated with this buffer.
+---@field parser? vim.treesitter.LanguageTree
local FoldInfo = {}
FoldInfo.__index = FoldInfo
---@private
-function FoldInfo.new()
+---@param bufnr integer
+function FoldInfo.new(bufnr)
return setmetatable({
levels0 = {},
levels = {},
+ parser = ts.get_parser(bufnr, nil, { error = false }),
}, FoldInfo)
end
@@ -69,7 +74,10 @@ local function compute_folds_levels(bufnr, info, srow, erow, parse_injections)
srow = srow or 0
erow = erow or api.nvim_buf_line_count(bufnr)
- local parser = assert(ts.get_parser(bufnr, nil, { error = false }))
+ local parser = info.parser
+ if not parser then
+ return
+ end
parser:parse(parse_injections and { srow, erow } or nil)
@@ -347,13 +355,21 @@ function M.foldexpr(lnum)
lnum = lnum or vim.v.lnum
local bufnr = api.nvim_get_current_buf()
- local parser = ts.get_parser(bufnr, nil, { error = false })
- if not parser then
- return '0'
- end
-
if not foldinfos[bufnr] then
- foldinfos[bufnr] = FoldInfo.new()
+ foldinfos[bufnr] = FoldInfo.new(bufnr)
+ api.nvim_create_autocmd('BufUnload', {
+ buffer = bufnr,
+ once = true,
+ callback = function()
+ foldinfos[bufnr] = nil
+ end,
+ })
+
+ local parser = foldinfos[bufnr].parser
+ if not parser then
+ return '0'
+ end
+
compute_folds_levels(bufnr, foldinfos[bufnr])
parser:register_cbs({
@@ -383,7 +399,7 @@ api.nvim_create_autocmd('OptionSet', {
or foldinfos[buf] and { buf }
or {}
for _, bufnr in ipairs(bufs) do
- foldinfos[bufnr] = FoldInfo.new()
+ foldinfos[bufnr] = FoldInfo.new(bufnr)
api.nvim_buf_call(bufnr, function()
compute_folds_levels(bufnr, foldinfos[bufnr])
end)
diff --git a/runtime/lua/vim/treesitter/highlighter.lua b/runtime/lua/vim/treesitter/highlighter.lua
index 8ce8652f7d..96503c38ea 100644
--- a/runtime/lua/vim/treesitter/highlighter.lua
+++ b/runtime/lua/vim/treesitter/highlighter.lua
@@ -299,6 +299,8 @@ local function on_line_impl(self, buf, line, is_spell_nav)
state.highlighter_query:query():iter_captures(root_node, self.bufnr, line, root_end_row + 1)
end
+ local captures = state.highlighter_query:query().captures
+
while line >= state.next_row do
local capture, node, metadata, match = state.iter(line)
@@ -311,7 +313,7 @@ local function on_line_impl(self, buf, line, is_spell_nav)
if capture then
local hl = state.highlighter_query:get_hl_from_capture(capture)
- local capture_name = state.highlighter_query:query().captures[capture]
+ local capture_name = captures[capture]
local spell, spell_pri_offset = get_spell(capture_name)
diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua
index 4b42164dc8..330eb45749 100644
--- a/runtime/lua/vim/treesitter/languagetree.lua
+++ b/runtime/lua/vim/treesitter/languagetree.lua
@@ -443,7 +443,7 @@ function LanguageTree:parse(range)
end
end
- if not self._injections_processed and range ~= false and range ~= nil then
+ if not self._injections_processed and range then
query_time = self:_add_injections()
self._injections_processed = true
end
diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua
index f9c497337f..1fc001b39f 100644
--- a/runtime/lua/vim/treesitter/query.lua
+++ b/runtime/lua/vim/treesitter/query.lua
@@ -7,6 +7,59 @@ local memoize = vim.func._memoize
local M = {}
+local function is_directive(name)
+ return string.sub(name, -1) == '!'
+end
+
+---@nodoc
+---@class vim.treesitter.query.ProcessedPredicate
+---@field [1] string predicate name
+---@field [2] boolean should match
+---@field [3] (integer|string)[] the original predicate
+
+---@alias vim.treesitter.query.ProcessedDirective (integer|string)[]
+
+---@nodoc
+---@class vim.treesitter.query.ProcessedPattern {
+---@field predicates vim.treesitter.query.ProcessedPredicate[]
+---@field directives vim.treesitter.query.ProcessedDirective[]
+
+--- Splits the query patterns into predicates and directives.
+---@param patterns table<integer, (integer|string)[][]>
+---@return table<integer, vim.treesitter.query.ProcessedPattern>
+local function process_patterns(patterns)
+ ---@type table<integer, vim.treesitter.query.ProcessedPattern>
+ local processed_patterns = {}
+
+ for k, pattern_list in pairs(patterns) do
+ ---@type vim.treesitter.query.ProcessedPredicate[]
+ local predicates = {}
+ ---@type vim.treesitter.query.ProcessedDirective[]
+ local directives = {}
+
+ for _, pattern in ipairs(pattern_list) do
+ -- Note: tree-sitter strips the leading # from predicates for us.
+ local pred_name = pattern[1]
+ ---@cast pred_name string
+
+ if is_directive(pred_name) then
+ table.insert(directives, pattern)
+ else
+ local should_match = true
+ if pred_name:match('^not%-') then
+ pred_name = pred_name:sub(5)
+ should_match = false
+ end
+ table.insert(predicates, { pred_name, should_match, pattern })
+ end
+ end
+
+ processed_patterns[k] = { predicates = predicates, directives = directives }
+ end
+
+ return processed_patterns
+end
+
---@nodoc
---Parsed query, see |vim.treesitter.query.parse()|
---
@@ -15,6 +68,7 @@ local M = {}
---@field captures string[] list of (unique) capture names defined in query
---@field info vim.treesitter.QueryInfo query context (e.g. captures, predicates, directives)
---@field query TSQuery userdata query object
+---@field private _processed_patterns table<integer, vim.treesitter.query.ProcessedPattern>
local Query = {}
Query.__index = Query
@@ -33,6 +87,7 @@ function Query.new(lang, ts_query)
patterns = query_info.patterns,
}
self.captures = self.info.captures
+ self._processed_patterns = process_patterns(self.info.patterns)
return self
end
@@ -751,84 +806,50 @@ function M.list_predicates()
return vim.tbl_keys(predicate_handlers)
end
-local function xor(x, y)
- return (x or y) and not (x and y)
-end
-
-local function is_directive(name)
- return string.sub(name, -1) == '!'
-end
-
---@private
----@param match TSQueryMatch
+---@param pattern_i integer
+---@param predicates vim.treesitter.query.ProcessedPredicate[]
+---@param captures table<integer, TSNode[]>
---@param source integer|string
-function Query:match_preds(match, source)
- local _, pattern = match:info()
- local preds = self.info.patterns[pattern]
-
- if not preds then
- return true
- end
-
- local captures = match:captures()
-
- for _, pred in pairs(preds) do
- -- Here we only want to return if a predicate DOES NOT match, and
- -- continue on the other case. This way unknown predicates will not be considered,
- -- which allows some testing and easier user extensibility (#12173).
- -- Also, tree-sitter strips the leading # from predicates for us.
- local is_not = false
-
- -- Skip over directives... they will get processed after all the predicates.
- if not is_directive(pred[1]) then
- local pred_name = pred[1]
- if pred_name:match('^not%-') then
- pred_name = pred_name:sub(5)
- is_not = true
- end
-
- local handler = predicate_handlers[pred_name]
-
- if not handler then
- error(string.format('No handler for %s', pred[1]))
- return false
- end
-
- local pred_matches = handler(captures, pattern, source, pred)
+---@return boolean whether the predicates match
+function Query:_match_predicates(predicates, pattern_i, captures, source)
+ for _, predicate in ipairs(predicates) do
+ local processed_name = predicate[1]
+ local should_match = predicate[2]
+ local orig_predicate = predicate[3]
+
+ local handler = predicate_handlers[processed_name]
+ if not handler then
+ error(string.format('No handler for %s', orig_predicate[1]))
+ return false
+ end
- if not xor(is_not, pred_matches) then
- return false
- end
+ local does_match = handler(captures, pattern_i, source, orig_predicate)
+ if does_match ~= should_match then
+ return false
end
end
return true
end
---@private
----@param match TSQueryMatch
+---@param pattern_i integer
+---@param directives vim.treesitter.query.ProcessedDirective[]
+---@param source integer|string
+---@param captures table<integer, TSNode[]>
---@return vim.treesitter.query.TSMetadata metadata
-function Query:apply_directives(match, source)
+function Query:_apply_directives(directives, pattern_i, captures, source)
---@type vim.treesitter.query.TSMetadata
local metadata = {}
- local _, pattern = match:info()
- local preds = self.info.patterns[pattern]
- if not preds then
- return metadata
- end
+ for _, directive in pairs(directives) do
+ local handler = directive_handlers[directive[1]]
- local captures = match:captures()
-
- for _, pred in pairs(preds) do
- if is_directive(pred[1]) then
- local handler = directive_handlers[pred[1]]
-
- if not handler then
- error(string.format('No handler for %s', pred[1]))
- end
-
- handler(captures, pattern, source, pred, metadata)
+ if not handler then
+ error(string.format('No handler for %s', directive[1]))
end
+
+ handler(captures, pattern_i, source, directive, metadata)
end
return metadata
@@ -852,12 +873,6 @@ local function value_or_node_range(start, stop, node)
return start, stop
end
---- @param match TSQueryMatch
---- @return integer
-local function match_id_hash(_, match)
- return (match:info())
-end
-
--- Iterates over all captures from all matches in {node}.
---
--- {source} is required if the query contains predicates; then the caller
@@ -902,8 +917,10 @@ function Query:iter_captures(node, source, start, stop)
local cursor = vim._create_ts_querycursor(node, self.query, start, stop, { match_limit = 256 })
- local apply_directives = memoize(match_id_hash, self.apply_directives, true)
- local match_preds = memoize(match_id_hash, self.match_preds, true)
+ -- For faster checks that a match is not in the cache.
+ local highest_cached_match_id = -1
+ ---@type table<integer, vim.treesitter.query.TSMetadata>
+ local match_cache = {}
local function iter(end_line)
local capture, captured_node, match = cursor:next_capture()
@@ -912,16 +929,37 @@ function Query:iter_captures(node, source, start, stop)
return
end
- if not match_preds(self, match, source) then
- local match_id = match:info()
- cursor:remove_match(match_id)
- if end_line and captured_node:range() > end_line then
- return nil, captured_node, nil, nil
- end
- return iter(end_line) -- tail call: try next match
+ local match_id, pattern_i = match:info()
+
+ --- @type vim.treesitter.query.TSMetadata
+ local metadata
+ if match_id <= highest_cached_match_id then
+ metadata = match_cache[match_id]
end
- local metadata = apply_directives(self, match, source)
+ if not metadata then
+ metadata = {}
+
+ local processed_pattern = self._processed_patterns[pattern_i]
+ if processed_pattern then
+ local captures = match:captures()
+
+ local predicates = processed_pattern.predicates
+ if not self:_match_predicates(predicates, pattern_i, captures, source) then
+ cursor:remove_match(match_id)
+ if end_line and captured_node:range() > end_line then
+ return nil, captured_node, nil, nil
+ end
+ return iter(end_line) -- tail call: try next match
+ end
+
+ local directives = processed_pattern.directives
+ metadata = self:_apply_directives(directives, pattern_i, captures, source)
+ end
+
+ highest_cached_match_id = math.max(highest_cached_match_id, match_id)
+ match_cache[match_id] = metadata
+ end
return capture, captured_node, metadata, match
end
@@ -984,17 +1022,22 @@ function Query:iter_matches(node, source, start, stop, opts)
return
end
- local match_id, pattern = match:info()
+ local match_id, pattern_i = match:info()
+ local processed_pattern = self._processed_patterns[pattern_i]
+ local captures = match:captures()
- if not self:match_preds(match, source) then
- cursor:remove_match(match_id)
- return iter() -- tail call: try next match
+ --- @type vim.treesitter.query.TSMetadata
+ local metadata = {}
+ if processed_pattern then
+ local predicates = processed_pattern.predicates
+ if not self:_match_predicates(predicates, pattern_i, captures, source) then
+ cursor:remove_match(match_id)
+ return iter() -- tail call: try next match
+ end
+ local directives = processed_pattern.directives
+ metadata = self:_apply_directives(directives, pattern_i, captures, source)
end
- local metadata = self:apply_directives(match, source)
-
- local captures = match:captures()
-
if opts.all == false then
-- Convert the match table into the old buggy version for backward
-- compatibility. This is slow, but we only do it when the caller explicitly opted into it by
@@ -1003,11 +1046,11 @@ function Query:iter_matches(node, source, start, stop, opts)
for k, v in pairs(captures or {}) do
old_match[k] = v[#v]
end
- return pattern, old_match, metadata
+ return pattern_i, old_match, metadata
end
-- TODO(lewis6991): create a new function that returns {match, metadata}
- return pattern, captures, metadata
+ return pattern_i, captures, metadata
end
return iter
end