diff options
| author | Josh Rahm <joshuarahm@gmail.com> | 2025-02-05 23:09:29 +0000 |
|---|---|---|
| committer | Josh Rahm <joshuarahm@gmail.com> | 2025-02-05 23:09:29 +0000 |
| commit | d5f194ce780c95821a855aca3c19426576d28ae0 (patch) | |
| tree | d45f461b19f9118ad2bb1f440a7a08973ad18832 /runtime/lua/vim/treesitter | |
| parent | c5d770d311841ea5230426cc4c868e8db27300a8 (diff) | |
| parent | 44740e561fc93afe3ebecfd3618bda2d2abeafb0 (diff) | |
| download | rneovim-rahm.tar.gz rneovim-rahm.tar.bz2 rneovim-rahm.zip | |
Diffstat (limited to 'runtime/lua/vim/treesitter')
| -rw-r--r-- | runtime/lua/vim/treesitter/_fold.lua | 325 | ||||
| -rw-r--r-- | runtime/lua/vim/treesitter/_meta/misc.lua | 8 | ||||
| -rw-r--r-- | runtime/lua/vim/treesitter/_meta/tsnode.lua | 16 | ||||
| -rw-r--r-- | runtime/lua/vim/treesitter/_query_linter.lua | 6 | ||||
| -rw-r--r-- | runtime/lua/vim/treesitter/dev.lua | 25 | ||||
| -rw-r--r-- | runtime/lua/vim/treesitter/highlighter.lua | 30 | ||||
| -rw-r--r-- | runtime/lua/vim/treesitter/language.lua | 7 | ||||
| -rw-r--r-- | runtime/lua/vim/treesitter/languagetree.lua | 356 | ||||
| -rw-r--r-- | runtime/lua/vim/treesitter/query.lua | 418 |
9 files changed, 749 insertions, 442 deletions
diff --git a/runtime/lua/vim/treesitter/_fold.lua b/runtime/lua/vim/treesitter/_fold.lua index 7237d2e7d4..38318347a7 100644 --- a/runtime/lua/vim/treesitter/_fold.lua +++ b/runtime/lua/vim/treesitter/_fold.lua @@ -19,76 +19,36 @@ 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 ---- Efficiently remove items from middle of a list a list. ---- ---- Calling table.remove() in a loop will re-index the tail of the table on ---- every iteration, instead this function will re-index the table exactly ---- once. ---- ---- Based on https://stackoverflow.com/questions/12394841/safely-remove-items-from-an-array-table-while-iterating/53038524#53038524 ---- ----@param t any[] ----@param first integer ----@param last integer -local function list_remove(t, first, last) - local n = #t - for i = 0, n - first do - t[first + i] = t[last + 1 + i] - t[last + 1 + i] = nil - end -end - ---@package ---@param srow integer ---@param erow integer 0-indexed, exclusive function FoldInfo:remove_range(srow, erow) - list_remove(self.levels, srow + 1, erow) - list_remove(self.levels0, srow + 1, erow) -end - ---- Efficiently insert items into the middle of a list. ---- ---- Calling table.insert() in a loop will re-index the tail of the table on ---- every iteration, instead this function will re-index the table exactly ---- once. ---- ---- Based on https://stackoverflow.com/questions/12394841/safely-remove-items-from-an-array-table-while-iterating/53038524#53038524 ---- ----@param t any[] ----@param first integer ----@param last integer ----@param v any -local function list_insert(t, first, last, v) - local n = #t - - -- Shift table forward - for i = n - first, 0, -1 do - t[last + 1 + i] = t[first + i] - end - - -- Fill in new values - for i = first, last do - t[i] = v - end + vim._list_remove(self.levels, srow + 1, erow) + vim._list_remove(self.levels0, srow + 1, erow) end ---@package ---@param srow integer ---@param erow integer 0-indexed, exclusive function FoldInfo:add_range(srow, erow) - list_insert(self.levels, srow + 1, erow, -1) - list_insert(self.levels0, srow + 1, erow, -1) + vim._list_insert(self.levels, srow + 1, erow, -1) + vim._list_insert(self.levels0, srow + 1, erow, -1) end ---@param range Range2 @@ -109,111 +69,122 @@ end ---@param info TS.FoldInfo ---@param srow integer? ---@param erow integer? 0-indexed, exclusive ----@param parse_injections? boolean -local function compute_folds_levels(bufnr, info, srow, erow, parse_injections) +---@param callback function? +local function compute_folds_levels(bufnr, info, srow, erow, callback) srow = srow or 0 erow = erow or api.nvim_buf_line_count(bufnr) - local parser = assert(ts.get_parser(bufnr, nil, { error = false })) - - parser:parse(parse_injections and { srow, erow } or nil) - - local enter_counts = {} ---@type table<integer, integer> - local leave_counts = {} ---@type table<integer, integer> - local prev_start = -1 - local prev_stop = -1 + local parser = info.parser + if not parser then + return + end - parser:for_each_tree(function(tree, ltree) - local query = ts.query.get(ltree:lang(), 'folds') - if not query then + parser:parse(nil, function(_, trees) + if not trees then return end - -- Collect folds starting from srow - 1, because we should first subtract the folds that end at - -- srow - 1 from the level of srow - 1 to get accurate level of srow. - for _, match, metadata in query:iter_matches(tree:root(), bufnr, math.max(srow - 1, 0), erow) do - for id, nodes in pairs(match) do - if query.captures[id] == 'fold' then - local range = ts.get_range(nodes[1], bufnr, metadata[id]) - local start, _, stop, stop_col = Range.unpack4(range) - - if #nodes > 1 then - -- assumes nodes are ordered by range - local end_range = ts.get_range(nodes[#nodes], bufnr, metadata[id]) - local _, _, end_stop, end_stop_col = Range.unpack4(end_range) - stop = end_stop - stop_col = end_stop_col - end + local enter_counts = {} ---@type table<integer, integer> + local leave_counts = {} ---@type table<integer, integer> + local prev_start = -1 + local prev_stop = -1 - if stop_col == 0 then - stop = stop - 1 - end + parser:for_each_tree(function(tree, ltree) + local query = ts.query.get(ltree:lang(), 'folds') + if not query then + return + end - local fold_length = stop - start + 1 - - -- Fold only multiline nodes that are not exactly the same as previously met folds - -- Checking against just the previously found fold is sufficient if nodes - -- are returned in preorder or postorder when traversing tree - if - fold_length > vim.wo.foldminlines and not (start == prev_start and stop == prev_stop) - then - enter_counts[start + 1] = (enter_counts[start + 1] or 0) + 1 - leave_counts[stop + 1] = (leave_counts[stop + 1] or 0) + 1 - prev_start = start - prev_stop = stop + -- Collect folds starting from srow - 1, because we should first subtract the folds that end at + -- srow - 1 from the level of srow - 1 to get accurate level of srow. + for _, match, metadata in query:iter_matches(tree:root(), bufnr, math.max(srow - 1, 0), erow) do + for id, nodes in pairs(match) do + if query.captures[id] == 'fold' then + local range = ts.get_range(nodes[1], bufnr, metadata[id]) + local start, _, stop, stop_col = Range.unpack4(range) + + if #nodes > 1 then + -- assumes nodes are ordered by range + local end_range = ts.get_range(nodes[#nodes], bufnr, metadata[id]) + local _, _, end_stop, end_stop_col = Range.unpack4(end_range) + stop = end_stop + stop_col = end_stop_col + end + + if stop_col == 0 then + stop = stop - 1 + end + + local fold_length = stop - start + 1 + + -- Fold only multiline nodes that are not exactly the same as previously met folds + -- Checking against just the previously found fold is sufficient if nodes + -- are returned in preorder or postorder when traversing tree + if + fold_length > vim.wo.foldminlines and not (start == prev_start and stop == prev_stop) + then + enter_counts[start + 1] = (enter_counts[start + 1] or 0) + 1 + leave_counts[stop + 1] = (leave_counts[stop + 1] or 0) + 1 + prev_start = start + prev_stop = stop + end end end end - end - end) + end) - local nestmax = vim.wo.foldnestmax - local level0_prev = info.levels0[srow] or 0 - local leave_prev = leave_counts[srow] or 0 - - -- We now have the list of fold opening and closing, fill the gaps and mark where fold start - for lnum = srow + 1, erow do - local enter_line = enter_counts[lnum] or 0 - local leave_line = leave_counts[lnum] or 0 - local level0 = level0_prev - leave_prev + enter_line - - -- Determine if it's the start/end of a fold - -- NB: vim's fold-expr interface does not have a mechanism to indicate that - -- two (or more) folds start at this line, so it cannot distinguish between - -- ( \n ( \n )) \n (( \n ) \n ) - -- versus - -- ( \n ( \n ) \n ( \n ) \n ) - -- Both are represented by ['>1', '>2', '2', '>2', '2', '1'], and - -- vim interprets as the second case. - -- If it did have such a mechanism, (clamped - clamped_prev) - -- would be the correct number of starts to pass on. - local adjusted = level0 ---@type integer - local prefix = '' - if enter_line > 0 then - prefix = '>' - if leave_line > 0 then - -- If this line ends a fold f1 and starts a fold f2, then move f1's end to the previous line - -- so that f2 gets the correct level on this line. This may reduce the size of f1 below - -- foldminlines, but we don't handle it for simplicity. - adjusted = level0 - leave_line - leave_line = 0 + local nestmax = vim.wo.foldnestmax + local level0_prev = info.levels0[srow] or 0 + local leave_prev = leave_counts[srow] or 0 + + -- We now have the list of fold opening and closing, fill the gaps and mark where fold start + for lnum = srow + 1, erow do + local enter_line = enter_counts[lnum] or 0 + local leave_line = leave_counts[lnum] or 0 + local level0 = level0_prev - leave_prev + enter_line + + -- Determine if it's the start/end of a fold + -- NB: vim's fold-expr interface does not have a mechanism to indicate that + -- two (or more) folds start at this line, so it cannot distinguish between + -- ( \n ( \n )) \n (( \n ) \n ) + -- versus + -- ( \n ( \n ) \n ( \n ) \n ) + -- Both are represented by ['>1', '>2', '2', '>2', '2', '1'], and + -- vim interprets as the second case. + -- If it did have such a mechanism, (clamped - clamped_prev) + -- would be the correct number of starts to pass on. + local adjusted = level0 ---@type integer + local prefix = '' + if enter_line > 0 then + prefix = '>' + if leave_line > 0 then + -- If this line ends a fold f1 and starts a fold f2, then move f1's end to the previous line + -- so that f2 gets the correct level on this line. This may reduce the size of f1 below + -- foldminlines, but we don't handle it for simplicity. + adjusted = level0 - leave_line + leave_line = 0 + end end - end - -- Clamp at foldnestmax. - local clamped = adjusted - if adjusted > nestmax then - prefix = '' - clamped = nestmax - end + -- Clamp at foldnestmax. + local clamped = adjusted + if adjusted > nestmax then + prefix = '' + clamped = nestmax + end - -- Record the "real" level, so that it can be used as "base" of later compute_folds_levels(). - info.levels0[lnum] = adjusted - info.levels[lnum] = prefix .. tostring(clamped) + -- Record the "real" level, so that it can be used as "base" of later compute_folds_levels(). + info.levels0[lnum] = adjusted + info.levels[lnum] = prefix .. tostring(clamped) - leave_prev = leave_line - level0_prev = adjusted - end + leave_prev = leave_line + level0_prev = adjusted + end + + if callback then + callback() + end + end) end local M = {} @@ -221,7 +192,7 @@ local M = {} ---@type table<integer,TS.FoldInfo> local foldinfos = {} -local group = api.nvim_create_augroup('treesitter/fold', {}) +local group = api.nvim_create_augroup('nvim.treesitter.fold', {}) --- Update the folds in the windows that contain the buffer and use expr foldmethod (assuming that --- the user doesn't use different foldexpr for the same buffer). @@ -298,12 +269,19 @@ local function schedule_if_loaded(bufnr, fn) end ---@param bufnr integer ----@param foldinfo TS.FoldInfo ---@param tree_changes Range4[] -local function on_changedtree(bufnr, foldinfo, tree_changes) +local function on_changedtree(bufnr, tree_changes) schedule_if_loaded(bufnr, function() + -- Buffer reload clears `foldinfos[bufnr]`, which may still be nil when callback is invoked. + local foldinfo = foldinfos[bufnr] + if not foldinfo then + return + end + local srow_upd, erow_upd ---@type integer?, integer? local max_erow = api.nvim_buf_line_count(bufnr) + -- TODO(ribru17): Replace this with a proper .all() awaiter once #19624 is resolved + local iterations = 0 for _, change in ipairs(tree_changes) do local srow, _, erow, ecol = Range.unpack4(change) -- If a parser doesn't have any ranges explicitly set, treesitter will @@ -317,24 +295,31 @@ local function on_changedtree(bufnr, foldinfo, tree_changes) end -- Start from `srow - foldminlines`, because this edit may have shrunken the fold below limit. srow = math.max(srow - vim.wo.foldminlines, 0) - compute_folds_levels(bufnr, foldinfo, srow, erow) srow_upd = srow_upd and math.min(srow_upd, srow) or srow erow_upd = erow_upd and math.max(erow_upd, erow) or erow - end - if #tree_changes > 0 then - foldinfo:foldupdate(bufnr, srow_upd, erow_upd) + compute_folds_levels(bufnr, foldinfo, srow, erow, function() + iterations = iterations + 1 + if iterations == #tree_changes then + foldinfo:foldupdate(bufnr, srow_upd, erow_upd) + end + end) end end) end ---@param bufnr integer ----@param foldinfo TS.FoldInfo ---@param start_row integer ---@param old_row integer ---@param old_col integer ---@param new_row integer ---@param new_col integer -local function on_bytes(bufnr, foldinfo, start_row, start_col, old_row, old_col, new_row, new_col) +local function on_bytes(bufnr, start_row, start_col, old_row, old_col, new_row, new_col) + -- Buffer reload clears `foldinfos[bufnr]`, which may still be nil when callback is invoked. + local foldinfo = foldinfos[bufnr] + if not foldinfo then + return + end + -- extend the end to fully include the range local end_row_old = start_row + old_row + 1 local end_row_new = start_row + new_row + 1 @@ -373,15 +358,16 @@ local function on_bytes(bufnr, foldinfo, start_row, start_col, old_row, old_col, -- is invoked. For example, `J` with non-zero count triggers multiple on_bytes before executing -- the scheduled callback. So we accumulate the edited ranges in `on_bytes_range`. schedule_if_loaded(bufnr, function() - if not foldinfo.on_bytes_range then + if not (foldinfo.on_bytes_range and foldinfos[bufnr]) then return end local srow, erow = foldinfo.on_bytes_range[1], foldinfo.on_bytes_range[2] foldinfo.on_bytes_range = nil -- Start from `srow - foldminlines`, because this edit may have shrunken the fold below limit. srow = math.max(srow - vim.wo.foldminlines, 0) - compute_folds_levels(bufnr, foldinfo, srow, erow) - foldinfo:foldupdate(bufnr, srow, erow) + compute_folds_levels(bufnr, foldinfo, srow, erow, function() + foldinfo:foldupdate(bufnr, srow, erow) + end) end) end end @@ -392,22 +378,30 @@ 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', 'VimEnter' }, { + 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({ on_changedtree = function(tree_changes) - on_changedtree(bufnr, foldinfos[bufnr], tree_changes) + on_changedtree(bufnr, tree_changes) end, on_bytes = function(_, _, start_row, start_col, _, old_row, old_col, _, new_row, new_col, _) - on_bytes(bufnr, foldinfos[bufnr], start_row, start_col, old_row, old_col, new_row, new_col) + on_bytes(bufnr, start_row, start_col, old_row, old_col, new_row, new_col) end, on_detach = function() @@ -423,10 +417,17 @@ api.nvim_create_autocmd('OptionSet', { pattern = { 'foldminlines', 'foldnestmax' }, desc = 'Refresh treesitter folds', callback = function() - for bufnr, _ in pairs(foldinfos) do - foldinfos[bufnr] = FoldInfo.new() - compute_folds_levels(bufnr, foldinfos[bufnr]) - foldinfos[bufnr]:foldupdate(bufnr, 0, api.nvim_buf_line_count(bufnr)) + local buf = api.nvim_get_current_buf() + local bufs = vim.v.option_type == 'global' and vim.tbl_keys(foldinfos) + or foldinfos[buf] and { buf } + or {} + for _, bufnr in ipairs(bufs) do + foldinfos[bufnr] = FoldInfo.new(bufnr) + api.nvim_buf_call(bufnr, function() + compute_folds_levels(bufnr, foldinfos[bufnr], nil, nil, function() + foldinfos[bufnr]:foldupdate(bufnr, 0, api.nvim_buf_line_count(bufnr)) + end) + end) end end, }) diff --git a/runtime/lua/vim/treesitter/_meta/misc.lua b/runtime/lua/vim/treesitter/_meta/misc.lua index 33701ef254..c532257f49 100644 --- a/runtime/lua/vim/treesitter/_meta/misc.lua +++ b/runtime/lua/vim/treesitter/_meta/misc.lua @@ -20,9 +20,15 @@ error('Cannot require a meta file') ---@class (exact) TSQueryInfo ---@field captures string[] ---@field patterns table<integer, (integer|string)[][]> +--- +---@class TSLangInfo +---@field fields string[] +---@field symbols table<string,boolean> +---@field _wasm boolean +---@field _abi_version integer --- @param lang string ---- @return table +--- @return TSLangInfo vim._ts_inspect_language = function(lang) end ---@return integer diff --git a/runtime/lua/vim/treesitter/_meta/tsnode.lua b/runtime/lua/vim/treesitter/_meta/tsnode.lua index d982b6a505..552905c3f0 100644 --- a/runtime/lua/vim/treesitter/_meta/tsnode.lua +++ b/runtime/lua/vim/treesitter/_meta/tsnode.lua @@ -68,12 +68,6 @@ function TSNode:named_child_count() end --- @return TSNode? function TSNode:named_child(index) end ---- Get the node's child that contains {descendant}. ---- @param descendant TSNode ---- @return TSNode? ---- @deprecated -function TSNode:child_containing_descendant(descendant) end - --- Get the node's child that contains {descendant} (includes {descendant}). --- --- For example, with the following node hierarchy: @@ -109,17 +103,9 @@ function TSNode:end_() end --- - end row --- - end column --- - end byte (if {include_bytes} is `true`) ---- @param include_bytes boolean? -function TSNode:range(include_bytes) end - ---- @nodoc --- @param include_bytes false? --- @return integer, integer, integer, integer -function TSNode:range(include_bytes) end - ---- @nodoc ---- @param include_bytes true ---- @return integer, integer, integer, integer, integer, integer +--- @overload fun(self: TSNode, include_bytes: true): integer, integer, integer, integer, integer, integer function TSNode:range(include_bytes) end --- Get the node's type as a string. diff --git a/runtime/lua/vim/treesitter/_query_linter.lua b/runtime/lua/vim/treesitter/_query_linter.lua index a825505378..3dfc6b0cfe 100644 --- a/runtime/lua/vim/treesitter/_query_linter.lua +++ b/runtime/lua/vim/treesitter/_query_linter.lua @@ -1,6 +1,6 @@ local api = vim.api -local namespace = api.nvim_create_namespace('vim.treesitter.query_linter') +local namespace = api.nvim_create_namespace('nvim.treesitter.query_linter') local M = {} @@ -138,7 +138,9 @@ local function lint_match(buf, match, query, lang_context, diagnostics) -- perform language-independent checks only for first lang if lang_context.is_first_lang and cap_id == 'error' then local node_text = vim.treesitter.get_node_text(node, buf):gsub('\n', ' ') - add_lint_for_node(diagnostics, { node:range() }, 'Syntax error: ' .. node_text) + ---@diagnostic disable-next-line: missing-fields LuaLS varargs bug + local range = { node:range() } --- @type Range4 + add_lint_for_node(diagnostics, range, 'Syntax error: ' .. node_text) end -- other checks rely on Neovim parser introspection diff --git a/runtime/lua/vim/treesitter/dev.lua b/runtime/lua/vim/treesitter/dev.lua index 26817cdba5..24dd8243db 100644 --- a/runtime/lua/vim/treesitter/dev.lua +++ b/runtime/lua/vim/treesitter/dev.lua @@ -119,7 +119,7 @@ function TSTreeView:new(bufnr, lang) end local t = { - ns = api.nvim_create_namespace('treesitter/dev-inspect'), + ns = api.nvim_create_namespace('nvim.treesitter.dev_inspect'), nodes = nodes, named = named, ---@type vim.treesitter.dev.TSTreeViewOpts @@ -135,15 +135,7 @@ function TSTreeView:new(bufnr, lang) return t end -local decor_ns = api.nvim_create_namespace('ts.dev') - ----@param range Range4 ----@return string -local function range_to_string(range) - ---@type integer, integer, integer, integer - local row, col, end_row, end_col = unpack(range) - return string.format('[%d, %d] - [%d, %d]', row, col, end_row, end_col) -end +local decor_ns = api.nvim_create_namespace('nvim.treesitter.dev') ---@param w integer ---@return boolean closed Whether the window was closed. @@ -227,14 +219,17 @@ function TSTreeView:draw(bufnr) local lang_hl_marks = {} ---@type table[] for i, item in self:iter() do - local range_str = range_to_string({ item.node:range() }) + local range_str = ('[%d, %d] - [%d, %d]'):format(item.node:range()) local lang_str = self.opts.lang and string.format(' %s', item.lang) or '' local text ---@type string if item.node:named() then - text = string.format('(%s', item.node:type()) + text = string.format('(%s%s', item.node:missing() and 'MISSING ' or '', item.node:type()) else text = string.format('%q', item.node:type()):gsub('\n', 'n') + if item.node:missing() then + text = string.format('(MISSING %s)', text) + end end if item.field then text = string.format('%s: %s', item.field, text) @@ -442,7 +437,7 @@ function M.inspect_tree(opts) end, }) - local group = api.nvim_create_augroup('treesitter/dev', {}) + local group = api.nvim_create_augroup('nvim.treesitter.dev', {}) api.nvim_create_autocmd('CursorMoved', { group = group, @@ -547,7 +542,7 @@ function M.inspect_tree(opts) }) end -local edit_ns = api.nvim_create_namespace('treesitter/dev-edit') +local edit_ns = api.nvim_create_namespace('nvim.treesitter.dev_edit') ---@param query_win integer ---@param base_win integer @@ -633,7 +628,7 @@ function M.edit_query(lang) -- can infer the language later. api.nvim_buf_set_name(query_buf, string.format('%s/query_editor.scm', lang)) - local group = api.nvim_create_augroup('treesitter/dev-edit', {}) + local group = api.nvim_create_augroup('nvim.treesitter.dev_edit', {}) api.nvim_create_autocmd({ 'TextChanged', 'InsertLeave' }, { group = group, buffer = query_buf, diff --git a/runtime/lua/vim/treesitter/highlighter.lua b/runtime/lua/vim/treesitter/highlighter.lua index 8ce8652f7d..6dd47811bd 100644 --- a/runtime/lua/vim/treesitter/highlighter.lua +++ b/runtime/lua/vim/treesitter/highlighter.lua @@ -2,7 +2,7 @@ local api = vim.api local query = vim.treesitter.query local Range = require('vim.treesitter._range') -local ns = api.nvim_create_namespace('treesitter/highlighter') +local ns = api.nvim_create_namespace('nvim.treesitter.highlighter') ---@alias vim.treesitter.highlighter.Iter fun(end_line: integer|nil): integer, TSNode, vim.treesitter.query.TSMetadata, TSQueryMatch @@ -69,6 +69,7 @@ end ---@field private _queries table<string,vim.treesitter.highlighter.Query> ---@field tree vim.treesitter.LanguageTree ---@field private redraw_count integer +---@field parsing boolean true if we are parsing asynchronously local TSHighlighter = { active = {}, } @@ -147,8 +148,6 @@ function TSHighlighter.new(tree, opts) vim.opt_local.spelloptions:append('noplainbuffer') end) - self.tree:parse() - return self end @@ -161,7 +160,10 @@ function TSHighlighter:destroy() vim.bo[self.bufnr].spelloptions = self.orig_spelloptions vim.b[self.bufnr].ts_highlight = nil if vim.g.syntax_on == 1 then - api.nvim_exec_autocmds('FileType', { group = 'syntaxset', buffer = self.bufnr }) + api.nvim_exec_autocmds( + 'FileType', + { group = 'syntaxset', buffer = self.bufnr, modeline = false } + ) end end end @@ -299,6 +301,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 +315,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) @@ -382,19 +386,23 @@ function TSHighlighter._on_spell_nav(_, _, buf, srow, _, erow, _) end ---@private ----@param _win integer ---@param buf integer ---@param topline integer ---@param botline integer -function TSHighlighter._on_win(_, _win, buf, topline, botline) +function TSHighlighter._on_win(_, _, buf, topline, botline) local self = TSHighlighter.active[buf] - if not self then + if not self or self.parsing then return false end - self.tree:parse({ topline, botline + 1 }) - self:prepare_highlight_states(topline, botline + 1) + self.parsing = self.tree:parse({ topline, botline + 1 }, function(_, trees) + if trees and self.parsing then + self.parsing = false + api.nvim__redraw({ buf = buf, valid = false, flush = false }) + end + end) == nil self.redraw_count = self.redraw_count + 1 - return true + self:prepare_highlight_states(topline, botline) + return #self._highlight_states > 0 end api.nvim_set_decoration_provider(ns, { diff --git a/runtime/lua/vim/treesitter/language.lua b/runtime/lua/vim/treesitter/language.lua index 446051dfd7..16d19bfc5a 100644 --- a/runtime/lua/vim/treesitter/language.lua +++ b/runtime/lua/vim/treesitter/language.lua @@ -133,8 +133,9 @@ function M.add(lang, opts) path = paths[1] end - return loadparser(path, lang, symbol_name) or nil, - string.format('Cannot load parser %s for language "%s"', path, lang) + local res = loadparser(path, lang, symbol_name) + return res, + res == nil and string.format('Cannot load parser %s for language "%s"', path, lang) or nil end --- @param x string|string[] @@ -174,7 +175,7 @@ end --- (`"`). --- ---@param lang string Language ----@return table +---@return TSLangInfo function M.inspect(lang) M.add(lang) return vim._ts_inspect_language(lang) diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index 4b42164dc8..ea745c4deb 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -44,6 +44,8 @@ local query = require('vim.treesitter.query') local language = require('vim.treesitter.language') local Range = require('vim.treesitter._range') +local default_parse_timeout_ms = 3 + ---@alias TSCallbackName ---| 'changedtree' ---| 'bytes' @@ -58,6 +60,8 @@ local Range = require('vim.treesitter._range') ---| 'on_child_added' ---| 'on_child_removed' +---@alias ParserThreadState { timeout: integer? } + --- @type table<TSCallbackNameOn,TSCallbackName> local TSCallbackNames = { on_changedtree = 'changedtree', @@ -76,8 +80,13 @@ local TSCallbackNames = { ---@field private _injections_processed boolean ---@field private _opts table Options ---@field private _parser TSParser Parser for language ----@field private _has_regions boolean +---Table of regions for which the tree is currently running an async parse +---@field private _ranges_being_parsed table<string, boolean> +---Table of callback queues, keyed by each region for which the callbacks should be run +---@field private _cb_queues table<string, fun(err?: string, trees?: table<integer, TSTree>)[]> ---@field private _regions table<integer, Range6[]>? +---The total number of regions. Since _regions can have holes, we cannot simply read this value from #_regions. +---@field private _num_regions integer ---List of regions this tree should manage and parse. If nil then regions are ---taken from _trees. This is mostly a short-lived cache for included_regions() ---@field private _lang string Language name @@ -85,7 +94,8 @@ local TSCallbackNames = { ---@field private _source (integer|string) Buffer or string to parse ---@field private _trees table<integer, TSTree> Reference to parsed tree (one for each language). ---Each key is the index of region, which is synced with _regions and _valid. ----@field private _valid boolean|table<integer,boolean> If the parsed tree is valid +---@field private _valid_regions table<integer,true> Set of valid region IDs. +---@field private _is_entirely_valid boolean Whether the entire tree (excluding children) is valid. ---@field private _logger? fun(logtype: string, msg: string) ---@field private _logfile? file* local LanguageTree = {} @@ -117,7 +127,7 @@ function LanguageTree.new(source, lang, opts) local injections = opts.injections or {} - --- @type vim.treesitter.LanguageTree + --- @class vim.treesitter.LanguageTree local self = { _source = source, _lang = lang, @@ -126,10 +136,13 @@ function LanguageTree.new(source, lang, opts) _opts = opts, _injection_query = injections[lang] and query.parse(lang, injections[lang]) or query.get(lang, 'injections'), - _has_regions = false, _injections_processed = false, - _valid = false, + _valid_regions = {}, + _num_regions = 1, + _is_entirely_valid = false, _parser = vim._create_ts_parser(lang), + _ranges_being_parsed = {}, + _cb_queues = {}, _callbacks = {}, _callbacks_rec = {}, } @@ -182,7 +195,7 @@ end ---Measure execution time of a function ---@generic R1, R2, R3 ----@param f fun(): R1, R2, R2 +---@param f fun(): R1, R2, R3 ---@return number, R1, R2, R3 local function tcall(f, ...) local start = vim.uv.hrtime() @@ -190,6 +203,7 @@ local function tcall(f, ...) local r = { f(...) } --- @type number local duration = (vim.uv.hrtime() - start) / 1000000 + --- @diagnostic disable-next-line: redundant-return-value return duration, unpack(r) end @@ -231,7 +245,9 @@ end --- tree in treesitter. Doesn't clear filesystem cache. Called often, so needs to be fast. ---@param reload boolean|nil function LanguageTree:invalidate(reload) - self._valid = false + self._valid_regions = {} + self._is_entirely_valid = false + self._parser:reset() -- buffer was reloaded, reparse all trees if reload then @@ -258,20 +274,51 @@ function LanguageTree:trees() end --- Gets the language of this tree node. +--- @return string function LanguageTree:lang() return self._lang end +--- @param region Range6[] +--- @param range? boolean|Range +--- @return boolean +local function intercepts_region(region, range) + if #region == 0 then + return true + end + + if range == nil then + return false + end + + if type(range) == 'boolean' then + return range + end + + for _, r in ipairs(region) do + if Range.intercepts(r, range) then + return true + end + end + + return false +end + --- Returns whether this LanguageTree is valid, i.e., |LanguageTree:trees()| reflects the latest --- state of the source. If invalid, user should call |LanguageTree:parse()|. ----@param exclude_children boolean|nil whether to ignore the validity of children (default `false`) +---@param exclude_children boolean? whether to ignore the validity of children (default `false`) +---@param range Range? range to check for validity ---@return boolean -function LanguageTree:is_valid(exclude_children) - local valid = self._valid +function LanguageTree:is_valid(exclude_children, range) + local valid_regions = self._valid_regions - if type(valid) == 'table' then - for i, _ in pairs(self:included_regions()) do - if not valid[i] then + if not self._is_entirely_valid then + if not range then + return false + end + -- TODO: Efficiently search for possibly intersecting regions using a binary search + for i, region in pairs(self:included_regions()) do + if not valid_regions[i] and intercepts_region(region, range) then return false end end @@ -283,97 +330,81 @@ function LanguageTree:is_valid(exclude_children) end for _, child in pairs(self._children) do - if not child:is_valid(exclude_children) then + if not child:is_valid(exclude_children, range) then return false end end end - if type(valid) == 'boolean' then - return valid - end - - self._valid = true return true end |