diff options
Diffstat (limited to 'runtime/lua/vim')
-rw-r--r-- | runtime/lua/vim/_meta/api_keysets.lua | 6 | ||||
-rw-r--r-- | runtime/lua/vim/_meta/vimfn.lua | 14 | ||||
-rw-r--r-- | runtime/lua/vim/_meta/vvars.lua | 12 | ||||
-rw-r--r-- | runtime/lua/vim/_options.lua | 6 | ||||
-rw-r--r-- | runtime/lua/vim/filetype.lua | 3 | ||||
-rw-r--r-- | runtime/lua/vim/filetype/detect.lua | 15 | ||||
-rw-r--r-- | runtime/lua/vim/fs.lua | 4 | ||||
-rw-r--r-- | runtime/lua/vim/func.lua | 14 | ||||
-rw-r--r-- | runtime/lua/vim/func/_memoize.lua | 56 | ||||
-rw-r--r-- | runtime/lua/vim/health.lua | 52 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/util.lua | 10 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/_fold.lua | 34 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/highlighter.lua | 4 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/languagetree.lua | 2 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/query.lua | 221 |
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 |