From a1e313ded6e4c46c58012639e5c0c6d0b009d52a Mon Sep 17 00:00:00 2001 From: Yi Ming Date: Fri, 29 Nov 2024 20:40:32 +0800 Subject: feat(lsp): support `textDocument/foldingRange` (#31311) * refactor(shared): extract `vim._list_insert` and `vim._list_remove` * feat(lsp): add `vim.lsp.foldexpr()` * docs(lsp): add a todo for state management * feat(lsp): add `vim.lsp.folding_range.foldclose()` * feat(lsp): schedule `foldclose()` if the buffer is not up-to-date * feat(lsp): add `vim.lsp.foldtext()` * feat(lsp): support multiple folding range providers * refactor(lsp): expose all folding related functions under `vim.lsp.*` * perf(lsp): add `lsp.MultiHandler` for do `foldupdate()` only once --- runtime/lua/vim/treesitter/_fold.lua | 53 +++--------------------------------- 1 file changed, 4 insertions(+), 49 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/_fold.lua b/runtime/lua/vim/treesitter/_fold.lua index 7237d2e7d4..0cb5b497c7 100644 --- a/runtime/lua/vim/treesitter/_fold.lua +++ b/runtime/lua/vim/treesitter/_fold.lua @@ -30,65 +30,20 @@ function FoldInfo.new() }, 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 -- cgit From b8c75a31e6f4716f542cd2000e4a7c19c1ae9d70 Mon Sep 17 00:00:00 2001 From: Riley Bruins Date: Sat, 17 Aug 2024 21:05:09 -0700 Subject: feat(treesitter): #trim! can trim all whitespace This commit also implements more generic trimming, acting on all whitespace (charwise) rather than just empty lines. It will unblock https://github.com/nvim-treesitter/nvim-treesitter/pull/3442 and allow for properly concealing markdown bullet markers regardless of indent width, e.g. --- runtime/lua/vim/treesitter/query.lua | 48 +++++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 14 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index 1677e8d364..3c7bc2eb89 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -572,13 +572,17 @@ local directive_handlers = { metadata[id].text = text:gsub(pattern, replacement) end, - -- Trim blank lines from end of the node - -- Example: (#trim! @fold) - -- TODO(clason): generalize to arbitrary whitespace removal + -- Trim whitespace from both sides of the node + -- Example: (#trim! @fold 1 1 1 1) ['trim!'] = function(match, _, bufnr, pred, metadata) local capture_id = pred[2] assert(type(capture_id) == 'number') + local trim_start_lines = pred[3] == '1' + local trim_start_cols = pred[4] == '1' + local trim_end_lines = pred[5] == '1' or not pred[3] -- default true for backwards compatibility + local trim_end_cols = pred[6] == '1' + local nodes = match[capture_id] if not nodes or #nodes == 0 then return @@ -588,20 +592,36 @@ local directive_handlers = { local start_row, start_col, end_row, end_col = node:range() - -- Don't trim if region ends in middle of a line - if end_col ~= 0 then - return - end - - while end_row >= start_row do - -- As we only care when end_col == 0, always inspect one line above end_row. - local end_line = api.nvim_buf_get_lines(bufnr, end_row - 1, end_row, true)[1] + local node_text = vim.split(vim.treesitter.get_node_text(node, bufnr), '\n') + local end_idx = #node_text + local start_idx = 1 - if end_line ~= '' then - break + if trim_end_lines then + while end_idx > 0 and node_text[end_idx]:find('^%s*$') do + end_idx = end_idx - 1 + end_row = end_row - 1 end + end + if trim_end_cols then + if end_idx == 0 then + end_row = start_row + end_col = start_col + else + local whitespace_start = node_text[end_idx]:find('(%s*)$') + end_col = (whitespace_start - 1) + (end_idx == 1 and start_col or 0) + end + end - end_row = end_row - 1 + if trim_start_lines then + while start_idx <= end_idx and node_text[start_idx]:find('^%s*$') do + start_idx = start_idx + 1 + start_row = start_row + 1 + end + end + if trim_start_cols and node_text[start_idx] then + local _, whitespace_end = node_text[start_idx]:find('^(%s*)') + whitespace_end = whitespace_end or 0 + start_col = (start_idx == 1 and start_col or 0) + whitespace_end end -- If this produces an invalid range, we just skip it. -- cgit From c63e49cce2d20cee129a6312319dde8dcea6e3f6 Mon Sep 17 00:00:00 2001 From: Riley Bruins Date: Sat, 7 Dec 2024 03:01:59 -0800 Subject: fix(treesitter): #trim! range for nodes ending at col 0 #31488 Problem: char-wise folding for `#trim!` ranges are improperly calculated for nodes that end at column 0, due to the way `get_node_text` works. Solution: Add the blank line that `get_node_text` removes for for nodes ending at column 0. Also properly set column positions when performing linewise trims. --- runtime/lua/vim/treesitter/query.lua | 9 +++++++++ 1 file changed, 9 insertions(+) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index 3c7bc2eb89..dbe3d54c2f 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -593,6 +593,11 @@ local directive_handlers = { local start_row, start_col, end_row, end_col = node:range() local node_text = vim.split(vim.treesitter.get_node_text(node, bufnr), '\n') + if end_col == 0 then + -- get_node_text() will ignore the last line if the node ends at column 0 + node_text[#node_text + 1] = '' + end + local end_idx = #node_text local start_idx = 1 @@ -600,6 +605,9 @@ local directive_handlers = { while end_idx > 0 and node_text[end_idx]:find('^%s*$') do end_idx = end_idx - 1 end_row = end_row - 1 + -- set the end position to the last column of the next line, or 0 if we just trimmed the + -- last line + end_col = end_idx > 0 and #node_text[end_idx] or 0 end end if trim_end_cols then @@ -616,6 +624,7 @@ local directive_handlers = { while start_idx <= end_idx and node_text[start_idx]:find('^%s*$') do start_idx = start_idx + 1 start_row = start_row + 1 + start_col = 0 end end if trim_start_cols and node_text[start_idx] then -- cgit From 48acbc4d645fe99532b33051006a65a57d36b981 Mon Sep 17 00:00:00 2001 From: Jaehwang Jung Date: Sun, 29 Dec 2024 16:00:47 +0900 Subject: fix(treesitter.foldexpr): refresh in the buffers affected by OptionSet --- runtime/lua/vim/treesitter/_fold.lua | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/_fold.lua b/runtime/lua/vim/treesitter/_fold.lua index 0cb5b497c7..10ba074ab5 100644 --- a/runtime/lua/vim/treesitter/_fold.lua +++ b/runtime/lua/vim/treesitter/_fold.lua @@ -378,9 +378,13 @@ api.nvim_create_autocmd('OptionSet', { pattern = { 'foldminlines', 'foldnestmax' }, desc = 'Refresh treesitter folds', callback = function() - for bufnr, _ in pairs(foldinfos) do + local bufs = vim.v.option_type == 'local' and { api.nvim_get_current_buf() } + or vim.tbl_keys(foldinfos) + for _, bufnr in ipairs(bufs) do foldinfos[bufnr] = FoldInfo.new() - compute_folds_levels(bufnr, foldinfos[bufnr]) + api.nvim_buf_call(bufnr, function() + compute_folds_levels(bufnr, foldinfos[bufnr]) + end) foldinfos[bufnr]:foldupdate(bufnr, 0, api.nvim_buf_line_count(bufnr)) end end, -- cgit From e4bc8b5967d22840c1e52c97acab0f77107cd48c Mon Sep 17 00:00:00 2001 From: Igor Date: Sun, 29 Dec 2024 12:23:24 -0300 Subject: fix(treesitter.foldexpr): only refresh valid buffers Problem: autocmd to refresh folds always uses the current buffer if the option type is local. However, the current buffer may not have a parser, and thus the assert that checks for a parser could fail. Solution: check if the foldinfo contains the buffer, and only refresh if so. --- runtime/lua/vim/treesitter/_fold.lua | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/_fold.lua b/runtime/lua/vim/treesitter/_fold.lua index 10ba074ab5..207ac1ab67 100644 --- a/runtime/lua/vim/treesitter/_fold.lua +++ b/runtime/lua/vim/treesitter/_fold.lua @@ -378,8 +378,10 @@ api.nvim_create_autocmd('OptionSet', { pattern = { 'foldminlines', 'foldnestmax' }, desc = 'Refresh treesitter folds', callback = function() - local bufs = vim.v.option_type == 'local' and { api.nvim_get_current_buf() } - or vim.tbl_keys(foldinfos) + 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() api.nvim_buf_call(bufnr, function() -- cgit From dc692f553aae367a03f286e0d59561247941f96c Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Wed, 1 Jan 2025 12:29:51 -0800 Subject: docs: misc #31479 --- runtime/lua/vim/treesitter/query.lua | 51 ++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 19 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index dbe3d54c2f..f9c497337f 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -1,3 +1,6 @@ +--- @brief This Lua |treesitter-query| interface allows you to create queries and use them to parse +--- text. See |vim.treesitter.query.parse()| for a working example. + local api = vim.api local language = require('vim.treesitter.language') local memoize = vim.func._memoize @@ -8,9 +11,9 @@ local M = {} ---Parsed query, see |vim.treesitter.query.parse()| --- ---@class vim.treesitter.Query ----@field lang string name of the language for this parser +---@field lang string parser language name ---@field captures string[] list of (unique) capture names defined in query ----@field info vim.treesitter.QueryInfo contains information used in the query (e.g. captures, predicates, directives) +---@field info vim.treesitter.QueryInfo query context (e.g. captures, predicates, directives) ---@field query TSQuery userdata query object local Query = {} Query.__index = Query @@ -228,20 +231,28 @@ M.get = memoize('concat-2', function(lang, query_name) return M.parse(lang, query_string) end) ---- Parse {query} as a string. (If the query is in a file, the caller ---- should read the contents into a string before calling). ---- ---- Returns a `Query` (see |lua-treesitter-query|) object which can be used to ---- search nodes in the syntax tree for the patterns defined in {query} ---- using the `iter_captures` and `iter_matches` methods. +--- Parses a {query} string and returns a `Query` object (|lua-treesitter-query|), which can be used +--- to search the tree for the query patterns (via |Query:iter_captures()|, |Query:iter_matches()|), +--- or inspect the query via these fields: +--- - `captures`: a list of unique capture names defined in the query (alias: `info.captures`). +--- - `info.patterns`: information about predicates. --- ---- Exposes `info` and `captures` with additional context about {query}. ---- - `captures` contains the list of unique capture names defined in {query}. ---- - `info.captures` also points to `captures`. ---- - `info.patterns` contains information about predicates. +--- Example (select the code then run `:'<,'>lua` to try it): +--- ```lua +--- local query = vim.treesitter.query.parse('vimdoc', [[ +--- ; query +--- ((h1) @str +--- (#trim! @str 1 1 1 1)) +--- ]]) +--- local tree = vim.treesitter.get_parser():parse()[1] +--- for id, node, metadata in query:iter_captures(tree:root(), 0) do +--- -- Print the node name and source text. +--- vim.print({node:type(), vim.treesitter.get_node_text(node, vim.api.nvim_get_current_buf())}) +--- end +--- ``` --- ---@param lang string Language to use for the query ----@param query string Query in s-expr syntax +---@param query string Query text, in s-expr syntax --- ---@return vim.treesitter.Query : Parsed query --- @@ -847,20 +858,22 @@ local function match_id_hash(_, match) return (match:info()) end ---- Iterate over all captures from all matches inside {node} +--- Iterates over all captures from all matches in {node}. --- ---- {source} is needed if the query contains predicates; then the caller +--- {source} is required if the query contains predicates; then the caller --- must ensure to use a freshly parsed tree consistent with the current --- text of the buffer (if relevant). {start} and {stop} can be used to limit --- matches inside a row range (this is typically used with root node --- as the {node}, i.e., to get syntax highlight matches in the current --- viewport). When omitted, the {start} and {stop} row values are used from the given node. --- ---- The iterator returns four values: a numeric id identifying the capture, ---- the captured node, metadata from any directives processing the match, ---- and the match itself. ---- The following example shows how to get captures by name: +--- The iterator returns four values: +--- 1. the numeric id identifying the capture +--- 2. the captured node +--- 3. metadata from any directives processing the match +--- 4. the match itself --- +--- Example: how to get captures by name: --- ```lua --- for id, node, metadata, match in query:iter_captures(tree:root(), bufnr, first, last) do --- local name = query.captures[id] -- name of the capture in the query -- cgit From b61051ccb4c23958d43d285b8b801af11620264f Mon Sep 17 00:00:00 2001 From: Riley Bruins Date: Sun, 1 Sep 2024 16:54:30 -0700 Subject: feat(func): allow manual cache invalidation for _memoize This commit also adds some tests for the existing memoization functionality. --- runtime/lua/vim/treesitter/query.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index f9c497337f..2b3b9096a6 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -902,8 +902,8 @@ 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) + local apply_directives = memoize(match_id_hash, self.apply_directives, false) + local match_preds = memoize(match_id_hash, self.match_preds, false) local function iter(end_line) local capture, captured_node, match = cursor:next_capture() -- cgit From 8d2ee542a82a0d162198f27de316ddfc81e8761c Mon Sep 17 00:00:00 2001 From: vanaigr Date: Wed, 18 Dec 2024 12:23:28 -0600 Subject: perf(decor): join predicates and matches cache --- runtime/lua/vim/treesitter/query.lua | 81 ++++++++++++++++++++---------------- 1 file changed, 45 insertions(+), 36 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index 2b3b9096a6..01fdb708eb 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -762,16 +762,7 @@ end ---@private ---@param match TSQueryMatch ---@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() - +function Query:match_preds(preds, pattern, captures, source) 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, @@ -807,17 +798,9 @@ end ---@private ---@param match TSQueryMatch ---@return vim.treesitter.query.TSMetadata metadata -function Query:apply_directives(match, source) +function Query:apply_directives(preds, pattern, 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 - - local captures = match:captures() for _, pred in pairs(preds) do if is_directive(pred[1]) then @@ -902,8 +885,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, false) - local match_preds = memoize(match_id_hash, self.match_preds, false) + -- For faster checks that a match is not in the cache. + local highest_cached_match_id = -1 + ---@type table + local match_cache = {} local function iter(end_line) local capture, captured_node, match = cursor:next_capture() @@ -912,16 +897,35 @@ 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 = 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 + local preds = self.info.patterns[pattern] + if preds then + local captures = match:captures() + + if not self:match_preds(preds, pattern, 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 + + metadata = self:apply_directives(preds, pattern, captures, source) + else + metadata = {} + 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 @@ -985,16 +989,21 @@ function Query:iter_matches(node, source, start, stop, opts) end local match_id, pattern = match:info() + local preds = self.info.patterns[pattern] + 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 preds then + if not self:match_preds(preds, pattern, captures, source) then + cursor:remove_match(match_id) + return iter() -- tail call: try next match + end + metadata = self:apply_directives(preds, pattern, captures, source) + else + metadata = {} 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 -- cgit From dd234135ad20119917831fd8ffcb19d8562022ca Mon Sep 17 00:00:00 2001 From: vanaigr Date: Wed, 18 Dec 2024 01:06:41 -0600 Subject: refactor: split predicates and directives --- runtime/lua/vim/treesitter/highlighter.lua | 4 +- runtime/lua/vim/treesitter/query.lua | 172 +++++++++++++++++------------ 2 files changed, 106 insertions(+), 70 deletions(-) (limited to 'runtime/lua/vim/treesitter') 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/query.lua b/runtime/lua/vim/treesitter/query.lua index 01fdb708eb..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 +---@return table +local function process_patterns(patterns) + ---@type table + 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 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,67 +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 ---@param source integer|string -function Query:match_preds(preds, pattern, captures, source) - 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 ---@return vim.treesitter.query.TSMetadata metadata -function Query:apply_directives(preds, pattern, captures, source) +function Query:_apply_directives(directives, pattern_i, captures, source) ---@type vim.treesitter.query.TSMetadata local metadata = {} - for _, pred in pairs(preds) do - if is_directive(pred[1]) then - local handler = directive_handlers[pred[1]] + for _, directive in pairs(directives) do + local handler = directive_handlers[directive[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 @@ -835,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 @@ -897,7 +929,7 @@ function Query:iter_captures(node, source, start, stop) return end - local match_id, pattern = match:info() + local match_id, pattern_i = match:info() --- @type vim.treesitter.query.TSMetadata local metadata @@ -906,11 +938,14 @@ function Query:iter_captures(node, source, start, stop) end if not metadata then - local preds = self.info.patterns[pattern] - if preds then + metadata = {} + + local processed_pattern = self._processed_patterns[pattern_i] + if processed_pattern then local captures = match:captures() - if not self:match_preds(preds, pattern, captures, source) then + 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 @@ -918,9 +953,8 @@ function Query:iter_captures(node, source, start, stop) return iter(end_line) -- tail call: try next match end - metadata = self:apply_directives(preds, pattern, captures, source) - else - metadata = {} + 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) @@ -988,20 +1022,20 @@ function Query:iter_matches(node, source, start, stop, opts) return end - local match_id, pattern = match:info() - local preds = self.info.patterns[pattern] + local match_id, pattern_i = match:info() + local processed_pattern = self._processed_patterns[pattern_i] local captures = match:captures() --- @type vim.treesitter.query.TSMetadata - local metadata - if preds then - if not self:match_preds(preds, pattern, captures, source) then + 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 - metadata = self:apply_directives(preds, pattern, captures, source) - else - metadata = {} + local directives = processed_pattern.directives + metadata = self:_apply_directives(directives, pattern_i, captures, source) end if opts.all == false then @@ -1012,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 -- cgit From 30de00687b899824bb319dfb3f7989ea3f936617 Mon Sep 17 00:00:00 2001 From: Riley Bruins Date: Mon, 6 Jan 2025 16:56:53 -0800 Subject: refactor(treesitter): simplify condition #31889 --- runtime/lua/vim/treesitter/languagetree.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim/treesitter') 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 -- cgit From d9ee0d2984e5fc30cb032785d32f42c72c7e64e1 Mon Sep 17 00:00:00 2001 From: Riley Bruins Date: Wed, 1 Jan 2025 11:33:45 -0800 Subject: perf(treesitter): don't fetch parser for each fold line **Problem:** The treesitter `foldexpr` calls `get_parser()` for each line in the buffer when calculating folds. This can be incredibly slow for buffers where a parser cannot be found (because the result is not cached), and exponentially more so when the user has many `runtimepath`s. **Solution:** Only fetch the parser when it is needed; that is, only when initializing fold data for a buffer. Co-authored-by: Jongwook Choi Co-authored-by: Justin M. Keyes --- runtime/lua/vim/treesitter/_fold.lua | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) (limited to 'runtime/lua/vim/treesitter') 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) -- cgit From 0c296ab22484b4c009d119908d1614a6c6d96b2c Mon Sep 17 00:00:00 2001 From: Riley Bruins Date: Thu, 9 Jan 2025 08:36:16 -0800 Subject: feat(docs): "yxx" runs Lua/Vimscript code examples #31904 `yxx` in Normal mode over a Lua or Vimscript code block section will execute the code. Co-authored-by: Justin M. Keyes --- runtime/lua/vim/treesitter/query.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index 1fc001b39f..b9bcbe9a80 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -292,7 +292,7 @@ end) --- - `captures`: a list of unique capture names defined in the query (alias: `info.captures`). --- - `info.patterns`: information about predicates. --- ---- Example (select the code then run `:'<,'>lua` to try it): +--- Example (to try it, use `yxx` or select the code then run `:'<,'>lua`): --- ```lua --- local query = vim.treesitter.query.parse('vimdoc', [[ --- ; query @@ -983,7 +983,7 @@ end --- -- `node` was captured by the `name` capture in the match --- --- local node_data = metadata[id] -- Node level metadata ---- ... use the info here ... +--- -- ... use the info here ... --- end --- end --- end -- cgit From cb02c20569b56545a1657d4f7f8f29171f1037d7 Mon Sep 17 00:00:00 2001 From: Riley Bruins Date: Fri, 10 Jan 2025 12:25:46 -0800 Subject: refactor(treesitter.foldexpr): remove unused parse_injections parameter --- runtime/lua/vim/treesitter/_fold.lua | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/_fold.lua b/runtime/lua/vim/treesitter/_fold.lua index d16013eca2..7f1d1b14d5 100644 --- a/runtime/lua/vim/treesitter/_fold.lua +++ b/runtime/lua/vim/treesitter/_fold.lua @@ -69,8 +69,7 @@ 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) +local function compute_folds_levels(bufnr, info, srow, erow) srow = srow or 0 erow = erow or api.nvim_buf_line_count(bufnr) @@ -79,7 +78,7 @@ local function compute_folds_levels(bufnr, info, srow, erow, parse_injections) return end - parser:parse(parse_injections and { srow, erow } or nil) + parser:parse() local enter_counts = {} ---@type table local leave_counts = {} ---@type table -- cgit From aa2b44fbb07f3ab4dd00ea4a3ae7c5d31bc20a9d Mon Sep 17 00:00:00 2001 From: Guilherme Soares <48023091+guilhas07@users.noreply.github.com> Date: Fri, 10 Jan 2025 22:46:19 +0000 Subject: fix(treesitter): don't return error message on success #31955 Problem: The `vim.treesitter.language.add` function returns a error message even when it succeeds. Solution: Don't return error message on success. --- runtime/lua/vim/treesitter/language.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/language.lua b/runtime/lua/vim/treesitter/language.lua index 446051dfd7..238a078703 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[] -- cgit From 3fdc4302415972eb5d98ba832372236be3d22572 Mon Sep 17 00:00:00 2001 From: Riley Bruins Date: Sat, 11 Jan 2025 15:44:07 -0800 Subject: perf(treesitter): cache queries strongly **Problem:** Query parsing uses a weak cache which is invalidated frequently **Solution:** Make the cache strong, and invalidate it manually when necessary (that is, when `rtp` is changed or `query.set()` is called) Co-authored-by: Christian Clason --- runtime/lua/vim/treesitter/query.lua | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index b9bcbe9a80..b0b0fecd38 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -262,6 +262,7 @@ local explicit_queries = setmetatable({}, { ---@param query_name string Name of the query (e.g., "highlights") ---@param text string Query text (unparsed). function M.set(lang, query_name, text) + M.get:clear(lang, query_name) explicit_queries[lang][query_name] = M.parse(lang, text) end @@ -284,7 +285,15 @@ M.get = memoize('concat-2', function(lang, query_name) end return M.parse(lang, query_string) -end) +end, false) + +api.nvim_create_autocmd('OptionSet', { + pattern = { 'runtimepath' }, + group = api.nvim_create_augroup('ts_query_cache_reset', { clear = true }), + callback = function() + M.get:clear() + end, +}) --- Parses a {query} string and returns a `Query` object (|lua-treesitter-query|), which can be used --- to search the tree for the query patterns (via |Query:iter_captures()|, |Query:iter_matches()|), @@ -316,7 +325,7 @@ M.parse = memoize('concat-2', function(lang, query) assert(language.add(lang)) local ts_query = vim._ts_parse_query(lang, query) return Query.new(lang, ts_query) -end) +end, false) --- Implementations of predicates that can optionally be prefixed with "any-". --- -- cgit From 45e606b1fddbfeee8fe28385b5371ca6f2fba71b Mon Sep 17 00:00:00 2001 From: Riley Bruins Date: Wed, 18 Dec 2024 10:48:33 -0800 Subject: feat(treesitter): async parsing **Problem:** Parsing can be slow for large files, and it is a blocking operation which can be disruptive and annoying. **Solution:** Provide a function for asynchronous parsing, which accepts a callback to be run after parsing completes. Co-authored-by: Lewis Russell Co-authored-by: Luuk van Baal Co-authored-by: VanaIgr --- runtime/lua/vim/treesitter/highlighter.lua | 19 ++-- runtime/lua/vim/treesitter/languagetree.lua | 143 ++++++++++++++++++++++++++-- runtime/lua/vim/treesitter/query.lua | 14 ++- 3 files changed, 156 insertions(+), 20 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/highlighter.lua b/runtime/lua/vim/treesitter/highlighter.lua index 96503c38ea..04e6ee8a9e 100644 --- a/runtime/lua/vim/treesitter/highlighter.lua +++ b/runtime/lua/vim/treesitter/highlighter.lua @@ -69,6 +69,7 @@ end ---@field private _queries table ---@field tree vim.treesitter.LanguageTree ---@field private redraw_count integer +---@field parsing boolean true if we are parsing asynchronously local TSHighlighter = { active = {}, } @@ -147,7 +148,7 @@ function TSHighlighter.new(tree, opts) vim.opt_local.spelloptions:append('noplainbuffer') end) - self.tree:parse() + self.tree:parse(nil, function() end) return self end @@ -384,19 +385,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/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index 330eb45749..945a2301a9 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' @@ -76,6 +78,10 @@ local TSCallbackNames = { ---@field private _injections_processed boolean ---@field private _opts table Options ---@field private _parser TSParser Parser for language +---Table of regions for which the tree is currently running an async parse +---@field private _ranges_being_parsed table +---Table of callback queues, keyed by each region for which the callbacks should be run +---@field private _cb_queues table)[]> ---@field private _has_regions boolean ---@field private _regions table? ---List of regions this tree should manage and parse. If nil then regions are @@ -130,6 +136,8 @@ function LanguageTree.new(source, lang, opts) _injections_processed = false, _valid = false, _parser = vim._create_ts_parser(lang), + _ranges_being_parsed = {}, + _cb_queues = {}, _callbacks = {}, _callbacks_rec = {}, } @@ -232,6 +240,7 @@ end ---@param reload boolean|nil function LanguageTree:invalidate(reload) self._valid = false + self._parser:reset() -- buffer was reloaded, reparse all trees if reload then @@ -334,10 +343,12 @@ end --- @private --- @param range boolean|Range? +--- @param timeout integer? --- @return Range6[] changes --- @return integer no_regions_parsed --- @return number total_parse_time -function LanguageTree:_parse_regions(range) +--- @return boolean finished whether async parsing still needs time +function LanguageTree:_parse_regions(range, timeout) local changes = {} local no_regions_parsed = 0 local total_parse_time = 0 @@ -357,9 +368,14 @@ function LanguageTree:_parse_regions(range) ) then self._parser:set_included_ranges(ranges) + self._parser:set_timeout(timeout and timeout * 1000 or 0) -- ms -> micros local parse_time, tree, tree_changes = tcall(self._parser.parse, self._parser, self._trees[i], self._source, true) + if not tree then + return changes, no_regions_parsed, total_parse_time, false + end + -- Pass ranges if this is an initial parse local cb_changes = self._trees[i] and tree_changes or tree:included_ranges(true) @@ -373,7 +389,7 @@ function LanguageTree:_parse_regions(range) end end - return changes, no_regions_parsed, total_parse_time + return changes, no_regions_parsed, total_parse_time, true end --- @private @@ -409,6 +425,82 @@ function LanguageTree:_add_injections() return query_time end +--- @param range boolean|Range? +--- @return string +local function range_to_string(range) + return type(range) == 'table' and table.concat(range, ',') or tostring(range) +end + +--- @private +--- @param range boolean|Range? +--- @param callback fun(err?: string, trees?: table) +function LanguageTree:_push_async_callback(range, callback) + local key = range_to_string(range) + self._cb_queues[key] = self._cb_queues[key] or {} + local queue = self._cb_queues[key] + queue[#queue + 1] = callback +end + +--- @private +--- @param range boolean|Range? +--- @param err? string +--- @param trees? table +function LanguageTree:_run_async_callbacks(range, err, trees) + local key = range_to_string(range) + for _, cb in ipairs(self._cb_queues[key]) do + cb(err, trees) + end + self._ranges_being_parsed[key] = false + self._cb_queues[key] = {} +end + +--- Run an asynchronous parse, calling {on_parse} when complete. +--- +--- @private +--- @param range boolean|Range? +--- @param on_parse fun(err?: string, trees?: table) +--- @return table? trees the list of parsed trees, if parsing completed synchronously +function LanguageTree:_async_parse(range, on_parse) + self:_push_async_callback(range, on_parse) + + -- If we are already running an async parse, just queue the callback. + local range_string = range_to_string(range) + if not self._ranges_being_parsed[range_string] then + self._ranges_being_parsed[range_string] = true + else + return + end + + local buf = vim.b[self._source] + local ct = buf.changedtick + local total_parse_time = 0 + local redrawtime = vim.o.redrawtime + local timeout = not vim.g._ts_force_sync_parsing and default_parse_timeout_ms or nil + + local function step() + -- If buffer was changed in the middle of parsing, reset parse state + if buf.changedtick ~= ct then + ct = buf.changedtick + total_parse_time = 0 + end + + local parse_time, trees, finished = tcall(self._parse, self, range, timeout) + total_parse_time = total_parse_time + parse_time + + if finished then + self:_run_async_callbacks(range, nil, trees) + return trees + elseif total_parse_time > redrawtime then + self:_run_async_callbacks(range, 'TIMEOUT', nil) + return nil + else + vim.schedule(step) + end + end + + return step() +end + --- Recursively parse all regions in the language tree using |treesitter-parsers| --- for the corresponding languages and run injection queries on the parsed trees --- to determine whether child trees should be created and parsed. @@ -420,11 +512,33 @@ end --- Set to `true` to run a complete parse of the source (Note: Can be slow!) --- Set to `false|nil` to only parse regions with empty ranges (typically --- only the root tree without injections). ---- @return table -function LanguageTree:parse(range) +--- @param on_parse fun(err?: string, trees?: table)? Function invoked when parsing completes. +--- When provided and `vim.g._ts_force_sync_parsing` is not set, parsing will run +--- asynchronously. The first argument to the function is a string respresenting the error type, +--- in case of a failure (currently only possible for timeouts). The second argument is the list +--- of trees returned by the parse (upon success), or `nil` if the parse timed out (determined +--- by 'redrawtime'). +--- +--- If parsing was still able to finish synchronously (within 3ms), `parse()` returns the list +--- of trees. Otherwise, it returns `nil`. +--- @return table? +function LanguageTree:parse(range, on_parse) + if on_parse then + return self:_async_parse(range, on_parse) + end + local trees, _ = self:_parse(range) + return trees +end + +--- @private +--- @param range boolean|Range|nil +--- @param timeout integer? +--- @return table trees +--- @return boolean finished +function LanguageTree:_parse(range, timeout) if self:is_valid() then self:_log('valid') - return self._trees + return self._trees, true end local changes --- @type Range6[]? @@ -433,10 +547,15 @@ function LanguageTree:parse(range) local no_regions_parsed = 0 local query_time = 0 local total_parse_time = 0 + local is_finished --- @type boolean -- At least 1 region is invalid if not self:is_valid(true) then - changes, no_regions_parsed, total_parse_time = self:_parse_regions(range) + changes, no_regions_parsed, total_parse_time, is_finished = self:_parse_regions(range, timeout) + timeout = timeout and math.max(timeout - total_parse_time, 0) + if not is_finished then + return self._trees, is_finished + end -- Need to run injections when we parsed something if no_regions_parsed > 0 then self._injections_processed = false @@ -457,10 +576,17 @@ function LanguageTree:parse(range) }) for _, child in pairs(self._children) do - child:parse(range) + if timeout == 0 then + return self._trees, false + end + local ctime, _, child_finished = tcall(child._parse, child, range, timeout) + timeout = timeout and math.max(timeout - ctime, 0) + if not child_finished then + return self._trees, child_finished + end end - return self._trees + return self._trees, true end --- Invokes the callback for each |LanguageTree| recursively. @@ -907,6 +1033,7 @@ function LanguageTree:_edit( ) end + self._parser:reset() self._regions = nil local changed_range = { diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index b0b0fecd38..66ab0d52f0 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -913,8 +913,8 @@ end ---@param start? integer Starting line for the search. Defaults to `node:start()`. ---@param stop? integer Stopping line for the search (end-exclusive). Defaults to `node:end_()`. --- ----@return (fun(end_line: integer|nil): integer, TSNode, vim.treesitter.query.TSMetadata, TSQueryMatch): ---- capture id, capture node, metadata, match +---@return (fun(end_line: integer|nil): integer, TSNode, vim.treesitter.query.TSMetadata, TSQueryMatch, TSTree): +--- capture id, capture node, metadata, match, tree --- ---@note Captures are only returned if the query pattern of a specific capture contained predicates. function Query:iter_captures(node, source, start, stop) @@ -924,6 +924,8 @@ function Query:iter_captures(node, source, start, stop) start, stop = value_or_node_range(start, stop, node) + -- Copy the tree to ensure it is valid during the entire lifetime of the iterator + local tree = node:tree():copy() local cursor = vim._create_ts_querycursor(node, self.query, start, stop, { match_limit = 256 }) -- For faster checks that a match is not in the cache. @@ -970,7 +972,7 @@ function Query:iter_captures(node, source, start, stop) match_cache[match_id] = metadata end - return capture, captured_node, metadata, match + return capture, captured_node, metadata, match, tree end return iter end @@ -1011,7 +1013,7 @@ end --- (last) node instead of the full list of matching nodes. This option is only for backward --- compatibility and will be removed in a future release. --- ----@return (fun(): integer, table, vim.treesitter.query.TSMetadata): pattern id, match, metadata +---@return (fun(): integer, table, vim.treesitter.query.TSMetadata, TSTree): pattern id, match, metadata, tree function Query:iter_matches(node, source, start, stop, opts) opts = opts or {} opts.match_limit = opts.match_limit or 256 @@ -1022,6 +1024,8 @@ function Query:iter_matches(node, source, start, stop, opts) start, stop = value_or_node_range(start, stop, node) + -- Copy the tree to ensure it is valid during the entire lifetime of the iterator + local tree = node:tree():copy() local cursor = vim._create_ts_querycursor(node, self.query, start, stop, opts) local function iter() @@ -1059,7 +1063,7 @@ function Query:iter_matches(node, source, start, stop, opts) end -- TODO(lewis6991): create a new function that returns {match, metadata} - return pattern_i, captures, metadata + return pattern_i, captures, metadata, tree end return iter end -- cgit From bd4ca22d0334a3323313dfd6975a80218ec65e36 Mon Sep 17 00:00:00 2001 From: Riley Bruins Date: Fri, 20 Dec 2024 16:23:52 -0800 Subject: feat(treesitter)!: don't parse tree in get_parser() or start() **Problem:** `vim.treesitter.get_parser()` and `vim.treesitter.start()` both parse the tree before returning it. This is problematic because if this is a sync parse, it will stall the editor on large files. If it is an async parse, the functions return stale trees. **Solution:** Remove this parsing side effect and leave it to the user to parse the returned trees, either synchronously or asynchronously. --- runtime/lua/vim/treesitter/highlighter.lua | 2 -- 1 file changed, 2 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/highlighter.lua b/runtime/lua/vim/treesitter/highlighter.lua index 04e6ee8a9e..be138885d5 100644 --- a/runtime/lua/vim/treesitter/highlighter.lua +++ b/runtime/lua/vim/treesitter/highlighter.lua @@ -148,8 +148,6 @@ function TSHighlighter.new(tree, opts) vim.opt_local.spelloptions:append('noplainbuffer') end) - self.tree:parse(nil, function() end) - return self end -- cgit From b192d58284a791c55f5ae000250fc948e9098d47 Mon Sep 17 00:00:00 2001 From: Riley Bruins Date: Mon, 13 Jan 2025 09:42:39 -0800 Subject: perf(treesitter): calculate folds asynchronously **Problem:** The treesitter `foldexpr` runs synchronous parses to calculate fold levels, which eliminates async parsing performance in the highlighter. **Solution:** Migrate the `foldexpr` to also calculate and apply fold levels asynchronously. --- runtime/lua/vim/treesitter/_fold.lua | 203 +++++++++++++++++++---------------- 1 file changed, 109 insertions(+), 94 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/_fold.lua b/runtime/lua/vim/treesitter/_fold.lua index 7f1d1b14d5..2777241e9f 100644 --- a/runtime/lua/vim/treesitter/_fold.lua +++ b/runtime/lua/vim/treesitter/_fold.lua @@ -69,7 +69,8 @@ end ---@param info TS.FoldInfo ---@param srow integer? ---@param erow integer? 0-indexed, exclusive -local function compute_folds_levels(bufnr, info, srow, erow) +---@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) @@ -78,104 +79,112 @@ local function compute_folds_levels(bufnr, info, srow, erow) return end - parser:parse() - - local enter_counts = {} ---@type table - local leave_counts = {} ---@type table - local prev_start = -1 - local prev_stop = -1 - - 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 + local leave_counts = {} ---@type table + 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 = {} @@ -266,6 +275,8 @@ local function on_changedtree(bufnr, foldinfo, tree_changes) schedule_if_loaded(bufnr, function() 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 @@ -279,12 +290,14 @@ 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 @@ -342,8 +355,9 @@ local function on_bytes(bufnr, foldinfo, start_row, start_col, old_row, old_col, 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 @@ -400,9 +414,10 @@ api.nvim_create_autocmd('OptionSet', { for _, bufnr in ipairs(bufs) do foldinfos[bufnr] = FoldInfo.new(bufnr) api.nvim_buf_call(bufnr, function() - compute_folds_levels(bufnr, foldinfos[bufnr]) + compute_folds_levels(bufnr, foldinfos[bufnr], nil, nil, function() + foldinfos[bufnr]:foldupdate(bufnr, 0, api.nvim_buf_line_count(bufnr)) + end) end) - foldinfos[bufnr]:foldupdate(bufnr, 0, api.nvim_buf_line_count(bufnr)) end end, }) -- cgit From 850084b519e18122820478a71bb4bfa4c15e528a Mon Sep 17 00:00:00 2001 From: Maria José Solano Date: Mon, 13 Jan 2025 19:39:03 -0800 Subject: refactor: use nvim.foo.bar format for namespaces --- runtime/lua/vim/treesitter/_query_linter.lua | 2 +- runtime/lua/vim/treesitter/dev.lua | 6 +++--- runtime/lua/vim/treesitter/highlighter.lua | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/_query_linter.lua b/runtime/lua/vim/treesitter/_query_linter.lua index a825505378..f6645beb28 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 = {} diff --git a/runtime/lua/vim/treesitter/dev.lua b/runtime/lua/vim/treesitter/dev.lua index 26817cdba5..0e886d0e27 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,7 +135,7 @@ function TSTreeView:new(bufnr, lang) return t end -local decor_ns = api.nvim_create_namespace('ts.dev') +local decor_ns = api.nvim_create_namespace('nvim.treesitter.dev') ---@param range Range4 ---@return string @@ -547,7 +547,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 diff --git a/runtime/lua/vim/treesitter/highlighter.lua b/runtime/lua/vim/treesitter/highlighter.lua index be138885d5..c11fa1999d 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 -- cgit From 09e01437c968be4c6e9f6bb3ac8811108c58008c Mon Sep 17 00:00:00 2001 From: Maria José Solano Date: Mon, 13 Jan 2025 19:45:11 -0800 Subject: refactor: use nvim.foo.bar format for autocommand groups --- runtime/lua/vim/treesitter/_fold.lua | 2 +- runtime/lua/vim/treesitter/dev.lua | 4 ++-- runtime/lua/vim/treesitter/query.lua | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/_fold.lua b/runtime/lua/vim/treesitter/_fold.lua index 7f1d1b14d5..cf5c40cd1e 100644 --- a/runtime/lua/vim/treesitter/_fold.lua +++ b/runtime/lua/vim/treesitter/_fold.lua @@ -183,7 +183,7 @@ local M = {} ---@type table 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). diff --git a/runtime/lua/vim/treesitter/dev.lua b/runtime/lua/vim/treesitter/dev.lua index 0e886d0e27..42c25dbdad 100644 --- a/runtime/lua/vim/treesitter/dev.lua +++ b/runtime/lua/vim/treesitter/dev.lua @@ -442,7 +442,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, @@ -633,7 +633,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/query.lua b/runtime/lua/vim/treesitter/query.lua index 66ab0d52f0..ad648f36cc 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -289,7 +289,7 @@ end, false) api.nvim_create_autocmd('OptionSet', { pattern = { 'runtimepath' }, - group = api.nvim_create_augroup('ts_query_cache_reset', { clear = true }), + group = api.nvim_create_augroup('nvim.treesitter.query_cache_reset', { clear = true }), callback = function() M.get:clear() end, -- cgit From 09bcb310681e3b87d5b8c5eb547b182554cff7b4 Mon Sep 17 00:00:00 2001 From: Evgeni Chasnovski Date: Wed, 15 Jan 2025 12:36:00 +0200 Subject: fix(docs): replace `yxx` mappings with `g==` #31947 Problem: `yx` uses "y" prefix, which shadows a builtin operator. Solution: Use `g=` (in the form of `g==` currently), drawing from precedent of CTRL-= and 'tpope/vim-scriptease'. --- runtime/lua/vim/treesitter/query.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index ad648f36cc..e43d0a8ad4 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -301,7 +301,7 @@ api.nvim_create_autocmd('OptionSet', { --- - `captures`: a list of unique capture names defined in the query (alias: `info.captures`). --- - `info.patterns`: information about predicates. --- ---- Example (to try it, use `yxx` or select the code then run `:'<,'>lua`): +--- Example (to try it, use `g==` or select the code then run `:'<,'>lua`): --- ```lua --- local query = vim.treesitter.query.parse('vimdoc', [[ --- ; query -- cgit From 6696ea7f103814d3d5700107546280bf50a4004a Mon Sep 17 00:00:00 2001 From: Jaehwang Jung Date: Sun, 19 Jan 2025 00:07:47 +0900 Subject: fix(treesitter): clean up parsing queue --- runtime/lua/vim/treesitter/languagetree.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index 945a2301a9..35a77f1afc 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -450,8 +450,8 @@ function LanguageTree:_run_async_callbacks(range, err, trees) for _, cb in ipairs(self._cb_queues[key]) do cb(err, trees) end - self._ranges_being_parsed[key] = false - self._cb_queues[key] = {} + self._ranges_being_parsed[key] = nil + self._cb_queues[key] = nil end --- Run an asynchronous parse, calling {on_parse} when complete. -- cgit From 27c88069538bf64dace1ed39512d914e88615ac1 Mon Sep 17 00:00:00 2001 From: Jaehwang Jung Date: Mon, 20 Jan 2025 21:17:36 +0900 Subject: docs(treesitter): expose LanguageTree:parent() #32108 Plugins may want to climb up the LanguageTree. Also add missing type annotations for other methods. --- runtime/lua/vim/treesitter/languagetree.lua | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index 35a77f1afc..3db7fe5c9e 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -267,6 +267,7 @@ function LanguageTree:trees() end --- Gets the language of this tree node. +--- @return string function LanguageTree:lang() return self._lang end @@ -307,11 +308,13 @@ function LanguageTree:is_valid(exclude_children) end --- Returns a map of language to child tree. +--- @return table function LanguageTree:children() return self._children end --- Returns the source content of the language tree (bufnr or string). +--- @return integer|string function LanguageTree:source() return self._source end @@ -630,7 +633,8 @@ function LanguageTree:add_child(lang) return self._children[lang] end ---- @package +---Returns the parent tree. `nil` for the root tree. +---@return vim.treesitter.LanguageTree? function LanguageTree:parent() return self._parent end -- cgit From f50f86b9ff5dd2aab7838801d3c1cad898ea0c77 Mon Sep 17 00:00:00 2001 From: Konrad Malik Date: Mon, 20 Jan 2025 17:17:46 +0100 Subject: fix(treesitter): compute folds on_changedtree only if not nil --- runtime/lua/vim/treesitter/_fold.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/_fold.lua b/runtime/lua/vim/treesitter/_fold.lua index 4a571bbaf7..f8d18d8427 100644 --- a/runtime/lua/vim/treesitter/_fold.lua +++ b/runtime/lua/vim/treesitter/_fold.lua @@ -387,7 +387,9 @@ function M.foldexpr(lnum) parser:register_cbs({ on_changedtree = function(tree_changes) - on_changedtree(bufnr, foldinfos[bufnr], tree_changes) + if foldinfos[bufnr] then + on_changedtree(bufnr, foldinfos[bufnr], tree_changes) + end end, on_bytes = function(_, _, start_row, start_col, _, old_row, old_col, _, new_row, new_col, _) -- cgit From eb60cd74fb5caa997e6253bef6a1f0b58e1b6ec6 Mon Sep 17 00:00:00 2001 From: Christian Clason Date: Mon, 27 Jan 2025 16:16:06 +0100 Subject: build(deps)!: bump tree-sitter to HEAD, wasmtime to v29.0.1 (#32200) Breaking change: `ts_node_child_containing_descendant()` was removed Breaking change: tree-sitter 0.25 (HEAD) required --- runtime/lua/vim/treesitter/_meta/tsnode.lua | 6 ------ 1 file changed, 6 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/_meta/tsnode.lua b/runtime/lua/vim/treesitter/_meta/tsnode.lua index d982b6a505..0c1b376fba 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: -- cgit From 6aa42e8f92bd8bea49b7b2accfe4ab67a5344e41 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Fri, 24 Jan 2025 13:01:25 +0000 Subject: fix: resolve all remaining LuaLS diagnostics --- runtime/lua/vim/treesitter/_meta/misc.lua | 8 +++++++- runtime/lua/vim/treesitter/_meta/tsnode.lua | 1 + runtime/lua/vim/treesitter/_query_linter.lua | 4 +++- runtime/lua/vim/treesitter/dev.lua | 10 +--------- runtime/lua/vim/treesitter/language.lua | 2 +- runtime/lua/vim/treesitter/languagetree.lua | 10 ++++++---- runtime/lua/vim/treesitter/query.lua | 2 ++ 7 files changed, 21 insertions(+), 16 deletions(-) (limited to 'runtime/lua/vim/treesitter') 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 +--- +---@class TSLangInfo +---@field fields string[] +---@field symbols table +---@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 0c1b376fba..b261b87253 100644 --- a/runtime/lua/vim/treesitter/_meta/tsnode.lua +++ b/runtime/lua/vim/treesitter/_meta/tsnode.lua @@ -104,6 +104,7 @@ function TSNode:end_() end --- - end column --- - end byte (if {include_bytes} is `true`) --- @param include_bytes boolean? +--- @return integer, integer, integer, integer function TSNode:range(include_bytes) end --- @nodoc diff --git a/runtime/lua/vim/treesitter/_query_linter.lua b/runtime/lua/vim/treesitter/_query_linter.lua index f6645beb28..3dfc6b0cfe 100644 --- a/runtime/lua/vim/treesitter/_query_linter.lua +++ b/runtime/lua/vim/treesitter/_query_linter.lua @@ -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 42c25dbdad..ab08e1a527 100644 --- a/runtime/lua/vim/treesitter/dev.lua +++ b/runtime/lua/vim/treesitter/dev.lua @@ -137,14 +137,6 @@ end local decor_ns = api.nvim_create_namespace('nvim.treesitter.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 - ---@param w integer ---@return boolean closed Whether the window was closed. local function close_win(w) @@ -227,7 +219,7 @@ 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 diff --git a/runtime/lua/vim/treesitter/language.lua b/runtime/lua/vim/treesitter/language.lua index 238a078703..16d19bfc5a 100644 --- a/runtime/lua/vim/treesitter/language.lua +++ b/runtime/lua/vim/treesitter/language.lua @@ -175,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 3db7fe5c9e..ecace67419 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -123,7 +123,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, @@ -190,7 +190,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() @@ -198,6 +198,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 @@ -550,14 +551,14 @@ function LanguageTree:_parse(range, timeout) local no_regions_parsed = 0 local query_time = 0 local total_parse_time = 0 - local is_finished --- @type boolean -- At least 1 region is invalid if not self:is_valid(true) then + local is_finished changes, no_regions_parsed, total_parse_time, is_finished = self:_parse_regions(range, timeout) timeout = timeout and math.max(timeout - total_parse_time, 0) if not is_finished then - return self._trees, is_finished + return self._trees, false end -- Need to run injections when we parsed something if no_regions_parsed > 0 then @@ -740,6 +741,7 @@ function LanguageTree:set_included_regions(new_regions) if type(range) == 'table' and #range == 4 then region[i] = Range.add_bytes(self._source, range --[[@as Range4]]) elseif type(range) == 'userdata' then + --- @diagnostic disable-next-line: missing-fields LuaLS varargs bug region[i] = { range:range(true) } end end diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index e43d0a8ad4..8055270a7f 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -262,6 +262,7 @@ local explicit_queries = setmetatable({}, { ---@param query_name string Name of the query (e.g., "highlights") ---@param text string Query text (unparsed). function M.set(lang, query_name, text) + --- @diagnostic disable-next-line: undefined-field LuaLS bad at generics M.get:clear(lang, query_name) explicit_queries[lang][query_name] = M.parse(lang, text) end @@ -291,6 +292,7 @@ api.nvim_create_autocmd('OptionSet', { pattern = { 'runtimepath' }, group = api.nvim_create_augroup('nvim.treesitter.query_cache_reset', { clear = true }), callback = function() + --- @diagnostic disable-next-line: undefined-field LuaLS bad at generics M.get:clear() end, }) -- cgit From c47496791a80f8b6b9e37866010305482de4c8ca Mon Sep 17 00:00:00 2001 From: Riley Bruins Date: Mon, 27 Jan 2025 14:25:06 -0800 Subject: docs(treesitter): fix TSNode:range() type signature #32224 Uses an overload to properly show the different return type based on the input parameter. --- runtime/lua/vim/treesitter/_meta/tsnode.lua | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/_meta/tsnode.lua b/runtime/lua/vim/treesitter/_meta/tsnode.lua index b261b87253..552905c3f0 100644 --- a/runtime/lua/vim/treesitter/_meta/tsnode.lua +++ b/runtime/lua/vim/treesitter/_meta/tsnode.lua @@ -103,18 +103,9 @@ function TSNode:end_() end --- - end row --- - end column --- - end byte (if {include_bytes} is `true`) ---- @param include_bytes boolean? ---- @return integer, integer, integer, integer -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. -- cgit From a119dab40f939121d5e5a0c622f19911c9c9ce03 Mon Sep 17 00:00:00 2001 From: luukvbaal Date: Tue, 28 Jan 2025 17:34:07 +0100 Subject: fix(treesitter): avoid computing foldlevels for reloaded buffer #32233 --- runtime/lua/vim/treesitter/_fold.lua | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/_fold.lua b/runtime/lua/vim/treesitter/_fold.lua index f8d18d8427..ad4110b83d 100644 --- a/runtime/lua/vim/treesitter/_fold.lua +++ b/runtime/lua/vim/treesitter/_fold.lua @@ -269,10 +269,15 @@ 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 @@ -303,13 +308,18 @@ local function on_changedtree(bufnr, foldinfo, tree_changes) 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 @@ -348,7 +358,7 @@ 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] @@ -387,13 +397,11 @@ function M.foldexpr(lnum) parser:register_cbs({ on_changedtree = function(tree_changes) - if foldinfos[bufnr] then - on_changedtree(bufnr, foldinfos[bufnr], tree_changes) - end + 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() -- cgit From b88874d33c15bb0fd7a421230f8bf819056d7665 Mon Sep 17 00:00:00 2001 From: Riley Bruins Date: Tue, 28 Jan 2025 09:59:04 -0800 Subject: fix(treesitter): empty queries can disable injections (#31748) **Problem:** Currently, if users want to efficiently disable injections, they have to delete the injection query files at their runtime path. This is because we only check for existence of the files before running the query over the entire buffer. **Solution:** Check for existence of query files, *and* that those files actually have captures. This will allow users to just comment out existing queries (or better yet, just add their own injection query to `~/.config/nvim` which contains only comments) to disable running the query over the entire buffer (a potentially slow operation) --- runtime/lua/vim/treesitter/languagetree.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index ecace67419..5e1156fa68 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -953,7 +953,7 @@ end --- @private --- @return table function LanguageTree:_get_injections() - if not self._injection_query then + if not self._injection_query or #self._injection_query.captures == 0 then return {} end -- cgit From 6711fa27ca6e822bfd2394ec513671617cc53efd Mon Sep 17 00:00:00 2001 From: Riley Bruins Date: Tue, 28 Jan 2025 12:22:25 -0800 Subject: fix(treesitter): recalculate folds on VimEnter #32240 **Problem:** In the case where the user sets the treesitter foldexpr upon startup in their `init.lua`, the fold info will be calculated before the parser has been loaded in, meaning folds will be properly calculated until edits or `:e`. **Solution:** Refresh fold information upon `VimEnter` as a sanity check to ensure that a parser really doesn't exist before always returning `'0'` in the foldexpr. --- runtime/lua/vim/treesitter/_fold.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/_fold.lua b/runtime/lua/vim/treesitter/_fold.lua index ad4110b83d..38318347a7 100644 --- a/runtime/lua/vim/treesitter/_fold.lua +++ b/runtime/lua/vim/treesitter/_fold.lua @@ -380,7 +380,7 @@ function M.foldexpr(lnum) if not foldinfos[bufnr] then foldinfos[bufnr] = FoldInfo.new(bufnr) - api.nvim_create_autocmd('BufUnload', { + api.nvim_create_autocmd({ 'BufUnload', 'VimEnter' }, { buffer = bufnr, once = true, callback = function() -- cgit From da0ae953490098c28bad4791e08e2cc4c2e385e2 Mon Sep 17 00:00:00 2001 From: Maria José Solano Date: Tue, 28 Jan 2025 23:59:28 -0800 Subject: feat(treesitter): support modelines in `query.set()` (#30257) --- runtime/lua/vim/treesitter/query.lua | 66 +++++++++++++++++++++++++++++------- 1 file changed, 54 insertions(+), 12 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index 8055270a7f..10fb82e533 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -5,6 +5,9 @@ local api = vim.api local language = require('vim.treesitter.language') local memoize = vim.func._memoize +local MODELINE_FORMAT = '^;+%s*inherits%s*:?%s*([a-z_,()]+)%s*$' +local EXTENDS_FORMAT = '^;+%s*extends%s*$' + local M = {} local function is_directive(name) @@ -167,9 +170,6 @@ function M.get_files(lang, query_name, is_included) -- ;+ inherits: ({language},)*{language} -- -- {language} ::= {lang} | ({lang}) - local MODELINE_FORMAT = '^;+%s*inherits%s*:?%s*([a-z_,()]+)%s*$' - local EXTENDS_FORMAT = '^;+%s*extends%s*$' - for _, filename in ipairs(lang_files) do local file, err = io.open(filename, 'r') if not file then @@ -242,8 +242,8 @@ local function read_query_files(filenames) return table.concat(contents, '') end --- The explicitly set queries from |vim.treesitter.query.set()| ----@type table> +-- The explicitly set query strings from |vim.treesitter.query.set()| +---@type table> local explicit_queries = setmetatable({}, { __index = function(t, k) local lang_queries = {} @@ -255,16 +255,27 @@ local explicit_queries = setmetatable({}, { --- Sets the runtime query named {query_name} for {lang} --- ---- This allows users to override any runtime files and/or configuration +--- This allows users to override or extend any runtime files and/or configuration --- set by plugins. --- +--- For example, you could enable spellchecking of `C` identifiers with the +--- following code: +--- ```lua +--- vim.treesitter.query.set( +--- 'c', +--- 'highlights', +--- [[;inherits c +--- (identifier) @spell]]) +--- ]]) +--- ``` +--- ---@param lang string Language to use for the query ---@param query_name string Name of the query (e.g., "highlights") ---@param text string Query text (unparsed). function M.set(lang, query_name, text) --- @diagnostic disable-next-line: undefined-field LuaLS bad at generics M.get:clear(lang, query_name) - explicit_queries[lang][query_name] = M.parse(lang, text) + explicit_queries[lang][query_name] = text end --- Returns the runtime query {query_name} for {lang}. @@ -274,12 +285,43 @@ end --- ---@return vim.treesitter.Query? : Parsed query. `nil` if no query files are found. M.get = memoize('concat-2', function(lang, query_name) + local query_string ---@type string + if explicit_queries[lang][query_name] then - return explicit_queries[lang][query_name] - end + local query_files = {} + local base_langs = {} ---@type string[] + + for line in explicit_queries[lang][query_name]:gmatch('([^\n]*)\n?') do + if not vim.startswith(line, ';') then + break + end + + local lang_list = line:match(MODELINE_FORMAT) + if lang_list then + for _, incl_lang in ipairs(vim.split(lang_list, ',')) do + local is_optional = incl_lang:match('%(.*%)') - local query_files = M.get_files(lang, query_name) - local query_string = read_query_files(query_files) + if is_optional then + add_included_lang(base_langs, lang, incl_lang:sub(2, #incl_lang - 1)) + else + add_included_lang(base_langs, lang, incl_lang) + end + end + elseif line:match(EXTENDS_FORMAT) then + table.insert(base_langs, lang) + end + end + + for _, base_lang in ipairs(base_langs) do + local base_files = M.get_files(base_lang, query_name, true) + vim.list_extend(query_files, base_files) + end + + query_string = read_query_files(query_files) .. explicit_queries[lang][query_name] + else + local query_files = M.get_files(lang, query_name) + query_string = read_query_files(query_files) + end if #query_string == 0 then return nil @@ -303,7 +345,7 @@ api.nvim_create_autocmd('OptionSet', { --- - `captures`: a list of unique capture names defined in the query (alias: `info.captures`). --- - `info.patterns`: information about predicates. --- ---- Example (to try it, use `g==` or select the code then run `:'<,'>lua`): +--- Example: --- ```lua --- local query = vim.treesitter.query.parse('vimdoc', [[ --- ; query -- cgit From 19f00bf32cebfa66a17e0f5945d62d7da1859623 Mon Sep 17 00:00:00 2001 From: Daniel Petrovic Date: Wed, 29 Jan 2025 09:02:49 +0100 Subject: fix(treesitter) Set modeline=false in TSHighlighter:destroy (#32234) Problem: `TSHighlighter:destroy()` causes double-processing of the modeline and failure of `b:undo_ftplugin`. Solution: Disable modeline in `TSHighlighter:destroy()` by setting `modeline=false` if executing `syntaxset` autocommands for the `FileType` event. Co-authored-by: Daniel Petrovic --- runtime/lua/vim/treesitter/highlighter.lua | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/highlighter.lua b/runtime/lua/vim/treesitter/highlighter.lua index c11fa1999d..6dd47811bd 100644 --- a/runtime/lua/vim/treesitter/highlighter.lua +++ b/runtime/lua/vim/treesitter/highlighter.lua @@ -160,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 -- cgit From e7ebc5c13d2d1658005a7fb477bc92718044746f Mon Sep 17 00:00:00 2001 From: notomo Date: Sat, 25 Jan 2025 21:15:01 +0900 Subject: fix(treesitter): stop async parsing if buffer is invalid Problem: Error occurs if delete buffer in the middle of parsing. Solution: Check if buffer is valid in parsing. --- runtime/lua/vim/treesitter/languagetree.lua | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index 5e1156fa68..8ea1c44cdc 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -475,13 +475,18 @@ function LanguageTree:_async_parse(range, on_parse) return end - local buf = vim.b[self._source] + local source = self._source + local buf = vim.b[source] local ct = buf.changedtick local total_parse_time = 0 local redrawtime = vim.o.redrawtime local timeout = not vim.g._ts_force_sync_parsing and default_parse_timeout_ms or nil local function step() + if type(source) == 'number' and not vim.api.nvim_buf_is_valid(source) then + return nil + end + -- If buffer was changed in the middle of parsing, reset parse state if buf.changedtick ~= ct then ct = buf.changedtick -- cgit From e71d2c817d1a2475551f58a98e411f6b39a5be3f Mon Sep 17 00:00:00 2001 From: dundargoc Date: Mon, 13 Jan 2025 15:48:02 +0100 Subject: docs: misc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Dustin S. Co-authored-by: Ferenc Fejes Co-authored-by: Maria José Solano Co-authored-by: Yochem van Rosmalen Co-authored-by: brianhuster Co-authored-by: zeertzjq --- runtime/lua/vim/treesitter/languagetree.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index 8ea1c44cdc..1f7872715f 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -523,7 +523,7 @@ end --- only the root tree without injections). --- @param on_parse fun(err?: string, trees?: table)? Function invoked when parsing completes. --- When provided and `vim.g._ts_force_sync_parsing` is not set, parsing will run ---- asynchronously. The first argument to the function is a string respresenting the error type, +--- asynchronously. The first argument to the function is a string representing the error type, --- in case of a failure (currently only possible for timeouts). The second argument is the list --- of trees returned by the parse (upon success), or `nil` if the parse timed out (determined --- by 'redrawtime'). -- cgit From 096ae3bfd7075dce69c70182ccedcd6d33e66d31 Mon Sep 17 00:00:00 2001 From: Riley Bruins Date: Thu, 30 Jan 2025 13:34:46 -0800 Subject: fix(treesitter): nil access when running string parser async --- runtime/lua/vim/treesitter/languagetree.lua | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index 1f7872715f..4e4da5a5ec 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -476,21 +476,26 @@ function LanguageTree:_async_parse(range, on_parse) end local source = self._source - local buf = vim.b[source] - local ct = buf.changedtick + local is_buffer_parser = type(source) == 'number' + local buf = is_buffer_parser and vim.b[source] or nil + local ct = is_buffer_parser and buf.changedtick or nil local total_parse_time = 0 local redrawtime = vim.o.redrawtime local timeout = not vim.g._ts_force_sync_parsing and default_parse_timeout_ms or nil local function step() - if type(source) == 'number' and not vim.api.nvim_buf_is_valid(source) then - return nil - end + if is_buffer_parser then + if + not vim.api.nvim_buf_is_valid(source --[[@as number]]) + then + return nil + end - -- If buffer was changed in the middle of parsing, reset parse state - if buf.changedtick ~= ct then - ct = buf.changedtick - total_parse_time = 0 + -- If buffer was changed in the middle of parsing, reset parse state + if buf.changedtick ~= ct then + ct = buf.changedtick + total_parse_time = 0 + end end local parse_time, trees, finished = tcall(self._parse, self, range, timeout) -- cgit From 02ea0e77a19b116006dc04848703aaeed3f50ded Mon Sep 17 00:00:00 2001 From: Riley Bruins Date: Sun, 2 Feb 2025 03:42:47 -0800 Subject: refactor(treesitter): drop `LanguageTree._has_regions` #32274 This simplifies some logic in `languagetree.lua`, removing the need for `_has_regions`, and removing side effects in `:included_regions()`. Before: - Edit is made which sets `_regions = nil` - Upon the next call to `included_regions()` (usually right after we marked `_regions` as `nil` due to an `_iter_regions()` call), if `_regions` is nil, we repopulate the table (as long as the tree actually has regions) After: - Edit is made which resets `_regions` if it exists - `included_regions()` no longer needs to perform this logic itself, and also no longer needs to read a `_has_regions` variable --- runtime/lua/vim/treesitter/languagetree.lua | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index 4e4da5a5ec..9571a117b8 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -82,7 +82,6 @@ local TSCallbackNames = { ---@field private _ranges_being_parsed table ---Table of callback queues, keyed by each region for which the callbacks should be run ---@field private _cb_queues table)[]> ----@field private _has_regions boolean ---@field private _regions table? ---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() @@ -132,7 +131,6 @@ 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, _parser = vim._create_ts_parser(lang), @@ -743,8 +741,6 @@ end ---@private ---@param new_regions (Range4|Range6|TSNode)[][] List of regions this tree should manage and parse. function LanguageTree:set_included_regions(new_regions) - self._has_regions = true - -- Transform the tables from 4 element long to 6 element long (with byte offset) for _, region in ipairs(new_regions) do for i, range in ipairs(region) do @@ -788,18 +784,8 @@ function LanguageTree:included_regions() return self._regions end - if not self._has_regions then - -- treesitter.c will default empty ranges to { -1, -1, -1, -1, -1, -1} (the full range) - return { {} } - end - - local regions = {} ---@type Range6[][] - for i, _ in pairs(self._trees) do - regions[i] = self._trees[i]:included_ranges(true) - end - - self._regions = regions - return regions + -- treesitter.c will default empty ranges to { -1, -1, -1, -1, -1, -1} (the full range) + return { {} } end ---@param node TSNode @@ -1050,7 +1036,14 @@ function LanguageTree:_edit( end self._parser:reset() - self._regions = nil + + if self._regions then + local regions = {} ---@type table + for i, tree in pairs(self._trees) do + regions[i] = tree:included_ranges(true) + end + self._regions = regions + end local changed_range = { start_row, -- cgit From 77be44563acb64a481d48f45c8dbbfca2d7db415 Mon Sep 17 00:00:00 2001 From: Riley Bruins Date: Sun, 2 Feb 2025 03:46:26 -0800 Subject: refactor(treesitter): always return valid range from parse() #32273 Problem: When running an initial parse, parse() returns an empty table rather than an actual range. In `languagetree.lua`, we manually check if a parse was incremental to determine the changed parse region. Solution: - Always return a range (in the C side) from parse(). - Simplify the language tree code a bit. - Logger no longer shows empty ranges on the initial parse. --- runtime/lua/vim/treesitter/languagetree.lua | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index 9571a117b8..725e95dfc9 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -378,10 +378,7 @@ function LanguageTree:_parse_regions(range, timeout) return changes, no_regions_parsed, total_parse_time, false end - -- Pass ranges if this is an initial parse - local cb_changes = self._trees[i] and tree_changes or tree:included_ranges(true) - - self:_do_callback('changedtree', cb_changes, tree) + self:_do_callback('changedtree', tree_changes, tree) self._trees[i] = tree vim.list_extend(changes, tree_changes) -- cgit From 9508d6a8146350ffc9f31f4263fa871bab9130bf Mon Sep 17 00:00:00 2001 From: Riley Bruins Date: Fri, 17 Jan 2025 14:35:05 -0800 Subject: refactor(treesitter): use coroutines for resuming _parse() logic This means that all work previously done by a `_parse()` iteration will be kept in future iterations. This prevents it from running indefinitely in some cases where the file is very large and there are 2+ injections. --- runtime/lua/vim/treesitter/languagetree.lua | 70 ++++++++++++++++++++--------- 1 file changed, 49 insertions(+), 21 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index 725e95dfc9..f876d9fe7d 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -60,6 +60,8 @@ local default_parse_timeout_ms = 3 ---| 'on_child_added' ---| 'on_child_removed' +---@alias ParserThreadState { timeout: integer? } + --- @type table local TSCallbackNames = { on_changedtree = 'changedtree', @@ -345,12 +347,12 @@ end --- @private --- @param range boolean|Range? ---- @param timeout integer? +--- @param thread_state ParserThreadState --- @return Range6[] changes --- @return integer no_regions_parsed --- @return number total_parse_time --- @return boolean finished whether async parsing still needs time -function LanguageTree:_parse_regions(range, timeout) +function LanguageTree:_parse_regions(range, thread_state) local changes = {} local no_regions_parsed = 0 local total_parse_time = 0 @@ -370,12 +372,18 @@ function LanguageTree:_parse_regions(range, timeout) ) then self._parser:set_included_ranges(ranges) - self._parser:set_timeout(timeout and timeout * 1000 or 0) -- ms -> micros + self._parser:set_timeout(thread_state.timeout and thread_state.timeout * 1000 or 0) -- ms -> micros + local parse_time, tree, tree_changes = tcall(self._parser.parse, self._parser, self._trees[i], self._source, true) + while true do + if tree then + break + end + coroutine.yield(changes, no_regions_parsed, total_parse_time, false) - if not tree then - return changes, no_regions_parsed, total_parse_time, false + parse_time, tree, tree_changes = + tcall(self._parser.parse, self._parser, self._trees[i], self._source, true) end self:_do_callback('changedtree', tree_changes, tree) @@ -476,7 +484,11 @@ function LanguageTree:_async_parse(range, on_parse) local ct = is_buffer_parser and buf.changedtick or nil local total_parse_time = 0 local redrawtime = vim.o.redrawtime - local timeout = not vim.g._ts_force_sync_parsing and default_parse_timeout_ms or nil + + local thread_state = {} ---@type ParserThreadState + + ---@type fun(): table, boolean + local parse = coroutine.wrap(self._parse) local function step() if is_buffer_parser then @@ -490,10 +502,12 @@ function LanguageTree:_async_parse(range, on_parse) if buf.changedtick ~= ct then ct = buf.changedtick total_parse_time = 0 + parse = coroutine.wrap(self._parse) end end - local parse_time, trees, finished = tcall(self._parse, self, range, timeout) + thread_state.timeout = not vim.g._ts_force_sync_parsing and default_parse_timeout_ms or nil + local parse_time, trees, finished = tcall(parse, self, range, thread_state) total_parse_time = total_parse_time + parse_time if finished then @@ -535,16 +549,16 @@ function LanguageTree:parse(range, on_parse) if on_parse then return self:_async_parse(range, on_parse) end - local trees, _ = self:_parse(range) + local trees, _ = self:_parse(range, {}) return trees end --- @private --- @param range boolean|Range|nil ---- @param timeout integer? +--- @param thread_state ParserThreadState --- @return table trees --- @return boolean finished -function LanguageTree:_parse(range, timeout) +function LanguageTree:_parse(range, thread_state) if self:is_valid() then self:_log('valid') return self._trees, true @@ -559,11 +573,18 @@ function LanguageTree:_parse(range, timeout) -- At least 1 region is invalid if not self:is_valid(true) then - local is_finished - changes, no_regions_parsed, total_parse_time, is_finished = self:_parse_regions(range, timeout) - timeout = timeout and math.max(timeout - total_parse_time, 0) - if not is_finished then - return self._trees, false + ---@type fun(self: vim.treesitter.LanguageTree, range: boolean|Range?, thread_state: ParserThreadState): Range6[], integer, number, boolean + local parse_regions = coroutine.wrap(self._parse_regions) + while true do + local is_finished + changes, no_regions_parsed, total_parse_time, is_finished = + parse_regions(self, range, thread_state) + thread_state.timeout = thread_state.timeout + and math.max(thread_state.timeout - total_parse_time, 0) + if is_finished then + break + end + coroutine.yield(self._trees, false) end -- Need to run injections when we parsed something if no_regions_parsed > 0 then @@ -585,13 +606,20 @@ function LanguageTree:_parse(range, timeout) }) for _, child in pairs(self._children) do - if timeout == 0 then - return self._trees, false + if thread_state.timeout == 0 then + coroutine.yield(self._trees, false) end - local ctime, _, child_finished = tcall(child._parse, child, range, timeout) - timeout = timeout and math.max(timeout - ctime, 0) - if not child_finished then - return self._trees, child_finished + + ---@type fun(): table, boolean + local parse = coroutine.wrap(child._parse) + + while true do + local ctime, _, child_finished = tcall(parse, child, range, thread_state) + if child_finished then + thread_state.timeout = thread_state.timeout and math.max(thread_state.timeout - ctime, 0) + break + end + coroutine.yield(self._trees, child_finished) end end -- cgit From 8543aa406c4ae88cc928372b2f8105005cdd0a80 Mon Sep 17 00:00:00 2001 From: Riley Bruins Date: Wed, 29 Jan 2025 15:53:34 -0800 Subject: feat(treesitter): allow LanguageTree:is_valid() to accept a range When given, only that range will be checked for validity rather than the entire tree. This is used in the highlighter to save CPU cycles since we only need to parse a certain region at a time anyway. --- runtime/lua/vim/treesitter/languagetree.lua | 129 +++++++++++++++------------- 1 file changed, 67 insertions(+), 62 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index f876d9fe7d..ea745c4deb 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -85,6 +85,8 @@ local TSCallbackNames = { ---Table of callback queues, keyed by each region for which the callbacks should be run ---@field private _cb_queues table)[]> ---@field private _regions table? +---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 @@ -92,7 +94,8 @@ local TSCallbackNames = { ---@field private _source (integer|string) Buffer or string to parse ---@field private _trees table 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 If the parsed tree is valid +---@field private _valid_regions table 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 = {} @@ -134,7 +137,9 @@ function LanguageTree.new(source, lang, opts) _injection_query = injections[lang] and query.parse(lang, injections[lang]) or query.get(lang, 'injections'), _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 = {}, @@ -240,7 +245,8 @@ 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 @@ -273,16 +279,46 @@ 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 @@ -294,17 +330,12 @@ 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 @@ -320,31 +351,6 @@ function LanguageTree:source() return self._source 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 - --- @private --- @param range boolean|Range? --- @param thread_state ParserThreadState @@ -357,15 +363,11 @@ function LanguageTree:_parse_regions(range, thread_state) local no_regions_parsed = 0 local total_parse_time = 0 - if type(self._valid) ~= 'table' then - self._valid = {} - end - -- If there are no ranges, set to an empty list -- so the included ranges in the parser are cleared. for i, ranges in pairs(self:included_regions()) do if - not self._valid[i] + not self._valid_regions[i] and ( intercepts_region(ranges, range) or (self._trees[i] and intercepts_region(self._trees[i]:included_ranges(false), range)) @@ -392,7 +394,13 @@ function LanguageTree:_parse_regions(range, thread_state) total_parse_time = total_parse_time + parse_time no_regions_parsed = no_regions_parsed + 1 - self._valid[i] = true + self._valid_regions[i] = true + + -- _valid_regions can have holes, but that is okay because this equality is only true when it + -- has no holes (meaning all regions are valid) + if #self._valid_regions == self._num_regions then + self._is_entirely_valid = true + end end end @@ -559,7 +567,7 @@ end --- @return table trees --- @return boolean finished function LanguageTree:_parse(range, thread_state) - if self:is_valid() then + if self:is_valid(nil, type(range) == 'table' and range or nil) then self:_log('valid') return self._trees, true end @@ -572,7 +580,7 @@ function LanguageTree:_parse(range, thread_state) local total_parse_time = 0 -- At least 1 region is invalid - if not self:is_valid(true) then + if not self:is_valid(true, type(range) == 'table' and range or nil) then ---@type fun(self: vim.treesitter.LanguageTree, range: boolean|Range?, thread_state: ParserThreadState): Range6[], integer, number, boolean local parse_regions = coroutine.wrap(self._parse_regions) while true do @@ -715,38 +723,34 @@ end ---region is valid or not. ---@param fn fun(index: integer, region: Range6[]): boolean function LanguageTree:_iter_regions(fn) - if not self._valid then + if vim.deep_equal(self._valid_regions, {}) then return end - local was_valid = type(self._valid) ~= 'table' - - if was_valid then - self:_log('was valid', self._valid) - self._valid = {} + if self._is_entirely_valid then + self:_log('was valid') end local all_valid = true for i, region in pairs(self:included_regions()) do - if was_valid or self._valid[i] then - self._valid[i] = fn(i, region) - if not self._valid[i] then + if self._valid_regions[i] then + -- Setting this to nil rather than false allows us to determine if all regions were parsed + -- just by checking the length of _valid_regions. + self._valid_regions[i] = fn(i, region) and true or nil + if not self._valid_regions[i] then self:_log(function() return 'invalidating region', i, region_tostr(region) end) end end - if not self._valid[i] then + if not self._valid_regions[i] then all_valid = false end end - -- Compress the valid value to 'true' if there are no invalid regions - if all_valid then - self._valid = all_valid - end + self._is_entirely_valid = all_valid end --- Sets the included regions that should be parsed by this |LanguageTree|. @@ -796,6 +800,7 @@ function LanguageTree:set_included_regions(new_regions) end self._regions = new_regions + self._num_regions = #new_regions end ---Gets the set of included regions managed by this LanguageTree. This can be different from the -- cgit From 09f9f0a94625002f4c70efbdf858fe6918cbc9c6 Mon Sep 17 00:00:00 2001 From: Riley Bruins Date: Tue, 4 Feb 2025 09:25:03 -0800 Subject: feat(treesitter): show which nodes are missing in InspectTree Now `:InspectTree` will show missing nodes as e.g. `(MISSING identifier)` or `(MISSING ";")` rather than just `(identifier)` or `";"`. This is doable because the `MISSING` keyword is now valid query syntax. Co-authored-by: Christian Clason --- runtime/lua/vim/treesitter/dev.lua | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/dev.lua b/runtime/lua/vim/treesitter/dev.lua index ab08e1a527..24dd8243db 100644 --- a/runtime/lua/vim/treesitter/dev.lua +++ b/runtime/lua/vim/treesitter/dev.lua @@ -224,9 +224,12 @@ function TSTreeView:draw(bufnr) 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) -- cgit