From c032e83b22994332dd8769ef34cb817906a63cac Mon Sep 17 00:00:00 2001 From: Christian Clason Date: Thu, 26 Jan 2023 09:42:23 +0100 Subject: fix(treesitter): validate language name Problem: Some injections (like markdown) allow specifying arbitrary language names for code blocks, which may be lead to errors when looking for a corresponding parser in runtime path. Solution: Validate that the language name only contains alphanumeric characters and `_` (e.g., for `c_sharp`) and error otherwise. --- runtime/lua/vim/treesitter/language.lua | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/language.lua b/runtime/lua/vim/treesitter/language.lua index c92d63b8c4..8634e53b7b 100644 --- a/runtime/lua/vim/treesitter/language.lua +++ b/runtime/lua/vim/treesitter/language.lua @@ -6,7 +6,7 @@ local M = {} --- --- Parsers are searched in the `parser` runtime directory, or the provided {path} --- ----@param lang string Language the parser should parse +---@param lang string Language the parser should parse (alphanumerical and `_` only) ---@param path (string|nil) Optional path the parser is located at ---@param silent (boolean|nil) Don't throw an error if language not found ---@param symbol_name (string|nil) Internal symbol name for the language to load @@ -16,13 +16,19 @@ function M.require_language(lang, path, silent, symbol_name) return true end if path == nil then - local fname = 'parser/' .. vim.fn.fnameescape(lang) .. '.*' + if not (lang and lang:match('[%w_]+') == lang) then + if silent then + return false + end + error("'" .. lang .. "' is not a valid language name") + end + + local fname = 'parser/' .. lang .. '.*' local paths = a.nvim_get_runtime_file(fname, false) if #paths == 0 then if silent then return false end - error("no parser for '" .. lang .. "' language, see :help treesitter-parsers") end path = paths[1] -- cgit From 9a5678463c96baf3b39cb3083ddf0da87d39aa23 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Sat, 4 Feb 2023 14:58:38 +0000 Subject: fix(treesitter): fix most diagnostics --- runtime/lua/vim/treesitter/_meta.lua | 60 +++++++++++++ runtime/lua/vim/treesitter/highlighter.lua | 50 +++++++++-- runtime/lua/vim/treesitter/languagetree.lua | 128 +++++++++++++++++++--------- runtime/lua/vim/treesitter/playground.lua | 36 ++++---- runtime/lua/vim/treesitter/query.lua | 109 ++++++++++++++++------- 5 files changed, 289 insertions(+), 94 deletions(-) create mode 100644 runtime/lua/vim/treesitter/_meta.lua (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/_meta.lua b/runtime/lua/vim/treesitter/_meta.lua new file mode 100644 index 0000000000..87b4560798 --- /dev/null +++ b/runtime/lua/vim/treesitter/_meta.lua @@ -0,0 +1,60 @@ +---@meta + +---@class TSNode +---@field id fun(self: TSNode): integer +---@field range fun(self: TSNode): integer, integer, integer, integer +---@field start fun(self: TSNode): integer, integer, integer +---@field end_ fun(self: TSNode): integer, integer, integer +---@field type fun(self: TSNode): string +---@field symbol fun(self: TSNode): integer +---@field named fun(self: TSNode): boolean +---@field missing fun(self: TSNode): boolean +---@field child_count fun(self: TSNode): integer +---@field named_child_count fun(self: TSNode): integer +---@field child fun(self: TSNode, integer): TSNode +---@field name_child fun(self: TSNode, integer): TSNode +---@field descendant_for_range fun(self: TSNode, integer, integer, integer, integer): TSNode +---@field named_descendant_for_range fun(self: TSNode, integer, integer, integer, integer): TSNode +---@field parent fun(self: TSNode): TSNode +---@field next_sibling fun(self: TSNode): TSNode +---@field prev_sibling fun(self: TSNode): TSNode +---@field next_named_sibling fun(self: TSNode): TSNode +---@field prev_named_sibling fun(self: TSNode): TSNode +---@field named_children fun(self: TSNode): TSNode[] +---@field has_error fun(self: TSNode): boolean +---@field iter_children fun(self: TSNode): fun(): TSNode, string +local TSNode = {} + +---@param query userdata +---@param captures true +---@param start integer +---@param end_ integer +---@return fun(): integer, TSNode, any +function TSNode:_rawquery(query, captures, start, end_) end + +---@param query userdata +---@param captures false +---@param start integer +---@param end_ integer +---@return fun(): string, any +function TSNode:_rawquery(query, captures, start, end_) end + +---@class TSParser +---@field parse fun(self: TSParser, tree, source: integer|string): TSTree, integer[] +---@field included_ranges fun(self: TSParser): integer[] +---@field set_included_ranges fun(self: TSParser, ranges: integer[][]) + +---@class TSTree +---@field root fun(self: TSTree): TSNode +---@field edit fun(self: TSTree, _: integer, _: integer, _: integer, _: integer, _: integer, _: integer, _: integer, _: integer, _:integer) +---@field copy fun(self: TSTree): TSTree + +---@return integer +vim._ts_get_language_version = function() end + +---@return integer +vim._ts_get_minimum_language_version = function() end + +---@param lang string +---@return TSParser +vim._create_ts_parser = function(lang) end diff --git a/runtime/lua/vim/treesitter/highlighter.lua b/runtime/lua/vim/treesitter/highlighter.lua index d77a0d0d03..8adaa4ef2f 100644 --- a/runtime/lua/vim/treesitter/highlighter.lua +++ b/runtime/lua/vim/treesitter/highlighter.lua @@ -1,13 +1,27 @@ local a = vim.api local query = require('vim.treesitter.query') --- support reload for quick experimentation +---@alias TSHlIter fun(): integer, TSNode, TSMetadata + +---@class TSHighlightState +---@field next_row integer +---@field iter TSHlIter|nil + ---@class TSHighlighter +---@field active table +---@field bufnr integer +---@field orig_spelloptions string +---@field _highlight_states table +---@field _queries table +---@field tree LanguageTree local TSHighlighter = rawget(vim.treesitter, 'TSHighlighter') or {} TSHighlighter.__index = TSHighlighter TSHighlighter.active = TSHighlighter.active or {} +---@class TSHighlighterQuery +---@field _query Query|nil +---@field hl_cache table local TSHighlighterQuery = {} TSHighlighterQuery.__index = TSHighlighterQuery @@ -46,7 +60,7 @@ end --- Creates a new highlighter using @param tree --- ----@param tree LanguageTree |LanguageTree| parser object to use for highlighting +---@param tree LanguageTree parser object to use for highlighting ---@param opts (table|nil) Configuration of the highlighter: --- - queries table overwrite queries used by the highlighter ---@return TSHighlighter Created highlighter object @@ -57,9 +71,10 @@ function TSHighlighter.new(tree, opts) error('TSHighlighter can not be used with a string parser source.') end - opts = opts or {} + opts = opts or {} ---@type { queries: table } self.tree = tree tree:register_cbs({ + ---@diagnostic disable:invisible on_changedtree = function(...) self:on_changedtree(...) end, @@ -67,17 +82,20 @@ function TSHighlighter.new(tree, opts) self:on_bytes(...) end, on_detach = function(...) + ---@diagnostic disable-next-line:redundant-parameter self:on_detach(...) end, }) - self.bufnr = tree:source() + self.bufnr = tree:source() --[[@as integer]] self.edit_count = 0 self.redraw_count = 0 self.line_count = {} -- A map of highlight states. -- This state is kept during rendering across each line update. self._highlight_states = {} + + ---@type table self._queries = {} -- Queries for a specific language can be overridden by a custom @@ -128,6 +146,8 @@ function TSHighlighter:destroy() end ---@private +---@param tstree TSTree +---@return TSHighlightState function TSHighlighter:get_highlight_state(tstree) if not self._highlight_states[tstree] then self._highlight_states[tstree] = { @@ -145,6 +165,8 @@ function TSHighlighter:reset_highlight_state() end ---@private +---@param start_row integer +---@param new_end integer function TSHighlighter:on_bytes(_, _, start_row, _, _, _, _, _, new_end) a.nvim__buf_redraw_range(self.bufnr, start_row, start_row + new_end + 1) end @@ -155,6 +177,7 @@ function TSHighlighter:on_detach() end ---@private +---@param changes integer[][]? function TSHighlighter:on_changedtree(changes) for _, ch in ipairs(changes or {}) do a.nvim__buf_redraw_range(self.bufnr, ch[1], ch[3] + 1) @@ -165,7 +188,7 @@ end -- ---@private ---@param lang string Language used by the highlighter. ----@return Query +---@return TSHighlighterQuery function TSHighlighter:get_query(lang) if not self._queries[lang] then self._queries[lang] = TSHighlighterQuery.new(lang) @@ -175,7 +198,12 @@ function TSHighlighter:get_query(lang) end ---@private +---@param self TSHighlighter +---@param buf integer +---@param line integer +---@param is_spell_nav boolean local function on_line_impl(self, buf, line, is_spell_nav) + ---@diagnostic disable:invisible self.tree:for_each_tree(function(tstree, tree) if not tstree then return @@ -213,7 +241,7 @@ local function on_line_impl(self, buf, line, is_spell_nav) local hl = highlighter_query.hl_cache[capture] local capture_name = highlighter_query:query().captures[capture] - local spell = nil + local spell = nil ---@type boolean? if capture_name == 'spell' then spell = true elseif capture_name == 'nospell' then @@ -242,6 +270,9 @@ local function on_line_impl(self, buf, line, is_spell_nav) end ---@private +---@param _win integer +---@param buf integer +---@param line integer function TSHighlighter._on_line(_, _win, buf, line, _) local self = TSHighlighter.active[buf] if not self then @@ -252,6 +283,9 @@ function TSHighlighter._on_line(_, _win, buf, line, _) end ---@private +---@param buf integer +---@param srow integer +---@param erow integer function TSHighlighter._on_spell_nav(_, _, buf, srow, _, erow, _) local self = TSHighlighter.active[buf] if not self then @@ -266,6 +300,7 @@ function TSHighlighter._on_spell_nav(_, _, buf, srow, _, erow, _) end ---@private +---@param buf integer function TSHighlighter._on_buf(_, buf) local self = TSHighlighter.active[buf] if self then @@ -274,6 +309,9 @@ function TSHighlighter._on_buf(_, buf) end ---@private +---@param _win integer +---@param buf integer +---@param _topline integer function TSHighlighter._on_win(_, _win, buf, _topline) local self = TSHighlighter.active[buf] if not self then diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index a1e96f8ef2..89aac3ae26 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -2,20 +2,39 @@ local a = vim.api local query = require('vim.treesitter.query') local language = require('vim.treesitter.language') ----@class LanguageTree ----@field _callbacks function[] Callback handlers ----@field _children LanguageTree[] Injected languages ----@field _injection_query table Queries defining injected languages ----@field _opts table Options ----@field _parser userdata Parser for language ----@field _regions table List of regions this tree should manage and parse ----@field _lang string Language name ----@field _regions table ----@field _source (number|string) Buffer or string to parse ----@field _trees userdata[] Reference to parsed |tstree| (one for each language) ----@field _valid boolean If the parsed tree is valid +---@alias Range {[1]: integer, [2]: integer, [3]: integer, [4]: integer} +-- +---@alias TSCallbackName +---| 'changedtree' +---| 'bytes' +---| 'detach' +---| 'child_added' +---| 'child_removed' + +---@alias TSCallbackNameOn +---| 'on_changedtree' +---| 'on_bytes' +---| 'on_detach' +---| 'on_child_added' +---| 'on_child_removed' +---@class LanguageTree +---@field private _callbacks table Callback handlers +---@field private _children table Injected languages +---@field private _injection_query Query Queries defining injected languages +---@field private _opts table Options +---@field private _parser TSParser Parser for language +---@field private _regions Range[][] List of regions this tree should manage and parse +---@field private _lang string Language name +---@field private _source (integer|string) Buffer or string to parse +---@field private _trees TSTree[] Reference to parsed tree (one for each language) +---@field private _valid boolean If the parsed tree is valid local LanguageTree = {} + +---@class LanguageTreeOpts +---@field queries table -- Deprecated +---@field injections table + LanguageTree.__index = LanguageTree --- A |LanguageTree| holds the treesitter parser for a given language {lang} used @@ -23,16 +42,17 @@ LanguageTree.__index = LanguageTree --- needs to store parsers for these child languages as well (which in turn may contain --- child languages themselves, hence the name). --- ----@param source (number|string) Buffer or a string of text to parse +---@param source (integer|string) Buffer or a string of text to parse ---@param lang string Root language this tree represents ---@param opts (table|nil) Optional keyword arguments: --- - injections table Mapping language to injection query strings. --- This is useful for overriding the built-in --- runtime file searching for the injection language --- query per language. ----@return LanguageTree |LanguageTree| parser object +---@return LanguageTree parser object function LanguageTree.new(source, lang, opts) language.require_language(lang) + ---@type LanguageTreeOpts opts = opts or {} if opts.queries then @@ -65,6 +85,7 @@ function LanguageTree.new(source, lang, opts) end --- Invalidates this parser and all its children +---@param reload boolean|nil function LanguageTree:invalidate(reload) self._valid = false @@ -73,7 +94,7 @@ function LanguageTree:invalidate(reload) self._trees = {} end - for _, child in ipairs(self._children) do + for _, child in pairs(self._children) do child:invalidate(reload) end end @@ -111,8 +132,8 @@ end --- This will run the injection query for this language to --- determine if any child languages should be created. --- ----@return userdata[] Table of parsed |tstree| ----@return table Change list +---@return TSTree[] +---@return table|nil Change list function LanguageTree:parse() if self._valid then return self._trees @@ -146,7 +167,7 @@ function LanguageTree:parse() end local injections_by_lang = self:_get_injections() - local seen_langs = {} + local seen_langs = {} ---@type table for lang, injection_ranges in pairs(injections_by_lang) do local has_lang = language.require_language(lang, nil, true) @@ -188,8 +209,8 @@ end --- Invokes the callback for each |LanguageTree| and its children recursively --- ----@param fn function(tree: LanguageTree, lang: string) ----@param include_self boolean Whether to include the invoking tree in the results +---@param fn fun(tree: LanguageTree, lang: string) +---@param include_self boolean|nil Whether to include the invoking tree in the results function LanguageTree:for_each_child(fn, include_self) if include_self then fn(self, self._lang) @@ -204,7 +225,7 @@ end --- --- Note: This includes the invoking tree's child trees as well. --- ----@param fn function(tree: TSTree, languageTree: LanguageTree) +---@param fn fun(tree: TSTree, ltree: LanguageTree) function LanguageTree:for_each_tree(fn) for _, tree in ipairs(self._trees) do fn(tree, self) @@ -221,7 +242,7 @@ end --- ---@private ---@param lang string Language to add. ----@return LanguageTree Injected |LanguageTree| +---@return LanguageTree injected function LanguageTree:add_child(lang) if self._children[lang] then self:remove_child(lang) @@ -258,7 +279,7 @@ end --- `remove_child` must be called on the parent to remove it. function LanguageTree:destroy() -- Cleanup here - for _, child in ipairs(self._children) do + for _, child in pairs(self._children) do child:destroy() end end @@ -280,20 +301,22 @@ end --- Note: This call invalidates the tree and requires it to be parsed again. --- ---@private ----@param regions table List of regions this tree should manage and parse. +---@param regions integer[][][] List of regions this tree should manage and parse. function LanguageTree:set_included_regions(regions) -- Transform the tables from 4 element long to 6 element long (with byte offset) for _, region in ipairs(regions) do for i, range in ipairs(region) do if type(range) == 'table' and #range == 4 then + ---@diagnostic disable-next-line:no-unknown local start_row, start_col, end_row, end_col = unpack(range) local start_byte = 0 local end_byte = 0 + local source = self._source -- TODO(vigoux): proper byte computation here, and account for EOL ? - if type(self._source) == 'number' then + if type(source) == 'number' then -- Easy case, this is a buffer parser - start_byte = a.nvim_buf_get_offset(self._source, start_row) + start_col - end_byte = a.nvim_buf_get_offset(self._source, end_row) + end_col + start_byte = a.nvim_buf_get_offset(source, start_row) + start_col + end_byte = a.nvim_buf_get_offset(source, end_row) + end_col elseif type(self._source) == 'string' then -- string parser, single `\n` delimited string start_byte = vim.fn.byteidx(self._source, start_col) @@ -320,9 +343,13 @@ function LanguageTree:included_regions() end ---@private +---@param node TSNode +---@param id integer +---@param metadata TSMetadata +---@return Range local function get_range_from_metadata(node, id, metadata) if metadata[id] and metadata[id].range then - return metadata[id].range + return metadata[id].range --[[@as Range]] end return { node:range() } end @@ -334,11 +361,13 @@ end --- TODO: Allow for an offset predicate to tailor the injection range --- instead of using the entire nodes range. ---@private +---@return table function LanguageTree:_get_injections() if not self._injection_query then return {} end + ---@type table>> local injections = {} for tree_index, tree in ipairs(self._trees) do @@ -348,14 +377,14 @@ function LanguageTree:_get_injections() for pattern, match, metadata in self._injection_query:iter_matches(root_node, self._source, start_line, end_line + 1) do - local lang = nil - local ranges = {} - local combined = metadata.combined + local lang = nil ---@type string + local ranges = {} ---@type Range[] + local combined = metadata.combined ---@type boolean -- Directives can configure how injections are captured as well as actual node captures. -- This allows more advanced processing for determining ranges and language resolution. if metadata.content then - local content = metadata.content + local content = metadata.content ---@type any -- Allow for captured nodes to be used if type(content) == 'number' then @@ -368,7 +397,7 @@ function LanguageTree:_get_injections() end if metadata.language then - lang = metadata.language + lang = metadata.language ---@type string end -- You can specify the content and language together @@ -379,7 +408,7 @@ function LanguageTree:_get_injections() -- Lang should override any other language tag if name == 'language' and not lang then - lang = query.get_node_text(node, self._source) + lang = query.get_node_text(node, self._source) --[[@as string]] elseif name == 'combined' then combined = true elseif name == 'content' and #ranges == 0 then @@ -417,6 +446,7 @@ function LanguageTree:_get_injections() end end + ---@type table local result = {} -- Generate a map by lang of node lists. @@ -429,11 +459,13 @@ function LanguageTree:_get_injections() for _, entry in pairs(patterns) do if entry.combined then + ---@diagnostic disable-next-line:no-unknown local regions = vim.tbl_map(function(e) return vim.tbl_flatten(e) end, entry.regions) table.insert(result[lang], regions) else + ---@diagnostic disable-next-line:no-unknown for _, ranges in ipairs(entry.regions) do table.insert(result[lang], ranges) end @@ -446,6 +478,7 @@ function LanguageTree:_get_injections() end ---@private +---@param cb_name TSCallbackName function LanguageTree:_do_callback(cb_name, ...) for _, cb in ipairs(self._callbacks[cb_name]) do cb(...) @@ -453,6 +486,17 @@ function LanguageTree:_do_callback(cb_name, ...) end ---@private +---@param bufnr integer +---@param changed_tick integer +---@param start_row integer +---@param start_col integer +---@param start_byte integer +---@param old_row integer +---@param old_col integer +---@param old_byte integer +---@param new_row integer +---@param new_col integer +---@param new_byte integer function LanguageTree:_on_bytes( bufnr, changed_tick, @@ -523,6 +567,7 @@ end --- - `on_child_added` : emitted when a child is added to the tree. --- - `on_child_removed` : emitted when a child is removed from the tree. function LanguageTree:register_cbs(cbs) + ---@cast cbs table if not cbs then return end @@ -549,6 +594,9 @@ function LanguageTree:register_cbs(cbs) end ---@private +---@param tree TSTree +---@param range Range +---@return boolean local function tree_contains(tree, range) local start_row, start_col, end_row, end_col = tree:root():range() local start_fits = start_row < range[1] or (start_row == range[1] and start_col <= range[2]) @@ -559,7 +607,7 @@ end --- Determines whether {range} is contained in the |LanguageTree|. --- ----@param range table `{ start_line, start_col, end_line, end_col }` +---@param range Range `{ start_line, start_col, end_line, end_col }` ---@return boolean function LanguageTree:contains(range) for _, tree in pairs(self._trees) do @@ -573,10 +621,10 @@ end --- Gets the tree that contains {range}. --- ----@param range table `{ start_line, start_col, end_line, end_col }` +---@param range Range `{ start_line, start_col, end_line, end_col }` ---@param opts table|nil Optional keyword arguments: --- - ignore_injections boolean Ignore injected languages (default true) ----@return userdata|nil Contained |tstree| +---@return TSTree|nil function LanguageTree:tree_for_range(range, opts) opts = opts or {} local ignore = vim.F.if_nil(opts.ignore_injections, true) @@ -602,10 +650,10 @@ end --- Gets the smallest named node that contains {range}. --- ----@param range table `{ start_line, start_col, end_line, end_col }` +---@param range Range `{ start_line, start_col, end_line, end_col }` ---@param opts table|nil Optional keyword arguments: --- - ignore_injections boolean Ignore injected languages (default true) ----@return userdata|nil Found |tsnode| +---@return TSNode|nil Found node function LanguageTree:named_node_for_range(range, opts) local tree = self:tree_for_range(range, opts) if tree then @@ -615,7 +663,7 @@ end --- Gets the appropriate language that contains {range}. --- ----@param range table `{ start_line, start_col, end_line, end_col }` +---@param range Range `{ start_line, start_col, end_line, end_col }` ---@return LanguageTree Managing {range} function LanguageTree:language_for_range(range) for _, child in pairs(self._children) do diff --git a/runtime/lua/vim/treesitter/playground.lua b/runtime/lua/vim/treesitter/playground.lua index bb073290c6..be7764e6f0 100644 --- a/runtime/lua/vim/treesitter/playground.lua +++ b/runtime/lua/vim/treesitter/playground.lua @@ -1,12 +1,13 @@ local api = vim.api -local M = {} - ----@class Playground +---@class TSPlayground ---@field ns number API namespace ---@field opts table Options table with the following keys: --- - anon (boolean): If true, display anonymous nodes --- - lang (boolean): If true, display the language alongside each node +---@field nodes Node[] +---@field named Node[] +local TSPlayground = {} --- ---@class Node ---@field id number Node id @@ -18,6 +19,7 @@ local M = {} ---@field end_lnum number Final line number of this node in the source buffer ---@field end_col number Final column number of this node in the source buffer ---@field lang string Source language of this node +---@field root TSNode --- Traverse all child nodes starting at {node}. --- @@ -31,10 +33,10 @@ local M = {} --- node of each of these trees is contained within a node in the primary tree. The {injections} --- table maps nodes in the primary tree to root nodes of injected trees. --- ----@param node userdata Starting node to begin traversal |tsnode| +---@param node TSNode Starting node to begin traversal |tsnode| ---@param depth number Current recursion depth ---@param lang string Language of the tree currently being traversed ----@param injections table Mapping of node ids to root nodes of injected language trees (see +---@param injections table Mapping of node ids to root nodes of injected language trees (see --- explanation above) ---@param tree Node[] Output table containing a list of tables each representing a node in the tree ---@private @@ -48,7 +50,7 @@ local function traverse(node, depth, lang, injections, tree) local type = child:type() local lnum, col, end_lnum, end_col = child:range() local named = child:named() - local text + local text ---@type string if named then if field then text = string.format('%s: (%s)', field, type) @@ -79,14 +81,14 @@ end --- Create a new Playground object. --- ----@param bufnr number Source buffer number +---@param bufnr integer Source buffer number ---@param lang string|nil Language of source buffer --- ----@return Playground|nil +---@return TSPlayground|nil ---@return string|nil Error message, if any --- ---@private -function M.new(self, bufnr, lang) +function TSPlayground:new(bufnr, lang) local ok, parser = pcall(vim.treesitter.get_parser, bufnr or 0, lang) if not ok then return nil, 'No parser available for the given buffer' @@ -96,7 +98,7 @@ function M.new(self, bufnr, lang) -- the primary tree that contains that root. Add a mapping from the node in the primary tree to -- the root in the child tree to the {injections} table. local root = parser:parse()[1]:root() - local injections = {} + local injections = {} ---@type table parser:for_each_child(function(child, lang_) child:for_each_tree(function(tree) local r = tree:root() @@ -112,7 +114,7 @@ function M.new(self, bufnr, lang) local nodes = traverse(root, 0, parser:lang(), injections, {}) - local named = {} + local named = {} ---@type Node[] for _, v in ipairs(nodes) do if v.named then named[#named + 1] = v @@ -138,9 +140,9 @@ end --- ---@param bufnr number Buffer number to write into. ---@private -function M.draw(self, bufnr) +function TSPlayground:draw(bufnr) vim.bo[bufnr].modifiable = true - local lines = {} + local lines = {} ---@type string[] for _, item in self:iter() do lines[#lines + 1] = table.concat({ string.rep(' ', item.depth), @@ -168,19 +170,19 @@ end ---@param i number Node number to get ---@return Node ---@private -function M.get(self, i) +function TSPlayground:get(i) local t = self.opts.anon and self.nodes or self.named return t[i] end --- Iterate over all of the nodes in this Playground object. --- ----@return function Iterator over all nodes in this Playground +---@return (fun(): integer, Node) Iterator over all nodes in this Playground ---@return table ---@return number ---@private -function M.iter(self) +function TSPlayground:iter() return ipairs(self.opts.anon and self.nodes or self.named) end -return M +return TSPlayground diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index dbf134573d..84ed2667b9 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -1,21 +1,25 @@ local a = vim.api local language = require('vim.treesitter.language') --- query: pattern matching on trees --- predicate matching is implemented in lua --- ---@class Query ---@field captures string[] List of captures used in query ----@field info table Contains used queries, predicates, directives +---@field info TSQueryInfo Contains used queries, predicates, directives ---@field query userdata Parsed query local Query = {} Query.__index = Query +---@class TSQueryInfo +---@field captures table +---@field patterns table + local M = {} ---@private +---@param files string[] +---@return string[] local function dedupe_files(files) local result = {} + ---@type table local seen = {} for _, path in ipairs(files) do @@ -65,10 +69,10 @@ function M.get_query_files(lang, query_name, is_included) return {} end - local base_query = nil + local base_query = nil ---@type string? local extensions = {} - local base_langs = {} + local base_langs = {} ---@type string[] -- Now get the base languages by looking at the first line of every file -- The syntax is the following : @@ -87,6 +91,7 @@ function M.get_query_files(lang, query_name, is_included) local extension = false for modeline in + ---@return string function() return file:read('*l') end @@ -97,6 +102,7 @@ function M.get_query_files(lang, query_name, is_included) local langlist = modeline:match(MODELINE_FORMAT) if langlist then + ---@diagnostic disable-next-line:param-type-mismatch for _, incllang in ipairs(vim.split(langlist, ',', true)) do local is_optional = incllang:match('%(.*%)') @@ -137,6 +143,8 @@ function M.get_query_files(lang, query_name, is_included) end ---@private +---@param filenames string[] +---@return string local function read_query_files(filenames) local contents = {} @@ -147,7 +155,8 @@ local function read_query_files(filenames) return table.concat(contents, '') end ---- The explicitly set queries from |vim.treesitter.query.set_query()| +-- The explicitly set queries from |vim.treesitter.query.set_query()| +---@type table> local explicit_queries = setmetatable({}, { __index = function(t, k) local lang_queries = {} @@ -174,7 +183,7 @@ end ---@param lang string Language to use for the query ---@param query_name string Name of the query (e.g. "highlights") --- ----@return Query Parsed query +---@return Query|nil Parsed query function M.get_query(lang, query_name) if explicit_queries[lang][query_name] then return explicit_queries[lang][query_name] @@ -188,6 +197,7 @@ function M.get_query(lang, query_name) end end +---@type {[string]: {[string]: Query}} local query_cache = vim.defaulttable(function() return setmetatable({}, { __mode = 'v' }) end) @@ -226,11 +236,11 @@ end --- Gets the text corresponding to a given node --- ----@param node userdata |tsnode| +---@param node TSNode ---@param source (number|string) Buffer or string from which the {node} is extracted ---@param opts (table|nil) Optional parameters. --- - concat: (boolean) Concatenate result in a string (default true) ----@return (string[]|string) +---@return (string[]|string|nil) function M.get_node_text(node, source, opts) opts = opts or {} local concat = vim.F.if_nil(opts.concat, true) @@ -239,12 +249,12 @@ function M.get_node_text(node, source, opts) local end_row, end_col, end_byte = node:end_() if type(source) == 'number' then - local lines local eof_row = a.nvim_buf_line_count(source) if start_row >= eof_row then return nil end + local lines ---@type string[] if end_col == 0 then lines = a.nvim_buf_get_lines(source, start_row, end_row, true) end_col = -1 @@ -267,8 +277,13 @@ function M.get_node_text(node, source, opts) end end +---@alias TSMatch table + +---@alias TSPredicate fun(match: TSMatch, _, _, predicate: any[]): boolean + -- Predicate handler receive the following arguments -- (match, pattern, bufnr, predicate) +---@type table local predicate_handlers = { ['eq?'] = function(match, _, source, predicate) local node = match[predicate[2]] @@ -277,13 +292,13 @@ local predicate_handlers = { end local node_text = M.get_node_text(node, source) - local str + local str ---@type string if type(predicate[3]) == 'string' then -- (#eq? @aa "foo") str = predicate[3] else -- (#eq? @aa @bb) - str = M.get_node_text(match[predicate[3]], source) + str = M.get_node_text(match[predicate[3]], source) --[[@as string]] end if node_text ~= str or str == nil then @@ -299,7 +314,7 @@ local predicate_handlers = { return true end local regex = predicate[3] - return string.find(M.get_node_text(node, source), regex) + return string.find(M.get_node_text(node, source) --[[@as string]], regex) ~= nil end, ['match?'] = (function() @@ -321,10 +336,12 @@ local predicate_handlers = { }) return function(match, _, source, pred) + ---@cast match TSMatch local node = match[pred[2]] if not node then return true end + ---@diagnostic disable-next-line no-unknown local regex = compiled_vim_regexes[pred[3]] return regex:match_str(M.get_node_text(node, source)) end @@ -335,7 +352,7 @@ local predicate_handlers = { if not node then return true end - local node_text = M.get_node_text(node, source) + local node_text = M.get_node_text(node, source) --[[@as string]] for i = 3, #predicate do if string.find(node_text, predicate[i], 1, true) then @@ -359,6 +376,7 @@ local predicate_handlers = { if not string_set then string_set = {} for i = 3, #predicate do + ---@diagnostic disable-next-line:no-unknown string_set[predicate[i]] = true end predicate['string_set'] = string_set @@ -371,21 +389,39 @@ local predicate_handlers = { -- As we provide lua-match? also expose vim-match? predicate_handlers['vim-match?'] = predicate_handlers['match?'] +---@class TSMetadata +---@field [integer] TSMetadata +---@field [string] integer|string +---@field range Range + +---@alias TSDirective fun(match: TSMatch, _, _, predicate: any[], metadata: TSMetadata) + +-- Predicate handler receive the following arguments +-- (match, pattern, bufnr, predicate) + -- Directives store metadata or perform side effects against a match. -- Directives should always end with a `!`. -- Directive handler receive the following arguments -- (match, pattern, bufnr, predicate, metadata) +---@type table local directive_handlers = { ['set!'] = function(_, _, _, pred, metadata) if #pred == 4 then -- (#set! @capture "key" "value") + ---@diagnostic disable-next-line:no-unknown local _, capture_id, key, value = unpack(pred) + ---@cast value integer|string + ---@cast capture_id integer + ---@cast key string if not metadata[capture_id] then metadata[capture_id] = {} end metadata[capture_id][key] = value else + ---@diagnostic disable-next-line:no-unknown local _, key, value = unpack(pred) + ---@cast value integer|string + ---@cast key string -- (#set! "key" "value") metadata[key] = value end @@ -393,9 +429,11 @@ local directive_handlers = { -- Shifts the range of a node. -- Example: (#offset! @_node 0 1 0 -1) ['offset!'] = function(match, _, _, pred, metadata) + ---@cast pred integer[] local capture_id = pred[2] local offset_node = match[capture_id] local range = { offset_node:range() } + ---@cast range integer[] bug in sumneko local start_row_offset = pred[3] or 0 local start_col_offset = pred[4] or 0 local end_row_offset = pred[5] or 0 @@ -419,8 +457,9 @@ local directive_handlers = { --- Adds a new predicate to be used in queries --- ---@param name string Name of the predicate, without leading # ----@param handler function(match:table, pattern:string, bufnr:number, predicate:string[]) +---@param handler function(match:table, pattern:string, bufnr:number, predicate:string[]) --- - see |vim.treesitter.query.add_directive()| for argument meanings +---@param force boolean function M.add_predicate(name, handler, force) if predicate_handlers[name] and not force then error(string.format('Overriding %s', name)) @@ -437,12 +476,13 @@ end --- metadata table `metadata[capture_id].key = value` --- ---@param name string Name of the directive, without leading # ----@param handler function(match:table, pattern:string, bufnr:number, predicate:string[], metadata:table) +---@param handler function(match:table, pattern:string, bufnr:number, predicate:string[], metadata:table) --- - match: see |treesitter-query| --- - node-level data are accessible via `match[capture_id]` --- - pattern: see |treesitter-query| --- - predicate: list of strings containing the full directive being called, e.g. --- `(node (#set! conceal "-"))` would get the predicate `{ "#set!", "conceal", "-" }` +---@param force boolean function M.add_directive(name, handler, force) if directive_handlers[name] and not force then error(string.format('Overriding %s', name)) @@ -474,6 +514,9 @@ local function is_directive(name) end ---@private +---@param match TSMatch +---@param pattern string +---@param source integer|string function Query:match_preds(match, pattern, source) local preds = self.info.patterns[pattern] @@ -482,8 +525,9 @@ function Query:match_preds(match, pattern, source) -- 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 pred_name - local is_not + local pred_name ---@type string + + local is_not ---@type boolean -- Skip over directives... they will get processed after all the predicates. if not is_directive(pred[1]) then @@ -513,6 +557,8 @@ function Query:match_preds(match, pattern, source) end ---@private +---@param match TSMatch +---@param metadata TSMetadata function Query:apply_directives(match, pattern, source, metadata) local preds = self.info.patterns[pattern] @@ -534,6 +580,10 @@ end -- When the node's range is used, the stop is incremented by 1 -- to make the search inclusive. ---@private +---@param start integer +---@param stop integer +---@param node TSNode +---@return integer, integer local function value_or_node_range(start, stop, node) if start == nil and stop == nil then local node_start, _, node_stop, _ = node:range() @@ -565,14 +615,12 @@ end --- end --- --- ----@param node userdata |tsnode| under which the search will occur ----@param source (number|string) Source buffer or string to extract text from +---@param node TSNode under which the search will occur +---@param source (integer|string) Source buffer or string to extract text from ---@param start number Starting line for the search ---@param stop number Stopping line for the search (end-exclusive) --- ----@return number capture Matching capture id ----@return table capture_node Capture for {node} ----@return table metadata for the {capture} +---@return (fun(): integer, TSNode, TSMetadata): capture id, capture node, metadata function Query:iter_captures(node, source, start, stop) if type(source) == 'number' and source == 0 then source = vim.api.nvim_get_current_buf() @@ -622,14 +670,12 @@ end --- end --- --- ----@param node userdata |tsnode| under which the search will occur ----@param source (number|string) Source buffer or string to search ----@param start number Starting line for the search ----@param stop number Stopping line for the search (end-exclusive) +---@param node TSNode under which the search will occur +---@param source (integer|string) Source buffer or string to search +---@param start integer Starting line for the search +---@param stop integer Stopping line for the search (end-exclusive) --- ----@return number pattern id ----@return table match ----@return table metadata +---@return (fun(): integer, table, table): pattern id, match, metadata function Query:iter_matches(node, source, start, stop) if type(source) == 'number' and source == 0 then source = vim.api.nvim_get_current_buf() @@ -638,6 +684,7 @@ function Query:iter_matches(node, source, start, stop) start, stop = value_or_node_range(start, stop, node) local raw_iter = node:_rawquery(self.query, false, start, stop) + ---@cast raw_iter fun(): string, any local function iter() local pattern, match = raw_iter() local metadata = {} -- cgit From bb8845340b1b9c2180fb19f049ff9deff5857d99 Mon Sep 17 00:00:00 2001 From: figsoda Date: Thu, 21 Jul 2022 12:08:37 +0100 Subject: feat(treesitter): allow capture text to be transformed Co-authored-by: Lewis Russell --- runtime/lua/vim/treesitter/languagetree.lua | 10 +++++++++- runtime/lua/vim/treesitter/query.lua | 15 +++++++++++++++ 2 files changed, 24 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 89aac3ae26..3e1bc5d1cb 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -354,6 +354,14 @@ local function get_range_from_metadata(node, id, metadata) return { node:range() } end +---@private +local function get_node_text(node, id, metadata, source) + if metadata[id] and metadata[id].text then + return metadata[id].text + end + return query.get_node_text(node, source) +end + --- Gets language injection points by language. --- --- This is where most of the injection processing occurs. @@ -408,7 +416,7 @@ function LanguageTree:_get_injections() -- Lang should override any other language tag if name == 'language' and not lang then - lang = query.get_node_text(node, self._source) --[[@as string]] + lang = get_node_text(node, id, metadata, self._source) --[[@as string]] elseif name == 'combined' then combined = true elseif name == 'content' and #ranges == 0 then diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index 84ed2667b9..5ec8c67462 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -452,6 +452,21 @@ local directive_handlers = { metadata[capture_id].range = range end end, + + -- Transform the content of the node + -- Example: (#gsub! @_node ".*%.(.*)" "%1") + ['gsub!'] = function(match, _, bufnr, pred, metadata) + assert(#pred == 4) + + local id = pred[2] + local node = match[id] + local text = M.get_node_text(node, bufnr, { metadata = metadata[id] }) or '' + + if not metadata[id] then + metadata[id] = {} + end + metadata[id].text = text:gsub(pred[3], pred[4]) + end, } --- Adds a new predicate to be used in queries -- cgit From e1d5ad1cb87d43c3d75619e239312d4ab2029b45 Mon Sep 17 00:00:00 2001 From: figsoda Date: Mon, 26 Dec 2022 16:10:59 -0500 Subject: feat(treesitter): add metadata option for get_node_text --- runtime/lua/vim/treesitter/languagetree.lua | 10 +---- runtime/lua/vim/treesitter/query.lua | 70 +++++++++++++++++------------ 2 files changed, 43 insertions(+), 37 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index 3e1bc5d1cb..8255c6f4fe 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -354,14 +354,6 @@ local function get_range_from_metadata(node, id, metadata) return { node:range() } end ----@private -local function get_node_text(node, id, metadata, source) - if metadata[id] and metadata[id].text then - return metadata[id].text - end - return query.get_node_text(node, source) -end - --- Gets language injection points by language. --- --- This is where most of the injection processing occurs. @@ -416,7 +408,7 @@ function LanguageTree:_get_injections() -- Lang should override any other language tag if name == 'language' and not lang then - lang = get_node_text(node, id, metadata, self._source) --[[@as string]] + lang = query.get_node_text(node, self._source, { metadata = metadata[id] }) elseif name == 'combined' then combined = true elseif name == 'content' and #ranges == 0 then diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index 5ec8c67462..9136b596be 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -55,6 +55,38 @@ local function add_included_lang(base_langs, lang, ilang) return false end +---@private +---@param buf (number) +---@param range (table) +---@param concat (boolean) +---@returns (string[]|string|nil) +local function buf_range_get_text(buf, range, concat) + local lines + local start_row, start_col, end_row, end_col = unpack(range) + local eof_row = a.nvim_buf_line_count(buf) + if start_row >= eof_row then + return nil + end + + if end_col == 0 then + lines = a.nvim_buf_get_lines(buf, start_row, end_row, true) + end_col = -1 + else + lines = a.nvim_buf_get_lines(buf, start_row, end_row + 1, true) + end + + if #lines > 0 then + if #lines == 1 then + lines[1] = string.sub(lines[1], start_col + 1, end_col) + else + lines[1] = string.sub(lines[1], start_col + 1) + lines[#lines] = string.sub(lines[#lines], 1, end_col) + end + end + + return concat and table.concat(lines, '\n') or lines +end + --- Gets the list of files used to make up a query --- ---@param lang string Language to get query for @@ -240,40 +272,22 @@ end ---@param source (number|string) Buffer or string from which the {node} is extracted ---@param opts (table|nil) Optional parameters. --- - concat: (boolean) Concatenate result in a string (default true) +--- - metadata (table) Metadata of a specific capture. This would be +--- set to `metadata[capture_id]` when using +--- |vim.treesitter.query.add_directive()|. ---@return (string[]|string|nil) function M.get_node_text(node, source, opts) opts = opts or {} local concat = vim.F.if_nil(opts.concat, true) + local metadata = opts.metadata or {} - local start_row, start_col, start_byte = node:start() - local end_row, end_col, end_byte = node:end_() - - if type(source) == 'number' then - local eof_row = a.nvim_buf_line_count(source) - if start_row >= eof_row then - return nil - end - - local lines ---@type string[] - if end_col == 0 then - lines = a.nvim_buf_get_lines(source, start_row, end_row, true) - end_col = -1 - else - lines = a.nvim_buf_get_lines(source, start_row, end_row + 1, true) - end - - if #lines > 0 then - if #lines == 1 then - lines[1] = string.sub(lines[1], start_col + 1, end_col) - else - lines[1] = string.sub(lines[1], start_col + 1) - lines[#lines] = string.sub(lines[#lines], 1, end_col) - end - end - - return concat and table.concat(lines, '\n') or lines + if metadata.text then + return metadata.text + elseif type(source) == 'number' then + return metadata.range and buf_range_get_text(source, metadata.range, concat) + or buf_range_get_text(source, { node:range() }, concat) elseif type(source) == 'string' then - return source:sub(start_byte + 1, end_byte) + return source:sub(select(3, node:start()) + 1, select(3, node:end_())) end end -- cgit From 4c66f5ff97a52fbc933fdbe1907c4b960d5a7403 Mon Sep 17 00:00:00 2001 From: figsoda Date: Mon, 26 Dec 2022 16:11:45 -0500 Subject: feat(treesitter): respect metadata[id].range for offset! --- runtime/lua/vim/treesitter/query.lua | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index 9136b596be..a0522d7cda 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -445,9 +445,11 @@ local directive_handlers = { ['offset!'] = function(match, _, _, pred, metadata) ---@cast pred integer[] local capture_id = pred[2] - local offset_node = match[capture_id] - local range = { offset_node:range() } - ---@cast range integer[] bug in sumneko + if not metadata[capture_id] then + metadata[capture_id] = {} + end + + local range = metadata[capture_id].range or { match[capture_id]:range() } local start_row_offset = pred[3] or 0 local start_col_offset = pred[4] or 0 local end_row_offset = pred[5] or 0 @@ -460,9 +462,6 @@ local directive_handlers = { -- If this produces an invalid range, we just skip it. if range[1] < range[3] or (range[1] == range[3] and range[2] <= range[4]) then - if not metadata[capture_id] then - metadata[capture_id] = {} - end metadata[capture_id].range = range end end, -- cgit From f5bad01869df449cb1e4ae7f264bcd5c8150f606 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Thu, 9 Feb 2023 15:20:47 +0000 Subject: feat(treesitter): playground improvements - Render node ranges as virtual text - Set filettype=query. The virtual text is to avoid parsing errors. - Make sure highlights text is always in view. --- runtime/lua/vim/treesitter/playground.lua | 36 +++++++++++++++++++------------ 1 file changed, 22 insertions(+), 14 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/playground.lua b/runtime/lua/vim/treesitter/playground.lua index be7764e6f0..001bc2d5bf 100644 --- a/runtime/lua/vim/treesitter/playground.lua +++ b/runtime/lua/vim/treesitter/playground.lua @@ -136,6 +136,8 @@ function TSPlayground:new(bufnr, lang) return t end +local decor_ns = api.nvim_create_namespace('ts.playground') + --- Write the contents of this Playground into {bufnr}. --- ---@param bufnr number Buffer number to write into. @@ -144,22 +146,28 @@ function TSPlayground:draw(bufnr) vim.bo[bufnr].modifiable = true local lines = {} ---@type string[] for _, item in self:iter() do - lines[#lines + 1] = table.concat({ - string.rep(' ', item.depth), - item.text, - item.lnum == item.end_lnum - and string.format(' [%d:%d-%d]', item.lnum + 1, item.col + 1, item.end_col) - or string.format( - ' [%d:%d-%d:%d]', - item.lnum + 1, - item.col + 1, - item.end_lnum + 1, - item.end_col - ), - self.opts.lang and string.format(' %s', item.lang) or '', - }) + lines[#lines + 1] = string.rep(' ', item.depth) .. item.text end api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) + + api.nvim_buf_clear_namespace(bufnr, decor_ns, 0, -1) + + for i, item in self:iter() do + local range_str + if item.lnum == item.end_lnum then + range_str = string.format('[%d:%d-%d]', item.lnum + 1, item.col + 1, item.end_col) + else + range_str = + string.format('[%d:%d-%d:%d]', item.lnum + 1, item.col + 1, item.end_lnum + 1, item.end_col) + end + + local lang_str = self.opts.lang and string.format(' %s', item.lang) or '' + + api.nvim_buf_set_extmark(bufnr, decor_ns, i - 1, 0, { + virt_text = { { range_str, 'Comment' }, { lang_str, 'Title' } }, + }) + end + vim.bo[bufnr].modifiable = false end -- cgit From a289e82142fdc5ff657dd30198546eeb1e115fe9 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Wed, 15 Feb 2023 12:26:07 +0000 Subject: fix(treesitter): make params optional --- 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 a0522d7cda..008e5a54d7 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -487,7 +487,7 @@ local directive_handlers = { ---@param name string Name of the predicate, without leading # ---@param handler function(match:table, pattern:string, bufnr:number, predicate:string[]) --- - see |vim.treesitter.query.add_directive()| for argument meanings ----@param force boolean +---@param force boolean|nil function M.add_predicate(name, handler, force) if predicate_handlers[name] and not force then error(string.format('Overriding %s', name)) @@ -510,7 +510,7 @@ end --- - pattern: see |treesitter-query| --- - predicate: list of strings containing the full directive being called, e.g. --- `(node (#set! conceal "-"))` would get the predicate `{ "#set!", "conceal", "-" }` ----@param force boolean +---@param force boolean|nil function M.add_directive(name, handler, force) if directive_handlers[name] and not force then error(string.format('Overriding %s', name)) -- cgit From 8714a4009c0f0be0bb27a6b3eb486eeb3d9f3049 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Tue, 21 Feb 2023 17:09:18 +0000 Subject: feat(treesitter): add filetype -> lang API Problem: vim.treesitter does not know how to map a specific filetype to a parser. This creates problems since in a few places (including in vim.treesitter itself), the filetype is incorrectly used in place of lang. Solution: Add an API to enable this: - Add vim.treesitter.language.add() as a replacement for vim.treesitter.language.require_language(). - Optional arguments are now passed via an opts table. - Also takes a filetype (or list of filetypes) so we can keep track of what filetypes are associated with which langs. - Deprecated vim.treesitter.language.require_language(). - Add vim.treesitter.language.get_lang() which returns the associated lang for a given filetype. - Add vim.treesitter.language.register() to associate filetypes to a lang without loading the parser. --- runtime/lua/vim/treesitter/health.lua | 2 +- runtime/lua/vim/treesitter/language.lua | 84 ++++++++++++++++++++++++++--- runtime/lua/vim/treesitter/languagetree.lua | 4 +- runtime/lua/vim/treesitter/query.lua | 2 +- 4 files changed, 80 insertions(+), 12 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/health.lua b/runtime/lua/vim/treesitter/health.lua index c0a1eca0ce..1abcdd0b31 100644 --- a/runtime/lua/vim/treesitter/health.lua +++ b/runtime/lua/vim/treesitter/health.lua @@ -17,7 +17,7 @@ function M.check() for _, parser in pairs(parsers) do local parsername = vim.fn.fnamemodify(parser, ':t:r') - local is_loadable, ret = pcall(ts.language.require_language, parsername) + local is_loadable, ret = pcall(ts.language.add, parsername) if not is_loadable or not ret then health.report_error( diff --git a/runtime/lua/vim/treesitter/language.lua b/runtime/lua/vim/treesitter/language.lua index 8634e53b7b..8637d7d544 100644 --- a/runtime/lua/vim/treesitter/language.lua +++ b/runtime/lua/vim/treesitter/language.lua @@ -2,19 +2,66 @@ local a = vim.api local M = {} +---@type table +local ft_to_lang = {} + +---@param filetype string +---@return string|nil +function M.get_lang(filetype) + return ft_to_lang[filetype] +end + +---@deprecated +function M.require_language(lang, path, silent, symbol_name) + return M.add(lang, { + silent = silent, + path = path, + symbol_name = symbol_name, + }) +end + +---@class treesitter.RequireLangOpts +---@field path? string +---@field silent? boolean +---@field filetype? string|string[] +---@field symbol_name? string + --- Asserts that a parser for the language {lang} is installed. --- --- Parsers are searched in the `parser` runtime directory, or the provided {path} --- ---@param lang string Language the parser should parse (alphanumerical and `_` only) ----@param path (string|nil) Optional path the parser is located at ----@param silent (boolean|nil) Don't throw an error if language not found ----@param symbol_name (string|nil) Internal symbol name for the language to load +---@param opts (table|nil) Options: +--- - filetype (string|string[]) Filetype(s) that lang can be parsed with. +--- Note this is not strictly the same as lang since a single lang can +--- parse multiple filetypes. +--- Defaults to lang. +--- - path (string|nil) Optional path the parser is located at +--- - symbol_name (string|nil) Internal symbol name for the language to load +--- - silent (boolean|nil) Don't throw an error if language not found ---@return boolean If the specified language is installed -function M.require_language(lang, path, silent, symbol_name) +function M.add(lang, opts) + ---@cast opts treesitter.RequireLangOpts + opts = opts or {} + local path = opts.path + local silent = opts.silent + local filetype = opts.filetype or lang + local symbol_name = opts.symbol_name + + vim.validate({ + lang = { lang, 'string' }, + path = { path, 'string', true }, + silent = { silent, 'boolean', true }, + symbol_name = { symbol_name, 'string', true }, + filetype = { filetype, { 'string', 'table' }, true }, + }) + + M.register(lang, filetype or lang) + if vim._ts_has_language(lang) then return true end + if path == nil then if not (lang and lang:match('[%w_]+') == lang) then if silent then @@ -35,9 +82,9 @@ function M.require_language(lang, path, silent, symbol_name) end if silent then - return pcall(function() - vim._ts_add_language(path, lang, symbol_name) - end) + if not pcall(vim._ts_add_language, path, lang, symbol_name) then + return false + end else vim._ts_add_language(path, lang, symbol_name) end @@ -45,6 +92,27 @@ function M.require_language(lang, path, silent, symbol_name) return true end +--- Register a lang to be used for a filetype (or list of filetypes). +---@param lang string Language to register +---@param filetype string|string[] Filetype(s) to associate with lang +function M.register(lang, filetype) + vim.validate({ + lang = { lang, 'string' }, + filetype = { filetype, { 'string', 'table' } }, + }) + + local filetypes ---@type string[] + if type(filetype) == 'string' then + filetypes = { filetype } + else + filetypes = filetype + end + + for _, f in ipairs(filetypes) do + ft_to_lang[f] = lang + end +end + --- Inspects the provided language. --- --- Inspecting provides some useful information on the language like node names, ... @@ -52,7 +120,7 @@ end ---@param lang string Language ---@return table function M.inspect_language(lang) - M.require_language(lang) + M.add(lang) return vim._ts_inspect_language(lang) end diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index 8255c6f4fe..81ad83db2c 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -51,7 +51,7 @@ LanguageTree.__index = LanguageTree --- query per language. ---@return LanguageTree parser object function LanguageTree.new(source, lang, opts) - language.require_language(lang) + language.add(lang) ---@type LanguageTreeOpts opts = opts or {} @@ -170,7 +170,7 @@ function LanguageTree:parse() local seen_langs = {} ---@type table for lang, injection_ranges in pairs(injections_by_lang) do - local has_lang = language.require_language(lang, nil, true) + local has_lang = language.add(lang, { silent = true }) -- Child language trees should just be ignored if not found, since -- they can depend on the text of a node. Intermediate strings diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index 008e5a54d7..83910316a6 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -252,7 +252,7 @@ end) --- ---@return Query Parsed query function M.parse_query(lang, query) - language.require_language(lang) + language.add(lang) local cached = query_cache[lang][query] if cached then return cached -- cgit From 05de0f4fea237132ff3a9a5a35e9c711c4ece579 Mon Sep 17 00:00:00 2001 From: Christian Clason Date: Tue, 21 Feb 2023 17:03:04 +0100 Subject: docs(treesitter): fix parse errors --- 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 81ad83db2c..c9fd4bb2ea 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -653,7 +653,7 @@ end ---@param range Range `{ start_line, start_col, end_line, end_col }` ---@param opts table|nil Optional keyword arguments: --- - ignore_injections boolean Ignore injected languages (default true) ----@return TSNode|nil Found node +---@return TSNode | nil Found node function LanguageTree:named_node_for_range(range, opts) local tree = self:tree_for_range(range, opts) if tree then -- cgit From 6dfbeb0d990d24657754463c6ab155c19e7f5f56 Mon Sep 17 00:00:00 2001 From: Christian Clason Date: Tue, 21 Feb 2023 17:39:29 +0100 Subject: docs: fix more treesitter parsing errors --- 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 83910316a6..58a29f2fe0 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -639,7 +639,7 @@ end --- -- typically useful info about the node: --- local type = node:type() -- type of the captured node --- local row1, col1, row2, col2 = node:range() -- range of the capture ---- ... use the info here ... +--- -- ... use the info here ... --- end --- --- @@ -693,7 +693,7 @@ end --- --- local node_data = metadata[id] -- Node level metadata --- ---- ... use the info here ... +--- -- ... use the info here ... --- end --- end --- -- cgit From 75e53341f37eeeda7d9be7f934249f7e5e4397e9 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Thu, 23 Feb 2023 15:19:52 +0000 Subject: perf(treesitter): smarter languagetree invalidation Problem: Treesitter injections are slow because all injected trees are invalidated on every change. Solution: Implement smarter invalidation to avoid reparsing injected regions. - In on_bytes, try and update self._regions as best we can. This PR just offsets any regions after the change. - Add valid flags for each region in self._regions. - Call on_bytes recursively for all children. - We still need to run the query every time for the top level tree. I don't know how to avoid this. However, if the new injection ranges don't change, then we re-use the old trees and avoid reparsing children. This should result in roughly a 2-3x reduction in tree parsing when the comment injections are enabled. --- runtime/lua/vim/treesitter/_range.lua | 126 ++++++++++++++ runtime/lua/vim/treesitter/languagetree.lua | 249 ++++++++++++++++++---------- runtime/lua/vim/treesitter/query.lua | 2 +- 3 files changed, 290 insertions(+), 87 deletions(-) create mode 100644 runtime/lua/vim/treesitter/_range.lua (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/_range.lua b/runtime/lua/vim/treesitter/_range.lua new file mode 100644 index 0000000000..b87542c20f --- /dev/null +++ b/runtime/lua/vim/treesitter/_range.lua @@ -0,0 +1,126 @@ +local api = vim.api + +local M = {} + +---@alias Range4 {[1]: integer, [2]: integer, [3]: integer, [4]: integer} +---@alias Range6 {[1]: integer, [2]: integer, [3]: integer, [4]: integer, [5]: integer, [6]: integer} + +---@private +---@param a_row integer +---@param a_col integer +---@param b_row integer +---@param b_col integer +---@return integer +--- 1: a > b +--- 0: a == b +--- -1: a < b +local function cmp_pos(a_row, a_col, b_row, b_col) + if a_row == b_row then + if a_col > b_col then + return 1 + elseif a_col < b_col then + return -1 + else + return 0 + end + elseif a_row > b_row then + return 1 + end + + return -1 +end + +M.cmp_pos = { + lt = function(...) + return cmp_pos(...) == -1 + end, + le = function(...) + return cmp_pos(...) ~= 1 + end, + gt = function(...) + return cmp_pos(...) == 1 + end, + ge = function(...) + return cmp_pos(...) ~= -1 + end, + eq = function(...) + return cmp_pos(...) == 0 + end, + ne = function(...) + return cmp_pos(...) ~= 0 + end, +} + +setmetatable(M.cmp_pos, { __call = cmp_pos }) + +---@private +---@param r1 Range4|Range6 +---@param r2 Range4|Range6 +---@return boolean +function M.intercepts(r1, r2) + local off_1 = #r1 == 6 and 1 or 0 + local off_2 = #r1 == 6 and 1 or 0 + + local srow_1, scol_1, erow_1, ecol_1 = r1[1], r2[2], r1[3 + off_1], r1[4 + off_1] + local srow_2, scol_2, erow_2, ecol_2 = r2[1], r2[2], r2[3 + off_2], r2[4 + off_2] + + -- r1 is above r2 + if M.cmp_pos.le(erow_1, ecol_1, srow_2, scol_2) then + return false + end + + -- r1 is below r2 + if M.cmp_pos.ge(srow_1, scol_1, erow_2, ecol_2) then + return false + end + + return true +end + +---@private +---@param r1 Range4|Range6 +---@param r2 Range4|Range6 +---@return boolean whether r1 contains r2 +function M.contains(r1, r2) + local off_1 = #r1 == 6 and 1 or 0 + local off_2 = #r1 == 6 and 1 or 0 + + local srow_1, scol_1, erow_1, ecol_1 = r1[1], r2[2], r1[3 + off_1], r1[4 + off_1] + local srow_2, scol_2, erow_2, ecol_2 = r2[1], r2[2], r2[3 + off_2], r2[4 + off_2] + + -- start doesn't fit + if M.cmp_pos.gt(srow_1, scol_1, srow_2, scol_2) then + return false + end + + -- end doesn't fit + if M.cmp_pos.lt(erow_1, ecol_1, erow_2, ecol_2) then + return false + end + + return true +end + +---@private +---@param source integer|string +---@param range Range4 +---@return Range6 +function M.add_bytes(source, range) + local start_row, start_col, end_row, end_col = range[1], range[2], range[3], range[4] + local start_byte = 0 + local end_byte = 0 + -- TODO(vigoux): proper byte computation here, and account for EOL ? + if type(source) == 'number' then + -- Easy case, this is a buffer parser + start_byte = api.nvim_buf_get_offset(source, start_row) + start_col + end_byte = api.nvim_buf_get_offset(source, end_row) + end_col + elseif type(source) == 'string' then + -- string parser, single `\n` delimited string + start_byte = vim.fn.byteidx(source, start_col) + end_byte = vim.fn.byteidx(source, end_col) + end + + return { start_row, start_col, start_byte, end_row, end_col, end_byte } +end + +return M diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index c9fd4bb2ea..2d4e2e595b 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -1,9 +1,8 @@ local a = vim.api local query = require('vim.treesitter.query') local language = require('vim.treesitter.language') +local Range = require('vim.treesitter._range') ----@alias Range {[1]: integer, [2]: integer, [3]: integer, [4]: integer} --- ---@alias TSCallbackName ---| 'changedtree' ---| 'bytes' @@ -24,11 +23,13 @@ local language = require('vim.treesitter.language') ---@field private _injection_query Query Queries defining injected languages ---@field private _opts table Options ---@field private _parser TSParser Parser for language ----@field private _regions Range[][] List of regions this tree should manage and parse +---@field private _regions Range6[][] List of regions this tree should manage and parse ---@field private _lang string Language name ---@field private _source (integer|string) Buffer or string to parse ---@field private _trees TSTree[] Reference to parsed tree (one for each language) ----@field private _valid boolean If the parsed tree is valid +---@field private _valid boolean|table If the parsed tree is valid +--- TODO(lewis6991): combine _regions, _valid and _trees +---@field private _is_child boolean local LanguageTree = {} ---@class LanguageTreeOpts @@ -114,6 +115,9 @@ end --- If the tree is invalid, call `parse()`. --- This will return the updated tree. function LanguageTree:is_valid() + if type(self._valid) == 'table' then + return #self._valid == #self._regions + end return self._valid end @@ -127,6 +131,16 @@ function LanguageTree:source() return self._source end +---@private +---This is only exposed so it can be wrapped for profiling +---@param old_tree TSTree +---@return TSTree, integer[] +function LanguageTree:_parse_tree(old_tree) + local tree, tree_changes = self._parser:parse(old_tree, self._source) + self:_do_callback('changedtree', tree_changes, tree) + return tree, tree_changes +end + --- Parses all defined regions using a treesitter parser --- for the language this tree represents. --- This will run the injection query for this language to @@ -135,35 +149,27 @@ end ---@return TSTree[] ---@return table|nil Change list function LanguageTree:parse() - if self._valid then + if self:is_valid() then return self._trees end - local parser = self._parser local changes = {} - local old_trees = self._trees - self._trees = {} - -- If there are no ranges, set to an empty list -- so the included ranges in the parser are cleared. - if self._regions and #self._regions > 0 then + if #self._regions > 0 then for i, ranges in ipairs(self._regions) do - local old_tree = old_trees[i] - parser:set_included_ranges(ranges) - - local tree, tree_changes = parser:parse(old_tree, self._source) - self:_do_callback('changedtree', tree_changes, tree) - - table.insert(self._trees, tree) - vim.list_extend(changes, tree_changes) + if not self._valid or not self._valid[i] then + self._parser:set_included_ranges(ranges) + local tree, tree_changes = self:_parse_tree(self._trees[i]) + self._trees[i] = tree + vim.list_extend(changes, tree_changes) + end end else - local tree, tree_changes = parser:parse(old_trees[1], self._source) - self:_do_callback('changedtree', tree_changes, tree) - - table.insert(self._trees, tree) - vim.list_extend(changes, tree_changes) + local tree, tree_changes = self:_parse_tree(self._trees[1]) + self._trees = { tree } + changes = tree_changes end local injections_by_lang = self:_get_injections() @@ -249,6 +255,7 @@ function LanguageTree:add_child(lang) end self._children[lang] = LanguageTree.new(self._source, lang, self._opts) + self._children[lang]._is_child = true self:invalidate() self:_do_callback('child_added', self._children[lang]) @@ -298,43 +305,35 @@ end --- This allows for embedded languages to be parsed together across different --- nodes, which is useful for templating languages like ERB and EJS. --- ---- Note: This call invalidates the tree and requires it to be parsed again. ---- ---@private ----@param regions integer[][][] List of regions this tree should manage and parse. +---@param regions Range4[][] List of regions this tree should manage and parse. function LanguageTree:set_included_regions(regions) -- Transform the tables from 4 element long to 6 element long (with byte offset) for _, region in ipairs(regions) do for i, range in ipairs(region) do if type(range) == 'table' and #range == 4 then - ---@diagnostic disable-next-line:no-unknown - local start_row, start_col, end_row, end_col = unpack(range) - local start_byte = 0 - local end_byte = 0 - local source = self._source - -- TODO(vigoux): proper byte computation here, and account for EOL ? - if type(source) == 'number' then - -- Easy case, this is a buffer parser - start_byte = a.nvim_buf_get_offset(source, start_row) + start_col - end_byte = a.nvim_buf_get_offset(source, end_row) + end_col - elseif type(self._source) == 'string' then - -- string parser, single `\n` delimited string - start_byte = vim.fn.byteidx(self._source, start_col) - end_byte = vim.fn.byteidx(self._source, end_col) - end + region[i] = Range.add_bytes(self._source, range) + end + end + end - region[i] = { start_row, start_col, start_byte, end_row, end_col, end_byte } + if #self._regions ~= #regions then + self._trees = {} + self:invalidate() + elseif self._valid ~= false then + if self._valid == true then + self._valid = {} + end + for i = 1, #regions do + self._valid[i] = true + if not vim.deep_equal(self._regions[i], regions[i]) then + self._valid[i] = nil + self._trees[i] = nil end end end self._regions = regions - -- Trees are no longer valid now that we have changed regions. - -- TODO(vigoux,steelsojka): Look into doing this smarter so we can use some of the - -- old trees for incremental parsing. Currently, this only - -- affects injected languages. - self._trees = {} - self:invalidate() end --- Gets the set of included regions @@ -346,10 +345,10 @@ end ---@param node TSNode ---@param id integer ---@param metadata TSMetadata ----@return Range +---@return Range4 local function get_range_from_metadata(node, id, metadata) if metadata[id] and metadata[id].range then - return metadata[id].range --[[@as Range]] + return metadata[id].range --[[@as Range4]] end return { node:range() } end @@ -378,7 +377,7 @@ function LanguageTree:_get_injections() self._injection_query:iter_matches(root_node, self._source, start_line, end_line + 1) do local lang = nil ---@type string - local ranges = {} ---@type Range[] + local ranges = {} ---@type Range4[] local combined = metadata.combined ---@type boolean -- Directives can configure how injections are captured as well as actual node captures. @@ -408,6 +407,7 @@ function LanguageTree:_get_injections() -- Lang should override any other language tag if name == 'language' and not lang then + ---@diagnostic disable-next-line lang = query.get_node_text(node, self._source, { metadata = metadata[id] }) elseif name == 'combined' then combined = true @@ -426,6 +426,8 @@ function LanguageTree:_get_injections() end end + assert(type(lang) == 'string') + -- Each tree index should be isolated from the other nodes. if not injections[tree_index] then injections[tree_index] = {} @@ -446,7 +448,7 @@ function LanguageTree:_get_injections() end end - ---@type table + ---@type table local result = {} -- Generate a map by lang of node lists. @@ -485,6 +487,45 @@ function LanguageTree:_do_callback(cb_name, ...) end end +---@private +---@param regions Range6[][] +---@param old_range Range6 +---@param new_range Range6 +---@return table region indices to invalidate +local function update_regions(regions, old_range, new_range) + ---@type table + local valid = {} + + for i, ranges in ipairs(regions or {}) do + valid[i] = true + for j, r in ipairs(ranges) do + if Range.intercepts(r, old_range) then + valid[i] = nil + break + end + + -- Range after change. Adjust + if Range.cmp_pos.gt(r[1], r[2], old_range[4], old_range[5]) then + local byte_offset = new_range[6] - old_range[6] + local row_offset = new_range[4] - old_range[4] + + -- Update the range to avoid invalidation in set_included_regions() + -- which will compare the regions against the parsed injection regions + ranges[j] = { + r[1] + row_offset, + r[2], + r[3] + byte_offset, + r[4] + row_offset, + r[5], + r[6] + byte_offset, + } + end + end + end + + return valid +end + ---@private ---@param bufnr integer ---@param changed_tick integer @@ -510,14 +551,53 @@ function LanguageTree:_on_bytes( new_col, new_byte ) - self:invalidate() - local old_end_col = old_col + ((old_row == 0) and start_col or 0) local new_end_col = new_col + ((new_row == 0) and start_col or 0) - -- Edit all trees recursively, together BEFORE emitting a bytes callback. - -- In most cases this callback should only be called from the root tree. - self:for_each_tree(function(tree) + local old_range = { + start_row, + start_col, + start_byte, + start_row + old_row, + old_end_col, + start_byte + old_byte, + } + + local new_range = { + start_row, + start_col, + start_byte, + start_row + new_row, + new_end_col, + start_byte + new_byte, + } + + local valid_regions = update_regions(self._regions, old_range, new_range) + + if #self._regions == 0 or #valid_regions == 0 then + self._valid = false + else + self._valid = valid_regions + end + + for _, child in pairs(self._children) do + child:_on_bytes( + bufnr, + changed_tick, + start_row, + start_col, + start_byte, + old_row, + old_col, + old_byte, + new_row, + new_col, + new_byte + ) + end + + -- Edit trees together BEFORE emitting a bytes callback. + for _, tree in ipairs(self._trees) do tree:edit( start_byte, start_byte + old_byte, @@ -529,22 +609,24 @@ function LanguageTree:_on_bytes( start_row + new_row, new_end_col ) - end) + end - self:_do_callback( - 'bytes', - bufnr, - changed_tick, - start_row, - start_col, - start_byte, - old_row, - old_col, - old_byte, - new_row, - new_col, - new_byte - ) + if not self._is_child then + self:_do_callback( + 'bytes', + bufnr, + changed_tick, + start_row, + start_col, + start_byte, + old_row, + old_col, + old_byte, + new_row, + new_col, + new_byte + ) + end end ---@private @@ -595,19 +677,15 @@ end ---@private ---@param tree TSTree ----@param range Range +---@param range Range4 ---@return boolean local function tree_contains(tree, range) - local start_row, start_col, end_row, end_col = tree:root():range() - local start_fits = start_row < range[1] or (start_row == range[1] and start_col <= range[2]) - local end_fits = end_row > range[3] or (end_row == range[3] and end_col >= range[4]) - - return start_fits and end_fits + return Range.contains({ tree:root():range() }, range) end --- Determines whether {range} is contained in the |LanguageTree|. --- ----@param range Range `{ start_line, start_col, end_line, end_col }` +---@param range Range4 `{ start_line, start_col, end_line, end_col }` ---@return boolean function LanguageTree:contains(range) for _, tree in pairs(self._trees) do @@ -621,7 +699,7 @@ end --- Gets the tree that contains {range}. --- ----@param range Range `{ start_line, start_col, end_line, end_col }` +---@param range Range4 `{ start_line, start_col, end_line, end_col }` ---@param opts table|nil Optional keyword arguments: --- - ignore_injections boolean Ignore injected languages (default true) ---@return TSTree|nil @@ -631,10 +709,9 @@ function LanguageTree:tree_for_range(range, opts) if not ignore then for _, child in pairs(self._children) do - for _, tree in pairs(child:trees()) do - if tree_contains(tree, range) then - return tree - end + local tree = child:tree_for_range(range, opts) + if tree then + return tree end end end @@ -650,7 +727,7 @@ end --- Gets the smallest named node that contains {range}. --- ----@param range Range `{ start_line, start_col, end_line, end_col }` +---@param range Range4 `{ start_line, start_col, end_line, end_col }` ---@param opts table|nil Optional keyword arguments: --- - ignore_injections boolean Ignore injected languages (default true) ---@return TSNode | nil Found node @@ -663,7 +740,7 @@ end --- Gets the appropriate language that contains {range}. --- ----@param range Range `{ start_line, start_col, end_line, end_col }` +---@param range Range4 `{ start_line, start_col, end_line, end_col }` ---@return LanguageTree Managing {range} function LanguageTree:language_for_range(range) for _, child in pairs(self._children) do diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index 58a29f2fe0..13d98a0625 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -406,7 +406,7 @@ predicate_handlers['vim-match?'] = predicate_handlers['match?'] ---@class TSMetadata ---@field [integer] TSMetadata ---@field [string] integer|string ----@field range Range +---@field range Range4 ---@alias TSDirective fun(match: TSMatch, _, _, predicate: any[], metadata: TSMetadata) -- cgit From 1df3f5ec6aca24cbe7b78ead5c37ad06a65c84e8 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Thu, 23 Feb 2023 17:05:20 +0000 Subject: feat(treesitter): upstream foldexpr from nvim-treesitter --- runtime/lua/vim/treesitter/_fold.lua | 173 +++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 runtime/lua/vim/treesitter/_fold.lua (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/_fold.lua b/runtime/lua/vim/treesitter/_fold.lua new file mode 100644 index 0000000000..a66cc6d543 --- /dev/null +++ b/runtime/lua/vim/treesitter/_fold.lua @@ -0,0 +1,173 @@ +local api = vim.api + +local M = {} + +--- Memoizes a function based on the buffer tick of the provided bufnr. +--- The cache entry is cleared when the buffer is detached to avoid memory leaks. +---@generic F: function +---@param fn F fn to memoize, taking the bufnr as first argument +---@return F +local function memoize_by_changedtick(fn) + ---@type table + local cache = {} + + ---@param bufnr integer + return function(bufnr, ...) + local tick = api.nvim_buf_get_changedtick(bufnr) + + if cache[bufnr] then + if cache[bufnr].last_tick == tick then + return cache[bufnr].result + end + else + local function detach_handler() + cache[bufnr] = nil + end + + -- Clean up logic only! + api.nvim_buf_attach(bufnr, false, { + on_detach = detach_handler, + on_reload = detach_handler, + }) + end + + cache[bufnr] = { + result = fn(bufnr, ...), + last_tick = tick, + } + + return cache[bufnr].result + end +end + +---@param bufnr integer +---@param capture string +---@param query_name string +---@param callback fun(id: integer, node:TSNode, metadata: TSMetadata) +local function iter_matches_with_capture(bufnr, capture, query_name, callback) + local parser = vim.treesitter.get_parser(bufnr) + + if not parser then + return + end + + parser:for_each_tree(function(tree, lang_tree) + local lang = lang_tree:lang() + local query = vim.treesitter.query.get_query(lang, query_name) + if query then + local root = tree:root() + local start, _, stop = root:range() + for _, match, metadata in query:iter_matches(root, bufnr, start, stop) do + for id, node in pairs(match) do + if query.captures[id] == capture then + callback(id, node, metadata) + end + end + end + end + end) +end + +---@private +--- TODO(lewis6991): copied from languagetree.lua. Consolidate +---@param node TSNode +---@param id integer +---@param metadata TSMetadata +---@return Range +local function get_range_from_metadata(node, id, metadata) + if metadata[id] and metadata[id].range then + return metadata[id].range --[[@as Range]] + end + return { node:range() } +end + +-- This is cached on buf tick to avoid computing that multiple times +-- Especially not for every line in the file when `zx` is hit +---@param bufnr integer +---@return table +local folds_levels = memoize_by_changedtick(function(bufnr) + local max_fold_level = vim.wo.foldnestmax + local function trim_level(level) + if level > max_fold_level then + return max_fold_level + end + return level + end + + -- start..stop is an inclusive range + local start_counts = {} ---@type table + local stop_counts = {} ---@type table + + local prev_start = -1 + local prev_stop = -1 + + local min_fold_lines = vim.wo.foldminlines + + iter_matches_with_capture(bufnr, 'fold', 'folds', function(id, node, metadata) + local range = get_range_from_metadata(node, id, metadata) + local start, stop, stop_col = range[1], range[3], range[4] + + 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 > min_fold_lines and not (start == prev_start and stop == prev_stop) then + start_counts[start] = (start_counts[start] or 0) + 1 + stop_counts[stop] = (stop_counts[stop] or 0) + 1 + prev_start = start + prev_stop = stop + end + end) + + ---@type table + local levels = {} + local current_level = 0 + + -- We now have the list of fold opening and closing, fill the gaps and mark where fold start + for lnum = 0, api.nvim_buf_line_count(bufnr) do + local last_trimmed_level = trim_level(current_level) + current_level = current_level + (start_counts[lnum] or 0) + local trimmed_level = trim_level(current_level) + current_level = current_level - (stop_counts[lnum] or 0) + + -- 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 ) + -- If it did have such a mechanism, (trimmed_level - last_trimmed_level) + -- would be the correct number of starts to pass on. + local prefix = '' + if trimmed_level - last_trimmed_level > 0 then + prefix = '>' + end + + levels[lnum + 1] = prefix .. tostring(trimmed_level) + end + + return levels +end) + +---@param lnum integer|nil +---@return string +function M.foldexpr(lnum) + lnum = lnum or vim.v.lnum + local bufnr = api.nvim_get_current_buf() + + ---@diagnostic disable-next-line:invisible + if not vim.treesitter._has_parser(bufnr) or not lnum then + return '0' + end + + local levels = folds_levels(bufnr) or {} + + return levels[lnum] or '0' +end + +return M -- cgit From 3f35ebb14dd8b1ceeef45c4f42949f14e3b54b88 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Thu, 23 Feb 2023 18:09:44 +0000 Subject: fix(treesitter): fixup language invalidation (#22381) --- runtime/lua/vim/treesitter/languagetree.lua | 38 +++++++++++++++++++---------- 1 file changed, 25 insertions(+), 13 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index 2d4e2e595b..122496cff3 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -27,7 +27,7 @@ local Range = require('vim.treesitter._range') ---@field private _lang string Language name ---@field private _source (integer|string) Buffer or string to parse ---@field private _trees TSTree[] Reference to parsed tree (one for each language) ----@field private _valid boolean|table If the parsed tree is valid +---@field private _valid boolean|table If the parsed tree is valid --- TODO(lewis6991): combine _regions, _valid and _trees ---@field private _is_child boolean local LanguageTree = {} @@ -115,10 +115,18 @@ end --- If the tree is invalid, call `parse()`. --- This will return the updated tree. function LanguageTree:is_valid() - if type(self._valid) == 'table' then - return #self._valid == #self._regions + local valid = self._valid + + if type(valid) == 'table' then + for _, v in ipairs(valid) do + if not v then + return false + end + end + return true end - return self._valid + + return valid end --- Returns a map of language to child tree. @@ -323,11 +331,17 @@ function LanguageTree:set_included_regions(regions) elseif self._valid ~= false then if self._valid == true then self._valid = {} + for i = 1, #regions do + self._valid[i] = true + end end + for i = 1, #regions do - self._valid[i] = true if not vim.deep_equal(self._regions[i], regions[i]) then - self._valid[i] = nil + self._valid[i] = false + end + + if not self._valid[i] then self._trees[i] = nil end end @@ -491,16 +505,16 @@ end ---@param regions Range6[][] ---@param old_range Range6 ---@param new_range Range6 ----@return table region indices to invalidate +---@return table region indices to invalidate local function update_regions(regions, old_range, new_range) - ---@type table + ---@type table local valid = {} for i, ranges in ipairs(regions or {}) do valid[i] = true for j, r in ipairs(ranges) do if Range.intercepts(r, old_range) then - valid[i] = nil + valid[i] = false break end @@ -572,12 +586,10 @@ function LanguageTree:_on_bytes( start_byte + new_byte, } - local valid_regions = update_regions(self._regions, old_range, new_range) - - if #self._regions == 0 or #valid_regions == 0 then + if #self._regions == 0 then self._valid = false else - self._valid = valid_regions + self._valid = update_regions(self._regions, old_range, new_range) end for _, child in pairs(self._children) do -- cgit From c57af5d41cd039194dbd9c6fb5b68b377d2a5b59 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Fri, 24 Feb 2023 09:50:59 +0000 Subject: feat(treesitter)!: remove silent option from language.add() Simply use `pcall` if you want to silence an error. --- runtime/lua/vim/treesitter/language.lua | 32 ++++++++++------------------- runtime/lua/vim/treesitter/languagetree.lua | 2 +- 2 files changed, 12 insertions(+), 22 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/language.lua b/runtime/lua/vim/treesitter/language.lua index 8637d7d544..0796383bf5 100644 --- a/runtime/lua/vim/treesitter/language.lua +++ b/runtime/lua/vim/treesitter/language.lua @@ -13,11 +13,19 @@ end ---@deprecated function M.require_language(lang, path, silent, symbol_name) - return M.add(lang, { + local opts = { silent = silent, path = path, symbol_name = symbol_name, - }) + } + + if silent then + local installed = pcall(M.add, lang, opts) + return installed + end + + M.add(lang, opts) + return true end ---@class treesitter.RequireLangOpts @@ -38,20 +46,16 @@ end --- Defaults to lang. --- - path (string|nil) Optional path the parser is located at --- - symbol_name (string|nil) Internal symbol name for the language to load ---- - silent (boolean|nil) Don't throw an error if language not found ----@return boolean If the specified language is installed function M.add(lang, opts) ---@cast opts treesitter.RequireLangOpts opts = opts or {} local path = opts.path - local silent = opts.silent local filetype = opts.filetype or lang local symbol_name = opts.symbol_name vim.validate({ lang = { lang, 'string' }, path = { path, 'string', true }, - silent = { silent, 'boolean', true }, symbol_name = { symbol_name, 'string', true }, filetype = { filetype, { 'string', 'table' }, true }, }) @@ -64,32 +68,18 @@ function M.add(lang, opts) if path == nil then if not (lang and lang:match('[%w_]+') == lang) then - if silent then - return false - end error("'" .. lang .. "' is not a valid language name") end local fname = 'parser/' .. lang .. '.*' local paths = a.nvim_get_runtime_file(fname, false) if #paths == 0 then - if silent then - return false - end error("no parser for '" .. lang .. "' language, see :help treesitter-parsers") end path = paths[1] end - if silent then - if not pcall(vim._ts_add_language, path, lang, symbol_name) then - return false - end - else - vim._ts_add_language(path, lang, symbol_name) - end - - return true + vim._ts_add_language(path, lang, symbol_name) end --- Register a lang to be used for a filetype (or list of filetypes). diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index 122496cff3..7ec7bbfa12 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -184,7 +184,7 @@ function LanguageTree:parse() local seen_langs = {} ---@type table for lang, injection_ranges in pairs(injections_by_lang) do - local has_lang = language.add(lang, { silent = true }) + local has_lang = pcall(language.add, lang) -- Child language trees should just be ignored if not found, since -- they can depend on the text of a node. Intermediate strings -- cgit From 774e59f3f9bf50c8350857c6722bb58df2dd940a Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Sun, 26 Feb 2023 16:53:33 +0000 Subject: feat(treesitter): expand the API --- runtime/lua/vim/treesitter/_meta.lua | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/_meta.lua b/runtime/lua/vim/treesitter/_meta.lua index 87b4560798..731a5ebf9f 100644 --- a/runtime/lua/vim/treesitter/_meta.lua +++ b/runtime/lua/vim/treesitter/_meta.lua @@ -2,6 +2,7 @@ ---@class TSNode ---@field id fun(self: TSNode): integer +---@field tree fun(self: TSNode): TSTree ---@field range fun(self: TSNode): integer, integer, integer, integer ---@field start fun(self: TSNode): integer, integer, integer ---@field end_ fun(self: TSNode): integer, integer, integer @@ -9,6 +10,7 @@ ---@field symbol fun(self: TSNode): integer ---@field named fun(self: TSNode): boolean ---@field missing fun(self: TSNode): boolean +---@field extra fun(self: TSNode): boolean ---@field child_count fun(self: TSNode): integer ---@field named_child_count fun(self: TSNode): integer ---@field child fun(self: TSNode, integer): TSNode @@ -21,7 +23,8 @@ ---@field next_named_sibling fun(self: TSNode): TSNode ---@field prev_named_sibling fun(self: TSNode): TSNode ---@field named_children fun(self: TSNode): TSNode[] ----@field has_error fun(self: TSNode): boolean +---@field has_changes fun(self: TSNode): boolean +---@field equal fun(self: TSNode, other: TSNode): boolean ---@field iter_children fun(self: TSNode): fun(): TSNode, string local TSNode = {} @@ -41,8 +44,11 @@ function TSNode:_rawquery(query, captures, start, end_) end ---@class TSParser ---@field parse fun(self: TSParser, tree, source: integer|string): TSTree, integer[] +---@field reset fun(self: TSParser) ---@field included_ranges fun(self: TSParser): integer[] ---@field set_included_ranges fun(self: TSParser, ranges: integer[][]) +---@field set_timeout fun(self: TSParser, timeout: integer) +---@field timeout fun(self: TSParser): integer ---@class TSTree ---@field root fun(self: TSTree): TSNode -- cgit From da56f06037c26180021c1e3b73a77fc990f0c1e3 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Mon, 27 Feb 2023 10:49:19 +0000 Subject: fix(treesitter): remove virtual text from playground Implement the range and lang annotations as comments instead --- runtime/lua/vim/treesitter/playground.lua | 46 +++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 14 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/playground.lua b/runtime/lua/vim/treesitter/playground.lua index 001bc2d5bf..fd5b687195 100644 --- a/runtime/lua/vim/treesitter/playground.lua +++ b/runtime/lua/vim/treesitter/playground.lua @@ -138,6 +138,19 @@ end local decor_ns = api.nvim_create_namespace('ts.playground') +---@private +---@param lnum integer +---@param col integer +---@param end_lnum integer +---@param end_col integer +---@return string +local function get_range_str(lnum, col, end_col, end_lnum) + if lnum == end_lnum then + return string.format('[%d:%d-%d]', lnum + 1, col + 1, end_col) + end + return string.format('[%d:%d-%d:%d]', lnum + 1, col + 1, end_lnum + 1, end_col) +end + --- Write the contents of this Playground into {bufnr}. --- ---@param bufnr number Buffer number to write into. @@ -145,26 +158,31 @@ local decor_ns = api.nvim_create_namespace('ts.playground') function TSPlayground:draw(bufnr) vim.bo[bufnr].modifiable = true local lines = {} ---@type string[] + local lang_hl_marks = {} ---@type table[] + for _, item in self:iter() do - lines[#lines + 1] = string.rep(' ', item.depth) .. item.text + local range_str = get_range_str(item.lnum, item.col, item.end_lnum, item.end_col) + local lang_str = self.opts.lang and string.format(' %s', item.lang) or '' + local line = string.rep(' ', item.depth) .. item.text .. '; ' .. range_str .. lang_str + + if self.opts.lang then + lang_hl_marks[#lang_hl_marks + 1] = { + col = #line - #lang_str, + end_col = #line, + } + end + + lines[#lines + 1] = line end + api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) api.nvim_buf_clear_namespace(bufnr, decor_ns, 0, -1) - for i, item in self:iter() do - local range_str - if item.lnum == item.end_lnum then - range_str = string.format('[%d:%d-%d]', item.lnum + 1, item.col + 1, item.end_col) - else - range_str = - string.format('[%d:%d-%d:%d]', item.lnum + 1, item.col + 1, item.end_lnum + 1, item.end_col) - end - - local lang_str = self.opts.lang and string.format(' %s', item.lang) or '' - - api.nvim_buf_set_extmark(bufnr, decor_ns, i - 1, 0, { - virt_text = { { range_str, 'Comment' }, { lang_str, 'Title' } }, + for i, m in ipairs(lang_hl_marks) do + api.nvim_buf_set_extmark(bufnr, decor_ns, i - 1, m.col, { + hl_group = 'Title', + end_col = m.end_col, }) end -- cgit From 5aa37e20e0e6cbabf82c5cf1d35c6f5e6e48f099 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Mon, 27 Feb 2023 15:01:09 +0000 Subject: fix(treesitter): ipairs -> pairs Fixes: https://github.com/nvim-treesitter/nvim-treesitter/issues/4349 --- 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 7ec7bbfa12..43fb866896 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -467,7 +467,7 @@ function LanguageTree:_get_injections() -- Generate a map by lang of node lists. -- Each list is a set of ranges that should be parsed together. - for _, lang_map in ipairs(injections) do + for _, lang_map in pairs(injections) do for lang, patterns in pairs(lang_map) do if not result[lang] then result[lang] = {} -- cgit From f64098a2df774c79dd454f63ac491570cdcaf2b2 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Mon, 27 Feb 2023 15:33:18 +0000 Subject: fix(treesitter): fixup for health --- runtime/lua/vim/treesitter/health.lua | 22 ++++++++++------------ runtime/lua/vim/treesitter/language.lua | 2 +- 2 files changed, 11 insertions(+), 13 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/health.lua b/runtime/lua/vim/treesitter/health.lua index 1abcdd0b31..fd1188fde4 100644 --- a/runtime/lua/vim/treesitter/health.lua +++ b/runtime/lua/vim/treesitter/health.lua @@ -2,28 +2,26 @@ local M = {} local ts = vim.treesitter local health = require('vim.health') ---- Lists the parsers currently installed ---- ----@return string[] list of parser files -function M.list_parsers() - return vim.api.nvim_get_runtime_file('parser/*', true) -end - --- Performs a healthcheck for treesitter integration function M.check() - local parsers = M.list_parsers() + local parsers = vim.api.nvim_get_runtime_file('parser/*', true) health.report_info(string.format('Nvim runtime ABI version: %d', ts.language_version)) for _, parser in pairs(parsers) do local parsername = vim.fn.fnamemodify(parser, ':t:r') - local is_loadable, ret = pcall(ts.language.add, parsername) + local is_loadable, err_or_nil = pcall(ts.language.add, parsername) - if not is_loadable or not ret then + if not is_loadable then health.report_error( - string.format('Parser "%s" failed to load (path: %s): %s', parsername, parser, ret or '?') + string.format( + 'Parser "%s" failed to load (path: %s): %s', + parsername, + parser, + err_or_nil or '?' + ) ) - elseif ret then + else local lang = ts.language.inspect_language(parsername) health.report_ok( string.format('Parser: %-10s ABI: %d, path: %s', parsername, lang._abi_version, parser) diff --git a/runtime/lua/vim/treesitter/language.lua b/runtime/lua/vim/treesitter/language.lua index 0796383bf5..5bcc786e88 100644 --- a/runtime/lua/vim/treesitter/language.lua +++ b/runtime/lua/vim/treesitter/language.lua @@ -63,7 +63,7 @@ function M.add(lang, opts) M.register(lang, filetype or lang) if vim._ts_has_language(lang) then - return true + return end if path == nil then -- cgit From 86ff239240e955ef6da95bc9c8814cfd4492f5aa Mon Sep 17 00:00:00 2001 From: Gregory Anders Date: Thu, 2 Mar 2023 14:15:18 -0700 Subject: refactor(treesitter): use string.format to create lines --- runtime/lua/vim/treesitter/playground.lua | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/playground.lua b/runtime/lua/vim/treesitter/playground.lua index fd5b687195..992433961f 100644 --- a/runtime/lua/vim/treesitter/playground.lua +++ b/runtime/lua/vim/treesitter/playground.lua @@ -146,9 +146,9 @@ local decor_ns = api.nvim_create_namespace('ts.playground') ---@return string local function get_range_str(lnum, col, end_col, end_lnum) if lnum == end_lnum then - return string.format('[%d:%d-%d]', lnum + 1, col + 1, end_col) + return string.format('[%d:%d - %d]', lnum + 1, col + 1, end_col) end - return string.format('[%d:%d-%d:%d]', lnum + 1, col + 1, end_lnum + 1, end_col) + return string.format('[%d:%d - %d:%d]', lnum + 1, col + 1, end_lnum + 1, end_col) end --- Write the contents of this Playground into {bufnr}. @@ -163,7 +163,8 @@ function TSPlayground:draw(bufnr) for _, item in self:iter() do local range_str = get_range_str(item.lnum, item.col, item.end_lnum, item.end_col) local lang_str = self.opts.lang and string.format(' %s', item.lang) or '' - local line = string.rep(' ', item.depth) .. item.text .. '; ' .. range_str .. lang_str + local line = + string.format('%s%s ; %s%s', string.rep(' ', item.depth), item.text, range_str, lang_str) if self.opts.lang then lang_hl_marks[#lang_hl_marks + 1] = { -- cgit From 6d4f48182131c36d57589eefd4cefe3c70256d04 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Fri, 3 Mar 2023 09:44:02 +0000 Subject: fix(treesitter): disallow empty filetypes Fixes #22473 --- runtime/lua/vim/treesitter/language.lua | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/language.lua b/runtime/lua/vim/treesitter/language.lua index 5bcc786e88..5f34d9cd56 100644 --- a/runtime/lua/vim/treesitter/language.lua +++ b/runtime/lua/vim/treesitter/language.lua @@ -60,6 +60,16 @@ function M.add(lang, opts) filetype = { filetype, { 'string', 'table' }, true }, }) + if filetype == '' then + error(string.format("'%s' is not a valid filetype", filetype)) + elseif type(filetype) == 'table' then + for _, f in ipairs(filetype) do + if f == '' then + error(string.format("'%s' is not a valid filetype", filetype)) + end + end + end + M.register(lang, filetype or lang) if vim._ts_has_language(lang) then -- cgit From f0a2ffab2923202f4454860ba1a7c7bd0e035ed2 Mon Sep 17 00:00:00 2001 From: Jaehwang Jung Date: Fri, 3 Mar 2023 20:05:59 +0900 Subject: fix(treesitter): typos in _range.lua fix(treesitter): typos _range.lua --- runtime/lua/vim/treesitter/_range.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/_range.lua b/runtime/lua/vim/treesitter/_range.lua index b87542c20f..8decd3a1fd 100644 --- a/runtime/lua/vim/treesitter/_range.lua +++ b/runtime/lua/vim/treesitter/_range.lua @@ -61,7 +61,7 @@ function M.intercepts(r1, r2) local off_1 = #r1 == 6 and 1 or 0 local off_2 = #r1 == 6 and 1 or 0 - local srow_1, scol_1, erow_1, ecol_1 = r1[1], r2[2], r1[3 + off_1], r1[4 + off_1] + local srow_1, scol_1, erow_1, ecol_1 = r1[1], r1[2], r1[3 + off_1], r1[4 + off_1] local srow_2, scol_2, erow_2, ecol_2 = r2[1], r2[2], r2[3 + off_2], r2[4 + off_2] -- r1 is above r2 @@ -85,7 +85,7 @@ function M.contains(r1, r2) local off_1 = #r1 == 6 and 1 or 0 local off_2 = #r1 == 6 and 1 or 0 - local srow_1, scol_1, erow_1, ecol_1 = r1[1], r2[2], r1[3 + off_1], r1[4 + off_1] + local srow_1, scol_1, erow_1, ecol_1 = r1[1], r1[2], r1[3 + off_1], r1[4 + off_1] local srow_2, scol_2, erow_2, ecol_2 = r2[1], r2[2], r2[3 + off_2], r2[4 + off_2] -- start doesn't fit -- cgit From 8414cfe7f4d8888698343cb54a3f373a28b365db Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Thu, 2 Mar 2023 20:46:59 +0100 Subject: docs: fix vim.treesitter tags Problem: Help tags like vim.treesitter.language.add() are confusing because `vim.treesitter.language` is (thankfully) not a user-facing module. Solution: Ignore the "fstem" when generating "treesitter" tags. --- runtime/lua/vim/treesitter/highlighter.lua | 4 +++- runtime/lua/vim/treesitter/languagetree.lua | 20 +++++++++----------- runtime/lua/vim/treesitter/query.lua | 5 ++--- 3 files changed, 14 insertions(+), 15 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/highlighter.lua b/runtime/lua/vim/treesitter/highlighter.lua index 8adaa4ef2f..e3deaf6ba6 100644 --- a/runtime/lua/vim/treesitter/highlighter.lua +++ b/runtime/lua/vim/treesitter/highlighter.lua @@ -58,7 +58,9 @@ function TSHighlighterQuery:query() return self._query end ---- Creates a new highlighter using @param tree +---@private +--- +--- Creates a highlighter for `tree`. --- ---@param tree LanguageTree parser object to use for highlighting ---@param opts (table|nil) Configuration of the highlighter: diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index 43fb866896..1bc7971eba 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -38,18 +38,16 @@ local LanguageTree = {} LanguageTree.__index = LanguageTree ---- A |LanguageTree| holds the treesitter parser for a given language {lang} used ---- to parse a buffer. As the buffer may contain injected languages, the LanguageTree ---- needs to store parsers for these child languages as well (which in turn may contain ---- child languages themselves, hence the name). +--- @private --- ----@param source (integer|string) Buffer or a string of text to parse ----@param lang string Root language this tree represents ----@param opts (table|nil) Optional keyword arguments: ---- - injections table Mapping language to injection query strings. ---- This is useful for overriding the built-in ---- runtime file searching for the injection language ---- query per language. +--- |LanguageTree| contains a tree of parsers: the root treesitter parser for {lang} and any +--- "injected" language parsers, which themselves may inject other languages, recursively. +--- +---@param source (integer|string) Buffer or text string to parse +---@param lang string Root language of this tree +---@param opts (table|nil) Optional arguments: +--- - injections table Map of language to injection query strings. Overrides the +--- built-in runtime file searching for language injections. ---@return LanguageTree parser object function LanguageTree.new(source, lang, opts) language.add(lang) diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index 13d98a0625..4e9871b59d 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -273,8 +273,7 @@ end ---@param opts (table|nil) Optional parameters. --- - concat: (boolean) Concatenate result in a string (default true) --- - metadata (table) Metadata of a specific capture. This would be ---- set to `metadata[capture_id]` when using ---- |vim.treesitter.query.add_directive()|. +--- set to `metadata[capture_id]` when using |vim.treesitter.add_directive()|. ---@return (string[]|string|nil) function M.get_node_text(node, source, opts) opts = opts or {} @@ -486,7 +485,7 @@ local directive_handlers = { --- ---@param name string Name of the predicate, without leading # ---@param handler function(match:table, pattern:string, bufnr:number, predicate:string[]) ---- - see |vim.treesitter.query.add_directive()| for argument meanings +--- - see |vim.treesitter.add_directive()| for argument meanings ---@param force boolean|nil function M.add_predicate(name, handler, force) if predicate_handlers[name] and not force then -- cgit From 128b82103ba477482bdfaf10ec71e0986876cca1 Mon Sep 17 00:00:00 2001 From: Jaehwang Jung Date: Sat, 4 Mar 2023 22:04:05 +0900 Subject: docs(treesitter): number → integer (#22513) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- runtime/lua/vim/treesitter/playground.lua | 22 +++++++++++----------- runtime/lua/vim/treesitter/query.lua | 12 ++++++------ 2 files changed, 17 insertions(+), 17 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/playground.lua b/runtime/lua/vim/treesitter/playground.lua index 992433961f..7f181c23fd 100644 --- a/runtime/lua/vim/treesitter/playground.lua +++ b/runtime/lua/vim/treesitter/playground.lua @@ -1,7 +1,7 @@ local api = vim.api ---@class TSPlayground ----@field ns number API namespace +---@field ns integer API namespace ---@field opts table Options table with the following keys: --- - anon (boolean): If true, display anonymous nodes --- - lang (boolean): If true, display the language alongside each node @@ -10,14 +10,14 @@ local api = vim.api local TSPlayground = {} --- ---@class Node ----@field id number Node id +---@field id integer Node id ---@field text string Node text ---@field named boolean True if this is a named (non-anonymous) node ----@field depth number Depth of the node within the tree ----@field lnum number Beginning line number of this node in the source buffer ----@field col number Beginning column number of this node in the source buffer ----@field end_lnum number Final line number of this node in the source buffer ----@field end_col number Final column number of this node in the source buffer +---@field depth integer Depth of the node within the tree +---@field lnum integer Beginning line number of this node in the source buffer +---@field col integer Beginning column number of this node in the source buffer +---@field end_lnum integer Final line number of this node in the source buffer +---@field end_col integer Final column number of this node in the source buffer ---@field lang string Source language of this node ---@field root TSNode @@ -34,7 +34,7 @@ local TSPlayground = {} --- table maps nodes in the primary tree to root nodes of injected trees. --- ---@param node TSNode Starting node to begin traversal |tsnode| ----@param depth number Current recursion depth +---@param depth integer Current recursion depth ---@param lang string Language of the tree currently being traversed ---@param injections table Mapping of node ids to root nodes of injected language trees (see --- explanation above) @@ -153,7 +153,7 @@ end --- Write the contents of this Playground into {bufnr}. --- ----@param bufnr number Buffer number to write into. +---@param bufnr integer Buffer number to write into. ---@private function TSPlayground:draw(bufnr) vim.bo[bufnr].modifiable = true @@ -194,7 +194,7 @@ end --- --- The node number is dependent on whether or not anonymous nodes are displayed. --- ----@param i number Node number to get +---@param i integer Node number to get ---@return Node ---@private function TSPlayground:get(i) @@ -206,7 +206,7 @@ end --- ---@return (fun(): integer, Node) Iterator over all nodes in this Playground ---@return table ----@return number +---@return integer ---@private function TSPlayground:iter() return ipairs(self.opts.anon and self.nodes or self.named) diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index 4e9871b59d..22f706585e 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -56,7 +56,7 @@ local function add_included_lang(base_langs, lang, ilang) end ---@private ----@param buf (number) +---@param buf (integer) ---@param range (table) ---@param concat (boolean) ---@returns (string[]|string|nil) @@ -269,7 +269,7 @@ end --- Gets the text corresponding to a given node --- ---@param node TSNode ----@param source (number|string) Buffer or string from which the {node} is extracted +---@param source (integer|string) Buffer or string from which the {node} is extracted ---@param opts (table|nil) Optional parameters. --- - concat: (boolean) Concatenate result in a string (default true) --- - metadata (table) Metadata of a specific capture. This would be @@ -484,7 +484,7 @@ local directive_handlers = { --- Adds a new predicate to be used in queries --- ---@param name string Name of the predicate, without leading # ----@param handler function(match:table, pattern:string, bufnr:number, predicate:string[]) +---@param handler function(match:table, pattern:string, bufnr:integer, predicate:string[]) --- - see |vim.treesitter.add_directive()| for argument meanings ---@param force boolean|nil function M.add_predicate(name, handler, force) @@ -503,7 +503,7 @@ end --- metadata table `metadata[capture_id].key = value` --- ---@param name string Name of the directive, without leading # ----@param handler function(match:table, pattern:string, bufnr:number, predicate:string[], metadata:table) +---@param handler function(match:table, pattern:string, bufnr:integer, predicate:string[], metadata:table) --- - match: see |treesitter-query| --- - node-level data are accessible via `match[capture_id]` --- - pattern: see |treesitter-query| @@ -644,8 +644,8 @@ end --- ---@param node TSNode under which the search will occur ---@param source (integer|string) Source buffer or string to extract text from ----@param start number Starting line for the search ----@param stop number Stopping line for the search (end-exclusive) +---@param start integer Starting line for the search +---@param stop integer Stopping line for the search (end-exclusive) --- ---@return (fun(): integer, TSNode, TSMetadata): capture id, capture node, metadata function Query:iter_captures(node, source, start, stop) -- cgit From 533d671271eb76373d9940161e7bfd201b7e7c2b Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Sun, 5 Mar 2023 18:15:29 -0500 Subject: docs: module-level docstrings (@defgroup) #22498 Problem: gen_vimdoc.py / lua2dox.lua does not support @defgroup or \defgroup except for "api-foo" modules. Solution: Modify `gen_vimdoc.py` to look for section names based on `helptag_fmt`. TODO: - Support @module ? https://github.com/LuaLS/lua-language-server/wiki/Annotations#module --- runtime/lua/vim/treesitter/languagetree.lua | 34 +++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index 1bc7971eba..b8b0dd867e 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -1,3 +1,37 @@ +--- @defgroup lua-treesitter-languagetree +--- +--- @brief A \*LanguageTree\* contains a tree of parsers: the root treesitter parser for {lang} and +--- any "injected" language parsers, which themselves may inject other languages, recursively. +--- For example a Lua buffer containing some Vimscript commands needs multiple parsers to fully +--- understand its contents. +--- +--- To create a LanguageTree (parser object) for a given buffer and language, use: +--- +---
lua
+---     local parser = vim.treesitter.get_parser(bufnr, lang)
+--- 
+--- +--- (where `bufnr=0` means current buffer). `lang` defaults to 'filetype'. +--- Note: currently the parser is retained for the lifetime of a buffer but this may change; +--- a plugin should keep a reference to the parser object if it wants incremental updates. +--- +--- Whenever you need to access the current syntax tree, parse the buffer: +--- +---
lua
+---     local tree = parser:parse()
+--- 
+--- +--- This returns a table of immutable |treesitter-tree| objects representing the current state of +--- the buffer. When the plugin wants to access the state after a (possible) edit it must call +--- `parse()` again. If the buffer wasn't edited, the same tree will be returned again without extra +--- work. If the buffer was parsed before, incremental parsing will be done of the changed parts. +--- +--- Note: To use the parser directly inside a |nvim_buf_attach()| Lua callback, you must call +--- |vim.treesitter.get_parser()| before you register your callback. But preferably parsing +--- shouldn't be done directly in the change callback anyway as they will be very frequent. Rather +--- a plugin that does any kind of analysis on a tree should use a timer to throttle too frequent +--- updates. + local a = vim.api local query = require('vim.treesitter.query') local language = require('vim.treesitter.language') -- cgit From f9a46391ab5961fe6c6b7d1efdc96befdd495c11 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Mon, 6 Mar 2023 10:57:14 +0000 Subject: refactor(treesitter): simplify some range functions --- runtime/lua/vim/treesitter/_range.lua | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/_range.lua b/runtime/lua/vim/treesitter/_range.lua index 8decd3a1fd..bec24a23a5 100644 --- a/runtime/lua/vim/treesitter/_range.lua +++ b/runtime/lua/vim/treesitter/_range.lua @@ -53,6 +53,26 @@ M.cmp_pos = { setmetatable(M.cmp_pos, { __call = cmp_pos }) +---@private +---Check if a variable is a valid range object +---@param r any +---@return boolean +function M.validate(r) + if type(r) ~= 'table' or #r ~= 6 and #r ~= 4 then + return false + end + + for _, e in + ipairs(r --[[@as any[] ]]) + do + if type(e) ~= 'number' then + return false + end + end + + return true +end + ---@private ---@param r1 Range4|Range6 ---@param r2 Range4|Range6 -- cgit From ddd257f75301a50c177fc24a693d39a45b47a689 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Wed, 8 Mar 2023 11:03:11 +0000 Subject: feat(treesitter): use upstream format for injection queries --- runtime/lua/vim/treesitter/_meta.lua | 8 +- runtime/lua/vim/treesitter/languagetree.lua | 240 ++++++++++++++++++++-------- runtime/lua/vim/treesitter/query.lua | 21 +-- 3 files changed, 181 insertions(+), 88 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/_meta.lua b/runtime/lua/vim/treesitter/_meta.lua index 731a5ebf9f..ad0854706b 100644 --- a/runtime/lua/vim/treesitter/_meta.lua +++ b/runtime/lua/vim/treesitter/_meta.lua @@ -14,7 +14,7 @@ ---@field child_count fun(self: TSNode): integer ---@field named_child_count fun(self: TSNode): integer ---@field child fun(self: TSNode, integer): TSNode ----@field name_child fun(self: TSNode, integer): TSNode +---@field named_child fun(self: TSNode, integer): TSNode ---@field descendant_for_range fun(self: TSNode, integer, integer, integer, integer): TSNode ---@field named_descendant_for_range fun(self: TSNode, integer, integer, integer, integer): TSNode ---@field parent fun(self: TSNode): TSNode @@ -43,10 +43,10 @@ function TSNode:_rawquery(query, captures, start, end_) end function TSNode:_rawquery(query, captures, start, end_) end ---@class TSParser ----@field parse fun(self: TSParser, tree, source: integer|string): TSTree, integer[] +---@field parse fun(self: TSParser, tree, source: integer|string): TSTree, Range4[] ---@field reset fun(self: TSParser) ----@field included_ranges fun(self: TSParser): integer[] ----@field set_included_ranges fun(self: TSParser, ranges: integer[][]) +---@field included_ranges fun(self: TSParser): Range4[] +---@field set_included_ranges fun(self: TSParser, ranges: Range6[]) ---@field set_timeout fun(self: TSParser, timeout: integer) ---@field timeout fun(self: TSParser): integer diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index b8b0dd867e..fbc602486b 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -399,6 +399,169 @@ local function get_range_from_metadata(node, id, metadata) return { node:range() } end +---@private +--- TODO(lewis6991): cleanup of the node_range interface +---@param node TSNode +---@param id integer +---@param metadata TSMetadata +---@return Range4[] +local function get_node_ranges(node, id, metadata, include_children) + local range = get_range_from_metadata(node, id, metadata) + + if include_children then + return { range } + end + + local ranges = {} ---@type Range4[] + + local srow, scol, erow, ecol = range[1], range[2], range[3], range[4] + + for i = 0, node:named_child_count() - 1 do + local child = node:named_child(i) + local child_srow, child_scol, child_erow, child_ecol = child:range() + if child_srow > srow or child_scol > scol then + table.insert(ranges, { srow, scol, child_srow, child_scol }) + end + srow = child_erow + scol = child_ecol + end + + if erow > srow or ecol > scol then + table.insert(ranges, { srow, scol, erow, ecol }) + end + + return ranges +end + +---@alias TSInjection table> + +---@private +---@param t table +---@param tree_index integer +---@param pattern integer +---@param lang string +---@param combined boolean +---@param ranges Range4[] +local function add_injection(t, tree_index, pattern, lang, combined, ranges) + assert(type(lang) == 'string') + + -- Each tree index should be isolated from the other nodes. + if not t[tree_index] then + t[tree_index] = {} + end + + if not t[tree_index][lang] then + t[tree_index][lang] = {} + end + + -- Key this by pattern. If combined is set to true all captures of this pattern + -- will be parsed by treesitter as the same "source". + -- If combined is false, each "region" will be parsed as a single source. + if not t[tree_index][lang][pattern] then + t[tree_index][lang][pattern] = { combined = combined, regions = {} } + end + + table.insert(t[tree_index][lang][pattern].regions, ranges) +end + +---@private +---Get node text +--- +---Note: `query.get_node_text` returns string|string[]|nil so use this simple alias function +---to annotate it returns string. +--- +---TODO(lewis6991): use [at]overload annotations on `query.get_node_text` +---@param node TSNode +---@param source integer|string +---@param metadata table +---@return string +local function get_node_text(node, source, metadata) + return query.get_node_text(node, source, { metadata = metadata }) --[[@as string]] +end + +---@private +--- Extract injections according to: +--- https://tree-sitter.github.io/tree-sitter/syntax-highlighting#language-injection +---@param match table +---@param metadata table +---@return string, boolean, Range4[] +function LanguageTree:_get_injection(match, metadata) + local ranges = {} ---@type Range4[] + local combined = metadata['injection.combined'] ~= nil + local lang = metadata['injection.language'] ---@type string + local include_children = metadata['injection.include-children'] ~= nil + + for id, node in pairs(match) do + local name = self._injection_query.captures[id] + + -- Lang should override any other language tag + if name == 'injection.language' then + lang = get_node_text(node, self._source, metadata[id]) + elseif name == 'injection.content' then + ranges = get_node_ranges(node, id, metadata, include_children) + end + end + + return lang, combined, ranges +end + +---@private +---@param match table +---@param metadata table +---@return string, boolean, Range4[] +function LanguageTree:_get_injection_deprecated(match, metadata) + local lang = nil ---@type string + local ranges = {} ---@type Range4[] + local combined = metadata.combined ~= nil + + -- Directives can configure how injections are captured as well as actual node captures. + -- This allows more advanced processing for determining ranges and language resolution. + if metadata.content then + local content = metadata.content ---@type any + + -- Allow for captured nodes to be used + if type(content) == 'number' then + content = { match[content]:range() } + end + + if type(content) == 'table' and #content >= 4 then + vim.list_extend(ranges, content) + end + end + + if metadata.language then + lang = metadata.language ---@type string + end + + -- You can specify the content and language together + -- using a tag with the language, for example + -- @javascript + for id, node in pairs(match) do + local name = self._injection_query.captures[id] + + -- Lang should override any other language tag + if name == 'language' and not lang then + lang = get_node_text(node, self._source, metadata[id]) + elseif name == 'combined' then + combined = true + elseif name == 'content' and #ranges == 0 then + table.insert(ranges, get_range_from_metadata(node, id, metadata)) + -- Ignore any tags that start with "_" + -- Allows for other tags to be used in matches + elseif string.sub(name, 1, 1) ~= '_' then + if not lang then + lang = name + end + + if #ranges == 0 then + table.insert(ranges, get_range_from_metadata(node, id, metadata)) + end + end + end + + return lang, combined, ranges +end + --- Gets language injection points by language. --- --- This is where most of the injection processing occurs. @@ -406,13 +569,13 @@ end --- TODO: Allow for an offset predicate to tailor the injection range --- instead of using the entire nodes range. ---@private ----@return table +---@return table function LanguageTree:_get_injections() if not self._injection_query then return {} end - ---@type table>> + ---@type table local injections = {} for tree_index, tree in ipairs(self._trees) do @@ -422,75 +585,12 @@ function LanguageTree:_get_injections() for pattern, match, metadata in self._injection_query:iter_matches(root_node, self._source, start_line, end_line + 1) do - local lang = nil ---@type string - local ranges = {} ---@type Range4[] - local combined = metadata.combined ---@type boolean - - -- Directives can configure how injections are captured as well as actual node captures. - -- This allows more advanced processing for determining ranges and language resolution. - if metadata.content then - local content = metadata.content ---@type any - - -- Allow for captured nodes to be used - if type(content) == 'number' then - content = { match[content]:range() } - end - - if type(content) == 'table' and #content >= 4 then - vim.list_extend(ranges, content) - end - end - - if metadata.language then - lang = metadata.language ---@type string - end - - -- You can specify the content and language together - -- using a tag with the language, for example - -- @javascript - for id, node in pairs(match) do - local name = self._injection_query.captures[id] - - -- Lang should override any other language tag - if name == 'language' and not lang then - ---@diagnostic disable-next-line - lang = query.get_node_text(node, self._source, { metadata = metadata[id] }) - elseif name == 'combined' then - combined = true - elseif name == 'content' and #ranges == 0 then - table.insert(ranges, get_range_from_metadata(node, id, metadata)) - -- Ignore any tags that start with "_" - -- Allows for other tags to be used in matches - elseif string.sub(name, 1, 1) ~= '_' then - if not lang then - lang = name - end - - if #ranges == 0 then - table.insert(ranges, get_range_from_metadata(node, id, metadata)) - end - end + local lang, combined, ranges = self:_get_injection(match, metadata) + if not lang then + -- TODO(lewis6991): remove after 0.9 (#20434) + lang, combined, ranges = self:_get_injection_deprecated(match, metadata) end - - assert(type(lang) == 'string') - - -- Each tree index should be isolated from the other nodes. - if not injections[tree_index] then - injections[tree_index] = {} - end - - if not injections[tree_index][lang] then - injections[tree_index][lang] = {} - end - - -- Key this by pattern. If combined is set to true all captures of this pattern - -- will be parsed by treesitter as the same "source". - -- If combined is false, each "region" will be parsed as a single source. - if not injections[tree_index][lang][pattern] then - injections[tree_index][lang][pattern] = { combined = combined, regions = {} } - end - - table.insert(injections[tree_index][lang][pattern].regions, ranges) + add_injection(injections, tree_index, pattern, lang, combined, ranges) end end diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index 22f706585e..59894cc7f5 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -407,7 +407,7 @@ predicate_handlers['vim-match?'] = predicate_handlers['match?'] ---@field [string] integer|string ---@field range Range4 ----@alias TSDirective fun(match: TSMatch, _, _, predicate: any[], metadata: TSMetadata) +---@alias TSDirective fun(match: TSMatch, _, _, predicate: (string|integer)[], metadata: TSMetadata) -- Predicate handler receive the following arguments -- (match, pattern, bufnr, predicate) @@ -419,24 +419,17 @@ predicate_handlers['vim-match?'] = predicate_handlers['match?'] ---@type table local directive_handlers = { ['set!'] = function(_, _, _, pred, metadata) - if #pred == 4 then - -- (#set! @capture "key" "value") - ---@diagnostic disable-next-line:no-unknown - local _, capture_id, key, value = unpack(pred) - ---@cast value integer|string - ---@cast capture_id integer - ---@cast key string + if #pred >= 3 and type(pred[2]) == 'number' then + -- (#set! @capture key value) + local capture_id, key, value = pred[2], pred[3], pred[4] if not metadata[capture_id] then metadata[capture_id] = {} end metadata[capture_id][key] = value else - ---@diagnostic disable-next-line:no-unknown - local _, key, value = unpack(pred) - ---@cast value integer|string - ---@cast key string - -- (#set! "key" "value") - metadata[key] = value + -- (#set! key value) + local key, value = pred[2], pred[3] + metadata[key] = value or true end end, -- Shifts the range of a node. -- cgit From 276b647fdba07bf1762d8dd371c4b655b8a418df Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Wed, 8 Mar 2023 17:22:28 +0000 Subject: refactor(treesitter): delegate region calculation to treesitter (#22553) --- runtime/lua/vim/treesitter/_meta.lua | 7 +- runtime/lua/vim/treesitter/_range.lua | 28 +- runtime/lua/vim/treesitter/languagetree.lua | 410 +++++++++++++++++----------- runtime/lua/vim/treesitter/query.lua | 1 + 4 files changed, 271 insertions(+), 175 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/_meta.lua b/runtime/lua/vim/treesitter/_meta.lua index ad0854706b..72823ccf26 100644 --- a/runtime/lua/vim/treesitter/_meta.lua +++ b/runtime/lua/vim/treesitter/_meta.lua @@ -3,7 +3,7 @@ ---@class TSNode ---@field id fun(self: TSNode): integer ---@field tree fun(self: TSNode): TSTree ----@field range fun(self: TSNode): integer, integer, integer, integer +---@field range fun(self: TSNode, include_bytes: boolean?): integer, integer, integer, integer, integer, integer ---@field start fun(self: TSNode): integer, integer, integer ---@field end_ fun(self: TSNode): integer, integer, integer ---@field type fun(self: TSNode): string @@ -43,9 +43,9 @@ function TSNode:_rawquery(query, captures, start, end_) end function TSNode:_rawquery(query, captures, start, end_) end ---@class TSParser ----@field parse fun(self: TSParser, tree, source: integer|string): TSTree, Range4[] +---@field parse fun(self: TSParser, tree: TSTree?, source: integer|string, include_bytes: boolean?): TSTree, integer[] ---@field reset fun(self: TSParser) ----@field included_ranges fun(self: TSParser): Range4[] +---@field included_ranges fun(self: TSParser, include_bytes: boolean?): integer[] ---@field set_included_ranges fun(self: TSParser, ranges: Range6[]) ---@field set_timeout fun(self: TSParser, timeout: integer) ---@field timeout fun(self: TSParser): integer @@ -54,6 +54,7 @@ function TSNode:_rawquery(query, captures, start, end_) end ---@field root fun(self: TSTree): TSNode ---@field edit fun(self: TSTree, _: integer, _: integer, _: integer, _: integer, _: integer, _: integer, _: integer, _: integer, _:integer) ---@field copy fun(self: TSTree): TSTree +---@field included_ranges fun(self: TSTree, include_bytes: boolean?): integer[] ---@return integer vim._ts_get_language_version = function() end diff --git a/runtime/lua/vim/treesitter/_range.lua b/runtime/lua/vim/treesitter/_range.lua index bec24a23a5..21e46a560a 100644 --- a/runtime/lua/vim/treesitter/_range.lua +++ b/runtime/lua/vim/treesitter/_range.lua @@ -78,11 +78,8 @@ end ---@param r2 Range4|Range6 ---@return boolean function M.intercepts(r1, r2) - local off_1 = #r1 == 6 and 1 or 0 - local off_2 = #r1 == 6 and 1 or 0 - - local srow_1, scol_1, erow_1, ecol_1 = r1[1], r1[2], r1[3 + off_1], r1[4 + off_1] - local srow_2, scol_2, erow_2, ecol_2 = r2[1], r2[2], r2[3 + off_2], r2[4 + off_2] + local srow_1, scol_1, erow_1, ecol_1 = M.unpack4(r1) + local srow_2, scol_2, erow_2, ecol_2 = M.unpack4(r2) -- r1 is above r2 if M.cmp_pos.le(erow_1, ecol_1, srow_2, scol_2) then @@ -97,16 +94,21 @@ function M.intercepts(r1, r2) return true end +---@private +---@param r Range4|Range6 +---@return integer, integer, integer, integer +function M.unpack4(r) + local off_1 = #r == 6 and 1 or 0 + return r[1], r[2], r[3 + off_1], r[4 + off_1] +end + ---@private ---@param r1 Range4|Range6 ---@param r2 Range4|Range6 ---@return boolean whether r1 contains r2 function M.contains(r1, r2) - local off_1 = #r1 == 6 and 1 or 0 - local off_2 = #r1 == 6 and 1 or 0 - - local srow_1, scol_1, erow_1, ecol_1 = r1[1], r1[2], r1[3 + off_1], r1[4 + off_1] - local srow_2, scol_2, erow_2, ecol_2 = r2[1], r2[2], r2[3 + off_2], r2[4 + off_2] + local srow_1, scol_1, erow_1, ecol_1 = M.unpack4(r1) + local srow_2, scol_2, erow_2, ecol_2 = M.unpack4(r2) -- start doesn't fit if M.cmp_pos.gt(srow_1, scol_1, srow_2, scol_2) then @@ -123,9 +125,13 @@ end ---@private ---@param source integer|string ----@param range Range4 +---@param range Range4|Range6 ---@return Range6 function M.add_bytes(source, range) + if type(range) == 'table' and #range == 6 then + return range --[[@as Range6]] + end + local start_row, start_col, end_row, end_col = range[1], range[2], range[3], range[4] local start_byte = 0 local end_byte = 0 diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index fbc602486b..57a60bf774 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -57,13 +57,13 @@ local Range = require('vim.treesitter._range') ---@field private _injection_query Query Queries defining injected languages ---@field private _opts table Options ---@field private _parser TSParser Parser for language ----@field private _regions Range6[][] List of regions this tree should manage and parse +---@field private _regions Range6[][]? +---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 ---@field private _source (integer|string) Buffer or string to parse ---@field private _trees TSTree[] Reference to parsed tree (one for each language) ---@field private _valid boolean|table If the parsed tree is valid ---- TODO(lewis6991): combine _regions, _valid and _trees ----@field private _is_child boolean local LanguageTree = {} ---@class LanguageTreeOpts @@ -98,7 +98,6 @@ function LanguageTree.new(source, lang, opts) _source = source, _lang = lang, _children = {}, - _regions = {}, _trees = {}, _opts = opts, _injection_query = injections[lang] and query.parse_query(lang, injections[lang]) @@ -117,6 +116,48 @@ function LanguageTree.new(source, lang, opts) return self end +---@private +---Measure execution time of a function +---@generic R1, R2, R3 +---@param f fun(): R1, R2, R2 +---@return integer, R1, R2, R3 +local function tcall(f, ...) + local start = vim.loop.hrtime() + ---@diagnostic disable-next-line + local r = { f(...) } + local duration = (vim.loop.hrtime() - start) / 1000000 + return duration, unpack(r) +end + +---@private +---@vararg any +function LanguageTree:_log(...) + if vim.g.__ts_debug == nil then + return + end + + local args = { ... } + if type(args[1]) == 'function' then + args = { args[1]() } + end + + local info = debug.getinfo(2, 'nl') + local nregions = #self:included_regions() + local prefix = + string.format('%s:%d: [%s:%d] ', info.name, info.currentline, self:lang(), nregions) + + a.nvim_out_write(prefix) + for _, x in ipairs(args) do + if type(x) == 'string' then + a.nvim_out_write(x) + else + a.nvim_out_write(vim.inspect(x, { newline = ' ', indent = '' })) + end + a.nvim_out_write(' ') + end + a.nvim_out_write('\n') +end + --- Invalidates this parser and all its children ---@param reload boolean|nil function LanguageTree:invalidate(reload) @@ -146,7 +187,9 @@ end --- Determines whether this tree is valid. --- If the tree is invalid, call `parse()`. --- This will return the updated tree. -function LanguageTree:is_valid() +---@param exclude_children boolean|nil +---@return boolean +function LanguageTree:is_valid(exclude_children) local valid = self._valid if type(valid) == 'table' then @@ -155,9 +198,18 @@ function LanguageTree:is_valid() return false end end - return true end + if not exclude_children then + for _, child in pairs(self._children) do + if not child:is_valid(exclude_children) then + return false + end + end + end + + assert(type(valid) == 'boolean') + return valid end @@ -171,16 +223,6 @@ function LanguageTree:source() return self._source end ----@private ----This is only exposed so it can be wrapped for profiling ----@param old_tree TSTree ----@return TSTree, integer[] -function LanguageTree:_parse_tree(old_tree) - local tree, tree_changes = self._parser:parse(old_tree, self._source) - self:_do_callback('changedtree', tree_changes, tree) - return tree, tree_changes -end - --- Parses all defined regions using a treesitter parser --- for the language this tree represents. --- This will run the injection query for this language to @@ -190,31 +232,39 @@ end ---@return table|nil Change list function LanguageTree:parse() if self:is_valid() then + self:_log('valid') return self._trees end local changes = {} - -- If there are no ranges, set to an empty list - -- so the included ranges in the parser are cleared. - if #self._regions > 0 then - for i, ranges in ipairs(self._regions) do + -- Collect some stats + local regions_parsed = 0 + local total_parse_time = 0 + + --- At least 1 region is invalid + if not self:is_valid(true) then + -- If there are no ranges, set to an empty list + -- so the included ranges in the parser are cleared. + for i, ranges in ipairs(self:included_regions()) do if not self._valid or not self._valid[i] then self._parser:set_included_ranges(ranges) - local tree, tree_changes = self:_parse_tree(self._trees[i]) + local parse_time, tree, tree_changes = + tcall(self._parser.parse, self._parser, self._trees[i], self._source) + + self:_do_callback('changedtree', tree_changes, tree) self._trees[i] = tree vim.list_extend(changes, tree_changes) + + total_parse_time = total_parse_time + parse_time + regions_parsed = regions_parsed + 1 end end - else - local tree, tree_changes = self:_parse_tree(self._trees[1]) - self._trees = { tree } - changes = tree_changes end - local injections_by_lang = self:_get_injections() local seen_langs = {} ---@type table + local query_time, injections_by_lang = tcall(self._get_injections, self) for lang, injection_ranges in pairs(injections_by_lang) do local has_lang = pcall(language.add, lang) @@ -229,15 +279,6 @@ function LanguageTree:parse() end child:set_included_regions(injection_ranges) - - local _, child_changes = child:parse() - - -- Propagate any child changes so they are included in the - -- the change list for the callback. - if child_changes then - vim.list_extend(changes, child_changes) - end - seen_langs[lang] = true end end @@ -248,6 +289,23 @@ function LanguageTree:parse() end end + self:_log({ + changes = changes, + regions_parsed = regions_parsed, + parse_time = total_parse_time, + query_time = query_time, + }) + + self:for_each_child(function(child) + local _, child_changes = child:parse() + + -- Propagate any child changes so they are included in the + -- the change list for the callback. + if child_changes then + vim.list_extend(changes, child_changes) + end + end) + self._valid = true return self._trees, changes @@ -295,8 +353,6 @@ function LanguageTree:add_child(lang) end self._children[lang] = LanguageTree.new(self._source, lang, self._opts) - self._children[lang]._is_child = true - self:invalidate() self:_do_callback('child_added', self._children[lang]) @@ -331,6 +387,53 @@ function LanguageTree:destroy() end end +---@private +---@param region Range6[] +local function region_tostr(region) + local srow, scol = region[1][1], region[1][2] + local erow, ecol = region[#region][4], region[#region][5] + return string.format('[%d:%d-%d:%d]', srow, scol, erow, ecol) +end + +---@private +---Sets self._valid properly and efficiently +---@param fn fun(index: integer, region: Range6[]): boolean +function LanguageTree:_validate_regions(fn) + if not self._valid then + return + end + + if type(self._valid) ~= 'table' then + self._valid = {} + end + + local all_valid = true + + for i, region in ipairs(self:included_regions()) do + if self._valid[i] == nil then + self._valid[i] = true + end + + if self._valid[i] then + self._valid[i] = fn(i, region) + if not self._valid[i] then + self:_log(function() + return 'invalidating region', i, region_tostr(region) + end) + end + end + + if not self._valid[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 +end + --- Sets the included regions that should be parsed by this |LanguageTree|. --- A region is a set of nodes and/or ranges that will be parsed in the same context. --- @@ -357,56 +460,57 @@ function LanguageTree:set_included_regions(regions) end end - if #self._regions ~= #regions then + if #self:included_regions() ~= #regions then self._trees = {} self:invalidate() - elseif self._valid ~= false then - if self._valid == true then - self._valid = {} - for i = 1, #regions do - self._valid[i] = true - end - end - - for i = 1, #regions do - if not vim.deep_equal(self._regions[i], regions[i]) then - self._valid[i] = false - end - - if not self._valid[i] then - self._trees[i] = nil - end - end + else + self:_validate_regions(function(i, region) + return vim.deep_equal(regions[i], region) + end) end - self._regions = regions end ---- Gets the set of included regions +---Gets the set of included regions +---@return integer[][] function LanguageTree:included_regions() - return self._regions + if self._regions then + return self._regions + end + + if #self._trees == 0 then + return { {} } + end + + local regions = {} ---@type Range6[][] + for i, _ in ipairs(self._trees) do + regions[i] = self._trees[i]:included_ranges(true) + end + + self._regions = regions + return regions end ---@private ---@param node TSNode ----@param id integer +---@param source integer|string ---@param metadata TSMetadata ----@return Range4 -local function get_range_from_metadata(node, id, metadata) - if metadata[id] and metadata[id].range then - return metadata[id].range --[[@as Range4]] +---@return Range6 +local function get_range_from_metadata(node, source, metadata) + if metadata and metadata.range then + return Range.add_bytes(source, metadata.range --[[@as Range4|Range6]]) end - return { node:range() } + return { node:range(true) } end ---@private --- TODO(lewis6991): cleanup of the node_range interface ---@param node TSNode ----@param id integer +---@param source string|integer ---@param metadata TSMetadata ---@return Range4[] -local function get_node_ranges(node, id, metadata, include_children) - local range = get_range_from_metadata(node, id, metadata) +local function get_node_ranges(node, source, metadata, include_children) + local range = get_range_from_metadata(node, source, metadata) if include_children then return { range } @@ -414,7 +518,7 @@ local function get_node_ranges(node, id, metadata, include_children) local ranges = {} ---@type Range4[] - local srow, scol, erow, ecol = range[1], range[2], range[3], range[4] + local srow, scol, erow, ecol = Range.unpack4(range) for i = 0, node:named_child_count() - 1 do local child = node:named_child(i) @@ -498,7 +602,7 @@ function LanguageTree:_get_injection(match, metadata) if name == 'injection.language' then lang = get_node_text(node, self._source, metadata[id]) elseif name == 'injection.content' then - ranges = get_node_ranges(node, id, metadata, include_children) + ranges = get_node_ranges(node, self._source, metadata[id], include_children) end end @@ -545,7 +649,7 @@ function LanguageTree:_get_injection_deprecated(match, metadata) elseif name == 'combined' then combined = true elseif name == 'content' and #ranges == 0 then - table.insert(ranges, get_range_from_metadata(node, id, metadata)) + table.insert(ranges, get_range_from_metadata(node, self._source, metadata[id])) -- Ignore any tags that start with "_" -- Allows for other tags to be used in matches elseif string.sub(name, 1, 1) ~= '_' then @@ -554,7 +658,7 @@ function LanguageTree:_get_injection_deprecated(match, metadata) end if #ranges == 0 then - table.insert(ranges, get_range_from_metadata(node, id, metadata)) + table.insert(ranges, get_range_from_metadata(node, self._source, metadata[id])) end end end @@ -569,7 +673,7 @@ end --- TODO: Allow for an offset predicate to tailor the injection range --- instead of using the entire nodes range. ---@private ----@return table +---@return table function LanguageTree:_get_injections() if not self._injection_query then return {} @@ -594,7 +698,7 @@ function LanguageTree:_get_injections() end end - ---@type table + ---@type table local result = {} -- Generate a map by lang of node lists. @@ -634,42 +738,51 @@ function LanguageTree:_do_callback(cb_name, ...) end ---@private ----@param regions Range6[][] ----@param old_range Range6 ----@param new_range Range6 ----@return table region indices to invalidate -local function update_regions(regions, old_range, new_range) - ---@type table - local valid = {} - - for i, ranges in ipairs(regions or {}) do - valid[i] = true - for j, r in ipairs(ranges) do - if Range.intercepts(r, old_range) then - valid[i] = false - break - end +function LanguageTree:_edit( + start_byte, + end_byte_old, + end_byte_new, + start_row, + start_col, + end_row_old, + end_col_old, + end_row_new, + end_col_new +) + for _, tree in ipairs(self._trees) do + tree:edit( + start_byte, + end_byte_old, + end_byte_new, + start_row, + start_col, + end_row_old, + end_col_old, + end_row_new, + end_col_new + ) + end + + self._regions = nil + + local changed_range = { + start_row, + start_col, + start_byte, + end_row_old, + end_col_old, + end_byte_old, + } - -- Range after change. Adjust - if Range.cmp_pos.gt(r[1], r[2], old_range[4], old_range[5]) then - local byte_offset = new_range[6] - old_range[6] - local row_offset = new_range[4] - old_range[4] - - -- Update the range to avoid invalidation in set_included_regions() - -- which will compare the regions against the parsed injection regions - ranges[j] = { - r[1] + row_offset, - r[2], - r[3] + byte_offset, - r[4] + row_offset, - r[5], - r[6] + byte_offset, - } + -- Validate regions after editing the tree + self:_validate_regions(function(_, region) + for _, r in ipairs(region) do + if Range.intercepts(r, changed_range) then + return false end end - end - - return valid + return true + end) end ---@private @@ -700,49 +813,26 @@ function LanguageTree:_on_bytes( local old_end_col = old_col + ((old_row == 0) and start_col or 0) local new_end_col = new_col + ((new_row == 0) and start_col or 0) - local old_range = { - start_row, - start_col, - start_byte, - start_row + old_row, - old_end_col, - start_byte + old_byte, - } - - local new_range = { + self:_log( + 'on_bytes', + bufnr, + changed_tick, start_row, start_col, start_byte, - start_row + new_row, - new_end_col, - start_byte + new_byte, - } - - if #self._regions == 0 then - self._valid = false - else - self._valid = update_regions(self._regions, old_range, new_range) - end - - for _, child in pairs(self._children) do - child:_on_bytes( - bufnr, - changed_tick, - start_row, - start_col, - start_byte, - old_row, - old_col, - old_byte, - new_row, - new_col, - new_byte - ) - end + old_row, + old_col, + old_byte, + new_row, + new_col, + new_byte + ) -- Edit trees together BEFORE emitting a bytes callback. - for _, tree in ipairs(self._trees) do - tree:edit( + ---@private + self:for_each_child(function(child) + ---@diagnostic disable-next-line:invisible + child:_edit( start_byte, start_byte + old_byte, start_byte + new_byte, @@ -753,24 +843,22 @@ function LanguageTree:_on_bytes( start_row + new_row, new_end_col ) - end + end, true) - if not self._is_child then - self:_do_callback( - 'bytes', - bufnr, - changed_tick, - start_row, - start_col, - start_byte, - old_row, - old_col, - old_byte, - new_row, - new_col, - new_byte - ) - end + self:_do_callback( + 'bytes', + bufnr, + changed_tick, + start_row, + start_col, + start_byte, + old_row, + old_col, + old_byte, + new_row, + new_col, + new_byte + ) end ---@private diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index 59894cc7f5..e7cf42283d 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -277,6 +277,7 @@ end ---@return (string[]|string|nil) function M.get_node_text(node, source, opts) opts = opts or {} + -- TODO(lewis6991): concat only works when source is number. local concat = vim.F.if_nil(opts.concat, true) local metadata = opts.metadata or {} -- cgit From b9f19d3e286d95d9209afbc479fa2eb908067fb1 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Wed, 8 Mar 2023 17:59:45 +0000 Subject: Revert "refactor(treesitter): delegate region calculation to treesitter" (#22575) Revert "refactor(treesitter): delegate region calculation to treesitter (#22553)" This reverts commit 276b647fdba07bf1762d8dd371c4b655b8a418df. --- runtime/lua/vim/treesitter/_meta.lua | 7 +- runtime/lua/vim/treesitter/_range.lua | 28 +- runtime/lua/vim/treesitter/languagetree.lua | 410 +++++++++++----------------- runtime/lua/vim/treesitter/query.lua | 1 - 4 files changed, 175 insertions(+), 271 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/_meta.lua b/runtime/lua/vim/treesitter/_meta.lua index 72823ccf26..ad0854706b 100644 --- a/runtime/lua/vim/treesitter/_meta.lua +++ b/runtime/lua/vim/treesitter/_meta.lua @@ -3,7 +3,7 @@ ---@class TSNode ---@field id fun(self: TSNode): integer ---@field tree fun(self: TSNode): TSTree ----@field range fun(self: TSNode, include_bytes: boolean?): integer, integer, integer, integer, integer, integer +---@field range fun(self: TSNode): integer, integer, integer, integer ---@field start fun(self: TSNode): integer, integer, integer ---@field end_ fun(self: TSNode): integer, integer, integer ---@field type fun(self: TSNode): string @@ -43,9 +43,9 @@ function TSNode:_rawquery(query, captures, start, end_) end function TSNode:_rawquery(query, captures, start, end_) end ---@class TSParser ----@field parse fun(self: TSParser, tree: TSTree?, source: integer|string, include_bytes: boolean?): TSTree, integer[] +---@field parse fun(self: TSParser, tree, source: integer|string): TSTree, Range4[] ---@field reset fun(self: TSParser) ----@field included_ranges fun(self: TSParser, include_bytes: boolean?): integer[] +---@field included_ranges fun(self: TSParser): Range4[] ---@field set_included_ranges fun(self: TSParser, ranges: Range6[]) ---@field set_timeout fun(self: TSParser, timeout: integer) ---@field timeout fun(self: TSParser): integer @@ -54,7 +54,6 @@ function TSNode:_rawquery(query, captures, start, end_) end ---@field root fun(self: TSTree): TSNode ---@field edit fun(self: TSTree, _: integer, _: integer, _: integer, _: integer, _: integer, _: integer, _: integer, _: integer, _:integer) ---@field copy fun(self: TSTree): TSTree ----@field included_ranges fun(self: TSTree, include_bytes: boolean?): integer[] ---@return integer vim._ts_get_language_version = function() end diff --git a/runtime/lua/vim/treesitter/_range.lua b/runtime/lua/vim/treesitter/_range.lua index 21e46a560a..bec24a23a5 100644 --- a/runtime/lua/vim/treesitter/_range.lua +++ b/runtime/lua/vim/treesitter/_range.lua @@ -78,8 +78,11 @@ end ---@param r2 Range4|Range6 ---@return boolean function M.intercepts(r1, r2) - local srow_1, scol_1, erow_1, ecol_1 = M.unpack4(r1) - local srow_2, scol_2, erow_2, ecol_2 = M.unpack4(r2) + local off_1 = #r1 == 6 and 1 or 0 + local off_2 = #r1 == 6 and 1 or 0 + + local srow_1, scol_1, erow_1, ecol_1 = r1[1], r1[2], r1[3 + off_1], r1[4 + off_1] + local srow_2, scol_2, erow_2, ecol_2 = r2[1], r2[2], r2[3 + off_2], r2[4 + off_2] -- r1 is above r2 if M.cmp_pos.le(erow_1, ecol_1, srow_2, scol_2) then @@ -94,21 +97,16 @@ function M.intercepts(r1, r2) return true end ----@private ----@param r Range4|Range6 ----@return integer, integer, integer, integer -function M.unpack4(r) - local off_1 = #r == 6 and 1 or 0 - return r[1], r[2], r[3 + off_1], r[4 + off_1] -end - ---@private ---@param r1 Range4|Range6 ---@param r2 Range4|Range6 ---@return boolean whether r1 contains r2 function M.contains(r1, r2) - local srow_1, scol_1, erow_1, ecol_1 = M.unpack4(r1) - local srow_2, scol_2, erow_2, ecol_2 = M.unpack4(r2) + local off_1 = #r1 == 6 and 1 or 0 + local off_2 = #r1 == 6 and 1 or 0 + + local srow_1, scol_1, erow_1, ecol_1 = r1[1], r1[2], r1[3 + off_1], r1[4 + off_1] + local srow_2, scol_2, erow_2, ecol_2 = r2[1], r2[2], r2[3 + off_2], r2[4 + off_2] -- start doesn't fit if M.cmp_pos.gt(srow_1, scol_1, srow_2, scol_2) then @@ -125,13 +123,9 @@ end ---@private ---@param source integer|string ----@param range Range4|Range6 +---@param range Range4 ---@return Range6 function M.add_bytes(source, range) - if type(range) == 'table' and #range == 6 then - return range --[[@as Range6]] - end - local start_row, start_col, end_row, end_col = range[1], range[2], range[3], range[4] local start_byte = 0 local end_byte = 0 diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index 57a60bf774..fbc602486b 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -57,13 +57,13 @@ local Range = require('vim.treesitter._range') ---@field private _injection_query Query Queries defining injected languages ---@field private _opts table Options ---@field private _parser TSParser Parser for language ----@field private _regions Range6[][]? ----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 _regions Range6[][] List of regions this tree should manage and parse ---@field private _lang string Language name ---@field private _source (integer|string) Buffer or string to parse ---@field private _trees TSTree[] Reference to parsed tree (one for each language) ---@field private _valid boolean|table If the parsed tree is valid +--- TODO(lewis6991): combine _regions, _valid and _trees +---@field private _is_child boolean local LanguageTree = {} ---@class LanguageTreeOpts @@ -98,6 +98,7 @@ function LanguageTree.new(source, lang, opts) _source = source, _lang = lang, _children = {}, + _regions = {}, _trees = {}, _opts = opts, _injection_query = injections[lang] and query.parse_query(lang, injections[lang]) @@ -116,48 +117,6 @@ function LanguageTree.new(source, lang, opts) return self end ----@private ----Measure execution time of a function ----@generic R1, R2, R3 ----@param f fun(): R1, R2, R2 ----@return integer, R1, R2, R3 -local function tcall(f, ...) - local start = vim.loop.hrtime() - ---@diagnostic disable-next-line - local r = { f(...) } - local duration = (vim.loop.hrtime() - start) / 1000000 - return duration, unpack(r) -end - ----@private ----@vararg any -function LanguageTree:_log(...) - if vim.g.__ts_debug == nil then - return - end - - local args = { ... } - if type(args[1]) == 'function' then - args = { args[1]() } - end - - local info = debug.getinfo(2, 'nl') - local nregions = #self:included_regions() - local prefix = - string.format('%s:%d: [%s:%d] ', info.name, info.currentline, self:lang(), nregions) - - a.nvim_out_write(prefix) - for _, x in ipairs(args) do - if type(x) == 'string' then - a.nvim_out_write(x) - else - a.nvim_out_write(vim.inspect(x, { newline = ' ', indent = '' })) - end - a.nvim_out_write(' ') - end - a.nvim_out_write('\n') -end - --- Invalidates this parser and all its children ---@param reload boolean|nil function LanguageTree:invalidate(reload) @@ -187,9 +146,7 @@ end --- Determines whether this tree is valid. --- If the tree is invalid, call `parse()`. --- This will return the updated tree. ----@param exclude_children boolean|nil ----@return boolean -function LanguageTree:is_valid(exclude_children) +function LanguageTree:is_valid() local valid = self._valid if type(valid) == 'table' then @@ -198,18 +155,9 @@ function LanguageTree:is_valid(exclude_children) return false end end + return true end - if not exclude_children then - for _, child in pairs(self._children) do - if not child:is_valid(exclude_children) then - return false - end - end - end - - assert(type(valid) == 'boolean') - return valid end @@ -223,6 +171,16 @@ function LanguageTree:source() return self._source end +---@private +---This is only exposed so it can be wrapped for profiling +---@param old_tree TSTree +---@return TSTree, integer[] +function LanguageTree:_parse_tree(old_tree) + local tree, tree_changes = self._parser:parse(old_tree, self._source) + self:_do_callback('changedtree', tree_changes, tree) + return tree, tree_changes +end + --- Parses all defined regions using a treesitter parser --- for the language this tree represents. --- This will run the injection query for this language to @@ -232,39 +190,31 @@ end ---@return table|nil Change list function LanguageTree:parse() if self:is_valid() then - self:_log('valid') return self._trees end local changes = {} - -- Collect some stats - local regions_parsed = 0 - local total_parse_time = 0 - - --- At least 1 region is invalid - if not self:is_valid(true) then - -- If there are no ranges, set to an empty list - -- so the included ranges in the parser are cleared. - for i, ranges in ipairs(self:included_regions()) do + -- If there are no ranges, set to an empty list + -- so the included ranges in the parser are cleared. + if #self._regions > 0 then + for i, ranges in ipairs(self._regions) do if not self._valid or not self._valid[i] then self._parser:set_included_ranges(ranges) - local parse_time, tree, tree_changes = - tcall(self._parser.parse, self._parser, self._trees[i], self._source) - - self:_do_callback('changedtree', tree_changes, tree) + local tree, tree_changes = self:_parse_tree(self._trees[i]) self._trees[i] = tree vim.list_extend(changes, tree_changes) - - total_parse_time = total_parse_time + parse_time - regions_parsed = regions_parsed + 1 end end + else + local tree, tree_changes = self:_parse_tree(self._trees[1]) + self._trees = { tree } + changes = tree_changes end + local injections_by_lang = self:_get_injections() local seen_langs = {} ---@type table - local query_time, injections_by_lang = tcall(self._get_injections, self) for lang, injection_ranges in pairs(injections_by_lang) do local has_lang = pcall(language.add, lang) @@ -279,6 +229,15 @@ function LanguageTree:parse() end child:set_included_regions(injection_ranges) + + local _, child_changes = child:parse() + + -- Propagate any child changes so they are included in the + -- the change list for the callback. + if child_changes then + vim.list_extend(changes, child_changes) + end + seen_langs[lang] = true end end @@ -289,23 +248,6 @@ function LanguageTree:parse() end end - self:_log({ - changes = changes, - regions_parsed = regions_parsed, - parse_time = total_parse_time, - query_time = query_time, - }) - - self:for_each_child(function(child) - local _, child_changes = child:parse() - - -- Propagate any child changes so they are included in the - -- the change list for the callback. - if child_changes then - vim.list_extend(changes, child_changes) - end - end) - self._valid = true return self._trees, changes @@ -353,6 +295,8 @@ function LanguageTree:add_child(lang) end self._children[lang] = LanguageTree.new(self._source, lang, self._opts) + self._children[lang]._is_child = true + self:invalidate() self:_do_callback('child_added', self._children[lang]) @@ -387,53 +331,6 @@ function LanguageTree:destroy() end end ----@private ----@param region Range6[] -local function region_tostr(region) - local srow, scol = region[1][1], region[1][2] - local erow, ecol = region[#region][4], region[#region][5] - return string.format('[%d:%d-%d:%d]', srow, scol, erow, ecol) -end - ----@private ----Sets self._valid properly and efficiently ----@param fn fun(index: integer, region: Range6[]): boolean -function LanguageTree:_validate_regions(fn) - if not self._valid then - return - end - - if type(self._valid) ~= 'table' then - self._valid = {} - end - - local all_valid = true - - for i, region in ipairs(self:included_regions()) do - if self._valid[i] == nil then - self._valid[i] = true - end - - if self._valid[i] then - self._valid[i] = fn(i, region) - if not self._valid[i] then - self:_log(function() - return 'invalidating region', i, region_tostr(region) - end) - end - end - - if not self._valid[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 -end - --- Sets the included regions that should be parsed by this |LanguageTree|. --- A region is a set of nodes and/or ranges that will be parsed in the same context. --- @@ -460,57 +357,56 @@ function LanguageTree:set_included_regions(regions) end end - if #self:included_regions() ~= #regions then + if #self._regions ~= #regions then self._trees = {} self:invalidate() - else - self:_validate_regions(function(i, region) - return vim.deep_equal(regions[i], region) - end) - end - self._regions = regions -end - ----Gets the set of included regions ----@return integer[][] -function LanguageTree:included_regions() - if self._regions then - return self._regions - end + elseif self._valid ~= false then + if self._valid == true then + self._valid = {} + for i = 1, #regions do + self._valid[i] = true + end + end - if #self._trees == 0 then - return { {} } - end + for i = 1, #regions do + if not vim.deep_equal(self._regions[i], regions[i]) then + self._valid[i] = false + end - local regions = {} ---@type Range6[][] - for i, _ in ipairs(self._trees) do - regions[i] = self._trees[i]:included_ranges(true) + if not self._valid[i] then + self._trees[i] = nil + end + end end self._regions = regions - return regions +end + +--- Gets the set of included regions +function LanguageTree:included_regions() + return self._regions end ---@private ---@param node TSNode ----@param source integer|string +---@param id integer ---@param metadata TSMetadata ----@return Range6 -local function get_range_from_metadata(node, source, metadata) - if metadata and metadata.range then - return Range.add_bytes(source, metadata.range --[[@as Range4|Range6]]) +---@return Range4 +local function get_range_from_metadata(node, id, metadata) + if metadata[id] and metadata[id].range then + return metadata[id].range --[[@as Range4]] end - return { node:range(true) } + return { node:range() } end ---@private --- TODO(lewis6991): cleanup of the node_range interface ---@param node TSNode ----@param source string|integer +---@param id integer ---@param metadata TSMetadata ---@return Range4[] -local function get_node_ranges(node, source, metadata, include_children) - local range = get_range_from_metadata(node, source, metadata) +local function get_node_ranges(node, id, metadata, include_children) + local range = get_range_from_metadata(node, id, metadata) if include_children then return { range } @@ -518,7 +414,7 @@ local function get_node_ranges(node, source, metadata, include_children) local ranges = {} ---@type Range4[] - local srow, scol, erow, ecol = Range.unpack4(range) + local srow, scol, erow, ecol = range[1], range[2], range[3], range[4] for i = 0, node:named_child_count() - 1 do local child = node:named_child(i) @@ -602,7 +498,7 @@ function LanguageTree:_get_injection(match, metadata) if name == 'injection.language' then lang = get_node_text(node, self._source, metadata[id]) elseif name == 'injection.content' then - ranges = get_node_ranges(node, self._source, metadata[id], include_children) + ranges = get_node_ranges(node, id, metadata, include_children) end end @@ -649,7 +545,7 @@ function LanguageTree:_get_injection_deprecated(match, metadata) elseif name == 'combined' then combined = true elseif name == 'content' and #ranges == 0 then - table.insert(ranges, get_range_from_metadata(node, self._source, metadata[id])) + table.insert(ranges, get_range_from_metadata(node, id, metadata)) -- Ignore any tags that start with "_" -- Allows for other tags to be used in matches elseif string.sub(name, 1, 1) ~= '_' then @@ -658,7 +554,7 @@ function LanguageTree:_get_injection_deprecated(match, metadata) end if #ranges == 0 then - table.insert(ranges, get_range_from_metadata(node, self._source, metadata[id])) + table.insert(ranges, get_range_from_metadata(node, id, metadata)) end end end @@ -673,7 +569,7 @@ end --- TODO: Allow for an offset predicate to tailor the injection range --- instead of using the entire nodes range. ---@private ----@return table +---@return table function LanguageTree:_get_injections() if not self._injection_query then return {} @@ -698,7 +594,7 @@ function LanguageTree:_get_injections() end end - ---@type table + ---@type table local result = {} -- Generate a map by lang of node lists. @@ -738,51 +634,42 @@ function LanguageTree:_do_callback(cb_name, ...) end ---@private -function LanguageTree:_edit( - start_byte, - end_byte_old, - end_byte_new, - start_row, - start_col, - end_row_old, - end_col_old, - end_row_new, - end_col_new -) - for _, tree in ipairs(self._trees) do - tree:edit( - start_byte, - end_byte_old, - end_byte_new, - start_row, - start_col, - end_row_old, - end_col_old, - end_row_new, - end_col_new - ) - end - - self._regions = nil - - local changed_range = { - start_row, - start_col, - start_byte, - end_row_old, - end_col_old, - end_byte_old, - } +---@param regions Range6[][] +---@param old_range Range6 +---@param new_range Range6 +---@return table region indices to invalidate +local function update_regions(regions, old_range, new_range) + ---@type table + local valid = {} + + for i, ranges in ipairs(regions or {}) do + valid[i] = true + for j, r in ipairs(ranges) do + if Range.intercepts(r, old_range) then + valid[i] = false + break + end - -- Validate regions after editing the tree - self:_validate_regions(function(_, region) - for _, r in ipairs(region) do - if Range.intercepts(r, changed_range) then - return false + -- Range after change. Adjust + if Range.cmp_pos.gt(r[1], r[2], old_range[4], old_range[5]) then + local byte_offset = new_range[6] - old_range[6] + local row_offset = new_range[4] - old_range[4] + + -- Update the range to avoid invalidation in set_included_regions() + -- which will compare the regions against the parsed injection regions + ranges[j] = { + r[1] + row_offset, + r[2], + r[3] + byte_offset, + r[4] + row_offset, + r[5], + r[6] + byte_offset, + } end end - return true - end) + end + + return valid end ---@private @@ -813,26 +700,49 @@ function LanguageTree:_on_bytes( local old_end_col = old_col + ((old_row == 0) and start_col or 0) local new_end_col = new_col + ((new_row == 0) and start_col or 0) - self:_log( - 'on_bytes', - bufnr, - changed_tick, + local old_range = { + start_row, + start_col, + start_byte, + start_row + old_row, + old_end_col, + start_byte + old_byte, + } + + local new_range = { start_row, start_col, start_byte, - old_row, - old_col, - old_byte, - new_row, - new_col, - new_byte - ) + start_row + new_row, + new_end_col, + start_byte + new_byte, + } + + if #self._regions == 0 then + self._valid = false + else + self._valid = update_regions(self._regions, old_range, new_range) + end + + for _, child in pairs(self._children) do + child:_on_bytes( + bufnr, + changed_tick, + start_row, + start_col, + start_byte, + old_row, + old_col, + old_byte, + new_row, + new_col, + new_byte + ) + end -- Edit trees together BEFORE emitting a bytes callback. - ---@private - self:for_each_child(function(child) - ---@diagnostic disable-next-line:invisible - child:_edit( + for _, tree in ipairs(self._trees) do + tree:edit( start_byte, start_byte + old_byte, start_byte + new_byte, @@ -843,22 +753,24 @@ function LanguageTree:_on_bytes( start_row + new_row, new_end_col ) - end, true) + end - self:_do_callback( - 'bytes', - bufnr, - changed_tick, - start_row, - start_col, - start_byte, - old_row, - old_col, - old_byte, - new_row, - new_col, - new_byte - ) + if not self._is_child then + self:_do_callback( + 'bytes', + bufnr, + changed_tick, + start_row, + start_col, + start_byte, + old_row, + old_col, + old_byte, + new_row, + new_col, + new_byte + ) + end end ---@private diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index e7cf42283d..59894cc7f5 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -277,7 +277,6 @@ end ---@return (string[]|string|nil) function M.get_node_text(node, source, opts) opts = opts or {} - -- TODO(lewis6991): concat only works when source is number. local concat = vim.F.if_nil(opts.concat, true) local metadata = opts.metadata or {} -- cgit From ae263aff9547b8b513c4fedaceb4cbf93c57b866 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Thu, 9 Mar 2023 16:09:39 +0000 Subject: refactor(treesitter): use byte ranges from treesitter (#22589) --- runtime/lua/vim/treesitter/_meta.lua | 7 +- runtime/lua/vim/treesitter/_range.lua | 35 ++- runtime/lua/vim/treesitter/languagetree.lua | 440 +++++++++++++++++----------- runtime/lua/vim/treesitter/query.lua | 1 + 4 files changed, 305 insertions(+), 178 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/_meta.lua b/runtime/lua/vim/treesitter/_meta.lua index ad0854706b..72823ccf26 100644 --- a/runtime/lua/vim/treesitter/_meta.lua +++ b/runtime/lua/vim/treesitter/_meta.lua @@ -3,7 +3,7 @@ ---@class TSNode ---@field id fun(self: TSNode): integer ---@field tree fun(self: TSNode): TSTree ----@field range fun(self: TSNode): integer, integer, integer, integer +---@field range fun(self: TSNode, include_bytes: boolean?): integer, integer, integer, integer, integer, integer ---@field start fun(self: TSNode): integer, integer, integer ---@field end_ fun(self: TSNode): integer, integer, integer ---@field type fun(self: TSNode): string @@ -43,9 +43,9 @@ function TSNode:_rawquery(query, captures, start, end_) end function TSNode:_rawquery(query, captures, start, end_) end ---@class TSParser ----@field parse fun(self: TSParser, tree, source: integer|string): TSTree, Range4[] +---@field parse fun(self: TSParser, tree: TSTree?, source: integer|string, include_bytes: boolean?): TSTree, integer[] ---@field reset fun(self: TSParser) ----@field included_ranges fun(self: TSParser): Range4[] +---@field included_ranges fun(self: TSParser, include_bytes: boolean?): integer[] ---@field set_included_ranges fun(self: TSParser, ranges: Range6[]) ---@field set_timeout fun(self: TSParser, timeout: integer) ---@field timeout fun(self: TSParser): integer @@ -54,6 +54,7 @@ function TSNode:_rawquery(query, captures, start, end_) end ---@field root fun(self: TSTree): TSNode ---@field edit fun(self: TSTree, _: integer, _: integer, _: integer, _: integer, _: integer, _: integer, _: integer, _: integer, _:integer) ---@field copy fun(self: TSTree): TSTree +---@field included_ranges fun(self: TSTree, include_bytes: boolean?): integer[] ---@return integer vim._ts_get_language_version = function() end diff --git a/runtime/lua/vim/treesitter/_range.lua b/runtime/lua/vim/treesitter/_range.lua index bec24a23a5..02918da23f 100644 --- a/runtime/lua/vim/treesitter/_range.lua +++ b/runtime/lua/vim/treesitter/_range.lua @@ -78,11 +78,8 @@ end ---@param r2 Range4|Range6 ---@return boolean function M.intercepts(r1, r2) - local off_1 = #r1 == 6 and 1 or 0 - local off_2 = #r1 == 6 and 1 or 0 - - local srow_1, scol_1, erow_1, ecol_1 = r1[1], r1[2], r1[3 + off_1], r1[4 + off_1] - local srow_2, scol_2, erow_2, ecol_2 = r2[1], r2[2], r2[3 + off_2], r2[4 + off_2] + local srow_1, scol_1, erow_1, ecol_1 = M.unpack4(r1) + local srow_2, scol_2, erow_2, ecol_2 = M.unpack4(r2) -- r1 is above r2 if M.cmp_pos.le(erow_1, ecol_1, srow_2, scol_2) then @@ -97,16 +94,28 @@ function M.intercepts(r1, r2) return true end +---@private +---@param r Range4|Range6 +---@return integer, integer, integer, integer +function M.unpack4(r) + local off_1 = #r == 6 and 1 or 0 + return r[1], r[2], r[3 + off_1], r[4 + off_1] +end + +---@private +---@param r Range6 +---@return integer, integer, integer, integer, integer, integer +function M.unpack6(r) + return r[1], r[2], r[3], r[4], r[5], r[6] +end + ---@private ---@param r1 Range4|Range6 ---@param r2 Range4|Range6 ---@return boolean whether r1 contains r2 function M.contains(r1, r2) - local off_1 = #r1 == 6 and 1 or 0 - local off_2 = #r1 == 6 and 1 or 0 - - local srow_1, scol_1, erow_1, ecol_1 = r1[1], r1[2], r1[3 + off_1], r1[4 + off_1] - local srow_2, scol_2, erow_2, ecol_2 = r2[1], r2[2], r2[3 + off_2], r2[4 + off_2] + local srow_1, scol_1, erow_1, ecol_1 = M.unpack4(r1) + local srow_2, scol_2, erow_2, ecol_2 = M.unpack4(r2) -- start doesn't fit if M.cmp_pos.gt(srow_1, scol_1, srow_2, scol_2) then @@ -123,9 +132,13 @@ end ---@private ---@param source integer|string ----@param range Range4 +---@param range Range4|Range6 ---@return Range6 function M.add_bytes(source, range) + if type(range) == 'table' and #range == 6 then + return range --[[@as Range6]] + end + local start_row, start_col, end_row, end_col = range[1], range[2], range[3], range[4] local start_byte = 0 local end_byte = 0 diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index fbc602486b..c89419085f 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -62,8 +62,6 @@ local Range = require('vim.treesitter._range') ---@field private _source (integer|string) Buffer or string to parse ---@field private _trees TSTree[] Reference to parsed tree (one for each language) ---@field private _valid boolean|table If the parsed tree is valid ---- TODO(lewis6991): combine _regions, _valid and _trees ----@field private _is_child boolean local LanguageTree = {} ---@class LanguageTreeOpts @@ -117,6 +115,48 @@ function LanguageTree.new(source, lang, opts) return self end +---@private +---Measure execution time of a function +---@generic R1, R2, R3 +---@param f fun(): R1, R2, R2 +---@return integer, R1, R2, R3 +local function tcall(f, ...) + local start = vim.loop.hrtime() + ---@diagnostic disable-next-line + local r = { f(...) } + local duration = (vim.loop.hrtime() - start) / 1000000 + return duration, unpack(r) +end + +---@private +---@vararg any +function LanguageTree:_log(...) + if vim.g.__ts_debug == nil then + return + end + + local args = { ... } + if type(args[1]) == 'function' then + args = { args[1]() } + end + + local info = debug.getinfo(2, 'nl') + local nregions = #self:included_regions() + local prefix = + string.format('%s:%d: [%s:%d] ', info.name, info.currentline, self:lang(), nregions) + + a.nvim_out_write(prefix) + for _, x in ipairs(args) do + if type(x) == 'string' then + a.nvim_out_write(x) + else + a.nvim_out_write(vim.inspect(x, { newline = ' ', indent = '' })) + end + a.nvim_out_write(' ') + end + a.nvim_out_write('\n') +end + --- Invalidates this parser and all its children ---@param reload boolean|nil function LanguageTree:invalidate(reload) @@ -146,7 +186,9 @@ end --- Determines whether this tree is valid. --- If the tree is invalid, call `parse()`. --- This will return the updated tree. -function LanguageTree:is_valid() +---@param exclude_children boolean|nil +---@return boolean +function LanguageTree:is_valid(exclude_children) local valid = self._valid if type(valid) == 'table' then @@ -155,9 +197,18 @@ function LanguageTree:is_valid() return false end end - return true end + if not exclude_children then + for _, child in pairs(self._children) do + if not child:is_valid(exclude_children) then + return false + end + end + end + + assert(type(valid) == 'boolean') + return valid end @@ -171,16 +222,6 @@ function LanguageTree:source() return self._source end ----@private ----This is only exposed so it can be wrapped for profiling ----@param old_tree TSTree ----@return TSTree, integer[] -function LanguageTree:_parse_tree(old_tree) - local tree, tree_changes = self._parser:parse(old_tree, self._source) - self:_do_callback('changedtree', tree_changes, tree) - return tree, tree_changes -end - --- Parses all defined regions using a treesitter parser --- for the language this tree represents. --- This will run the injection query for this language to @@ -190,31 +231,45 @@ end ---@return table|nil Change list function LanguageTree:parse() if self:is_valid() then + self:_log('valid') return self._trees end local changes = {} - -- If there are no ranges, set to an empty list - -- so the included ranges in the parser are cleared. - if #self._regions > 0 then - for i, ranges in ipairs(self._regions) do - if not self._valid or not self._valid[i] then - self._parser:set_included_ranges(ranges) - local tree, tree_changes = self:_parse_tree(self._trees[i]) - self._trees[i] = tree - vim.list_extend(changes, tree_changes) + -- Collect some stats + local regions_parsed = 0 + local total_parse_time = 0 + + --- At least 1 region is invalid + if not self:is_valid(true) then + local function _parsetree(index) + local parse_time, tree, tree_changes = + tcall(self._parser.parse, self._parser, self._trees[index], self._source) + + self:_do_callback('changedtree', tree_changes, tree) + self._trees[index] = tree + vim.list_extend(changes, tree_changes) + + total_parse_time = total_parse_time + parse_time + regions_parsed = regions_parsed + 1 + end + + if #self._regions > 0 then + for i, ranges in ipairs(self._regions) do + if not self._valid or not self._valid[i] then + self._parser:set_included_ranges(ranges) + _parsetree(i) + end end + else + _parsetree(1) end - else - local tree, tree_changes = self:_parse_tree(self._trees[1]) - self._trees = { tree } - changes = tree_changes end - local injections_by_lang = self:_get_injections() local seen_langs = {} ---@type table + local query_time, injections_by_lang = tcall(self._get_injections, self) for lang, injection_ranges in pairs(injections_by_lang) do local has_lang = pcall(language.add, lang) @@ -229,15 +284,6 @@ function LanguageTree:parse() end child:set_included_regions(injection_ranges) - - local _, child_changes = child:parse() - - -- Propagate any child changes so they are included in the - -- the change list for the callback. - if child_changes then - vim.list_extend(changes, child_changes) - end - seen_langs[lang] = true end end @@ -248,6 +294,23 @@ function LanguageTree:parse() end end + self:_log({ + changes = changes, + regions_parsed = regions_parsed, + parse_time = total_parse_time, + query_time = query_time, + }) + + self:for_each_child(function(child) + local _, child_changes = child:parse() + + -- Propagate any child changes so they are included in the + -- the change list for the callback. + if child_changes then + vim.list_extend(changes, child_changes) + end + end) + self._valid = true return self._trees, changes @@ -295,8 +358,6 @@ function LanguageTree:add_child(lang) end self._children[lang] = LanguageTree.new(self._source, lang, self._opts) - self._children[lang]._is_child = true - self:invalidate() self:_do_callback('child_added', self._children[lang]) @@ -331,6 +392,54 @@ function LanguageTree:destroy() end end +---@private +---@param region Range6[] +local function region_tostr(region) + local srow, scol = region[1][1], region[1][2] + local erow, ecol = region[#region][4], region[#region][5] + return string.format('[%d:%d-%d:%d]', srow, scol, erow, ecol) +end + +---@private +---Iterate through all the regions. fn returns a boolean to indicate if the +---region is valid or not. +---@param fn fun(index: integer, region: Range6[]): boolean +function LanguageTree:_iter_regions(fn) + if not self._valid then + return + end + + if type(self._valid) ~= 'table' then + self._valid = {} + end + + local all_valid = true + + for i, region in ipairs(self._regions) do + if self._valid[i] == nil then + self._valid[i] = true + end + + if self._valid[i] then + self._valid[i] = fn(i, region) + if not self._valid[i] then + self:_log(function() + return 'invalidating region', i, region_tostr(region) + end) + end + end + + if not self._valid[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 +end + --- Sets the included regions that should be parsed by this |LanguageTree|. --- A region is a set of nodes and/or ranges that will be parsed in the same context. --- @@ -346,10 +455,10 @@ end --- nodes, which is useful for templating languages like ERB and EJS. --- ---@private ----@param regions Range4[][] List of regions this tree should manage and parse. -function LanguageTree:set_included_regions(regions) +---@param new_regions Range4[][] List of regions this tree should manage and parse. +function LanguageTree:set_included_regions(new_regions) -- Transform the tables from 4 element long to 6 element long (with byte offset) - for _, region in ipairs(regions) do + for _, region in ipairs(new_regions) do for i, range in ipairs(region) do if type(range) == 'table' and #range == 4 then region[i] = Range.add_bytes(self._source, range) @@ -357,83 +466,75 @@ function LanguageTree:set_included_regions(regions) end end - if #self._regions ~= #regions then + if #self._regions ~= #new_regions then self._trees = {} self:invalidate() - elseif self._valid ~= false then - if self._valid == true then - self._valid = {} - for i = 1, #regions do - self._valid[i] = true - end - end - - for i = 1, #regions do - if not vim.deep_equal(self._regions[i], regions[i]) then - self._valid[i] = false - end - - if not self._valid[i] then - self._trees[i] = nil - end - end + else + self:_iter_regions(function(i, region) + return vim.deep_equal(new_regions[i], region) + end) end - - self._regions = regions + self._regions = new_regions end ---- Gets the set of included regions +---Gets the set of included regions +---@return integer[][] function LanguageTree:included_regions() return self._regions end ---@private ---@param node TSNode ----@param id integer +---@param source integer|string ---@param metadata TSMetadata ----@return Range4 -local function get_range_from_metadata(node, id, metadata) - if metadata[id] and metadata[id].range then - return metadata[id].range --[[@as Range4]] +---@return Range6 +local function get_range_from_metadata(node, source, metadata) + if metadata and metadata.range then + return Range.add_bytes(source, metadata.range --[[@as Range4|Range6]]) end - return { node:range() } + return { node:range(true) } end ---@private --- TODO(lewis6991): cleanup of the node_range interface ---@param node TSNode ----@param id integer +---@param source string|integer ---@param metadata TSMetadata ----@return Range4[] -local function get_node_ranges(node, id, metadata, include_children) - local range = get_range_from_metadata(node, id, metadata) +---@return Range6[] +local function get_node_ranges(node, source, metadata, include_children) + local range = get_range_from_metadata(node, source, metadata) if include_children then return { range } end - local ranges = {} ---@type Range4[] + local ranges = {} ---@type Range6[] - local srow, scol, erow, ecol = range[1], range[2], range[3], range[4] + local srow, scol, sbyte, erow, ecol, ebyte = Range.unpack6(range) for i = 0, node:named_child_count() - 1 do local child = node:named_child(i) - local child_srow, child_scol, child_erow, child_ecol = child:range() - if child_srow > srow or child_scol > scol then - table.insert(ranges, { srow, scol, child_srow, child_scol }) + local c_srow, c_scol, c_sbyte, c_erow, c_ecol, c_ebyte = child:range(true) + if c_srow > srow or c_scol > scol then + ranges[#ranges + 1] = { srow, scol, sbyte, c_srow, c_scol, c_sbyte } end - srow = child_erow - scol = child_ecol + srow = c_erow + scol = c_ecol + sbyte = c_ebyte end if erow > srow or ecol > scol then - table.insert(ranges, { srow, scol, erow, ecol }) + ranges[#ranges + 1] = Range.add_bytes(source, { srow, scol, sbyte, erow, ecol, ebyte }) end return ranges end ----@alias TSInjection table> +---@class TSInjectionElem +---@field combined boolean +---@field regions Range6[][] + +---@alias TSInjection table> ---@private ---@param t table @@ -498,7 +599,7 @@ function LanguageTree:_get_injection(match, metadata) if name == 'injection.language' then lang = get_node_text(node, self._source, metadata[id]) elseif name == 'injection.content' then - ranges = get_node_ranges(node, id, metadata, include_children) + ranges = get_node_ranges(node, self._source, metadata[id], include_children) end end @@ -545,7 +646,7 @@ function LanguageTree:_get_injection_deprecated(match, metadata) elseif name == 'combined' then combined = true elseif name == 'content' and #ranges == 0 then - table.insert(ranges, get_range_from_metadata(node, id, metadata)) + ranges[#ranges + 1] = get_range_from_metadata(node, self._source, metadata[id]) -- Ignore any tags that start with "_" -- Allows for other tags to be used in matches elseif string.sub(name, 1, 1) ~= '_' then @@ -554,7 +655,7 @@ function LanguageTree:_get_injection_deprecated(match, metadata) end if #ranges == 0 then - table.insert(ranges, get_range_from_metadata(node, id, metadata)) + ranges[#ranges + 1] = get_range_from_metadata(node, self._source, metadata[id]) end end end @@ -569,7 +670,7 @@ end --- TODO: Allow for an offset predicate to tailor the injection range --- instead of using the entire nodes range. ---@private ----@return table +---@return table function LanguageTree:_get_injections() if not self._injection_query then return {} @@ -594,7 +695,7 @@ function LanguageTree:_get_injections() end end - ---@type table + ---@type table local result = {} -- Generate a map by lang of node lists. @@ -613,7 +714,6 @@ function LanguageTree:_get_injections() end, entry.regions) table.insert(result[lang], regions) else - ---@diagnostic disable-next-line:no-unknown for _, ranges in ipairs(entry.regions) do table.insert(result[lang], ranges) end @@ -634,30 +734,68 @@ function LanguageTree:_do_callback(cb_name, ...) end ---@private ----@param regions Range6[][] ----@param old_range Range6 ----@param new_range Range6 ----@return table region indices to invalidate -local function update_regions(regions, old_range, new_range) - ---@type table - local valid = {} - - for i, ranges in ipairs(regions or {}) do - valid[i] = true - for j, r in ipairs(ranges) do - if Range.intercepts(r, old_range) then - valid[i] = false - break +function LanguageTree:_edit( + start_byte, + end_byte_old, + end_byte_new, + start_row, + start_col, + end_row_old, + end_col_old, + end_row_new, + end_col_new +) + for _, tree in ipairs(self._trees) do + tree:edit( + start_byte, + end_byte_old, + end_byte_new, + start_row, + start_col, + end_row_old, + end_col_old, + end_row_new, + end_col_new + ) + end + + local changed_range = { + start_row, + start_col, + start_byte, + end_row_old, + end_col_old, + end_byte_old, + } + + local new_range = { + start_row, + start_col, + start_byte, + end_row_new, + end_col_new, + end_byte_new, + } + + if #self._regions == 0 then + self._valid = false + end + + -- Validate regions after editing the tree + self:_iter_regions(function(_, region) + for i, r in ipairs(region) do + if Range.intercepts(r, changed_range) then + return false end -- Range after change. Adjust - if Range.cmp_pos.gt(r[1], r[2], old_range[4], old_range[5]) then - local byte_offset = new_range[6] - old_range[6] - local row_offset = new_range[4] - old_range[4] + if Range.cmp_pos.gt(r[1], r[2], changed_range[4], changed_range[5]) then + local byte_offset = new_range[6] - changed_range[6] + local row_offset = new_range[4] - changed_range[4] -- Update the range to avoid invalidation in set_included_regions() -- which will compare the regions against the parsed injection regions - ranges[j] = { + region[i] = { r[1] + row_offset, r[2], r[3] + byte_offset, @@ -667,9 +805,8 @@ local function update_regions(regions, old_range, new_range) } end end - end - - return valid + return true + end) end ---@private @@ -700,49 +837,26 @@ function LanguageTree:_on_bytes( local old_end_col = old_col + ((old_row == 0) and start_col or 0) local new_end_col = new_col + ((new_row == 0) and start_col or 0) - local old_range = { - start_row, - start_col, - start_byte, - start_row + old_row, - old_end_col, - start_byte + old_byte, - } - - local new_range = { + self:_log( + 'on_bytes', + bufnr, + changed_tick, start_row, start_col, start_byte, - start_row + new_row, - new_end_col, - start_byte + new_byte, - } - - if #self._regions == 0 then - self._valid = false - else - self._valid = update_regions(self._regions, old_range, new_range) - end - - for _, child in pairs(self._children) do - child:_on_bytes( - bufnr, - changed_tick, - start_row, - start_col, - start_byte, - old_row, - old_col, - old_byte, - new_row, - new_col, - new_byte - ) - end + old_row, + old_col, + old_byte, + new_row, + new_col, + new_byte + ) -- Edit trees together BEFORE emitting a bytes callback. - for _, tree in ipairs(self._trees) do - tree:edit( + ---@private + self:for_each_child(function(child) + ---@diagnostic disable-next-line:invisible + child:_edit( start_byte, start_byte + old_byte, start_byte + new_byte, @@ -753,24 +867,22 @@ function LanguageTree:_on_bytes( start_row + new_row, new_end_col ) - end + end, true) - if not self._is_child then - self:_do_callback( - 'bytes', - bufnr, - changed_tick, - start_row, - start_col, - start_byte, - old_row, - old_col, - old_byte, - new_row, - new_col, - new_byte - ) - end + self:_do_callback( + 'bytes', + bufnr, + changed_tick, + start_row, + start_col, + start_byte, + old_row, + old_col, + old_byte, + new_row, + new_col, + new_byte + ) end ---@private diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index 59894cc7f5..e7cf42283d 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -277,6 +277,7 @@ end ---@return (string[]|string|nil) function M.get_node_text(node, source, opts) opts = opts or {} + -- TODO(lewis6991): concat only works when source is number. local concat = vim.F.if_nil(opts.concat, true) local metadata = opts.metadata or {} -- cgit From adfa9de8ebc4bce96d212280eccddc0306d1b013 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Fri, 10 Mar 2023 10:12:57 +0000 Subject: fix(treesitter): do not error on empty filetype Ignore instead --- runtime/lua/vim/treesitter/language.lua | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/language.lua b/runtime/lua/vim/treesitter/language.lua index 5f34d9cd56..47375fd5e6 100644 --- a/runtime/lua/vim/treesitter/language.lua +++ b/runtime/lua/vim/treesitter/language.lua @@ -60,16 +60,6 @@ function M.add(lang, opts) filetype = { filetype, { 'string', 'table' }, true }, }) - if filetype == '' then - error(string.format("'%s' is not a valid filetype", filetype)) - elseif type(filetype) == 'table' then - for _, f in ipairs(filetype) do - if f == '' then - error(string.format("'%s' is not a valid filetype", filetype)) - end - end - end - M.register(lang, filetype or lang) if vim._ts_has_language(lang) then @@ -109,7 +99,9 @@ function M.register(lang, filetype) end for _, f in ipairs(filetypes) do - ft_to_lang[f] = lang + if f ~= '' then + ft_to_lang[f] = lang + end end end -- cgit From 46b73bf22cb951151de9bf0712d42e194000b677 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Thu, 9 Mar 2023 15:28:55 +0000 Subject: perf(treesitter): more efficient foldexpr --- runtime/lua/vim/treesitter/_fold.lua | 290 +++++++++++++++++++++++------------ 1 file changed, 188 insertions(+), 102 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/_fold.lua b/runtime/lua/vim/treesitter/_fold.lua index a66cc6d543..435cb9fdb6 100644 --- a/runtime/lua/vim/treesitter/_fold.lua +++ b/runtime/lua/vim/treesitter/_fold.lua @@ -1,139 +1,157 @@ +local Range = require('vim.treesitter._range') + local api = vim.api -local M = {} +---@class FoldInfo +---@field levels table +---@field levels0 table +---@field private start_counts table +---@field private stop_counts table +local FoldInfo = {} +FoldInfo.__index = FoldInfo ---- Memoizes a function based on the buffer tick of the provided bufnr. ---- The cache entry is cleared when the buffer is detached to avoid memory leaks. ----@generic F: function ----@param fn F fn to memoize, taking the bufnr as first argument ----@return F -local function memoize_by_changedtick(fn) - ---@type table - local cache = {} - - ---@param bufnr integer - return function(bufnr, ...) - local tick = api.nvim_buf_get_changedtick(bufnr) - - if cache[bufnr] then - if cache[bufnr].last_tick == tick then - return cache[bufnr].result - end - else - local function detach_handler() - cache[bufnr] = nil - end +function FoldInfo.new() + return setmetatable({ + start_counts = {}, + stop_counts = {}, + levels0 = {}, + levels = {}, + }, FoldInfo) +end - -- Clean up logic only! - api.nvim_buf_attach(bufnr, false, { - on_detach = detach_handler, - on_reload = detach_handler, - }) - end +---@param srow integer +---@param erow integer +function FoldInfo:invalidate_range(srow, erow) + for i = srow, erow do + self.start_counts[i + 1] = nil + self.stop_counts[i + 1] = nil + self.levels0[i + 1] = nil + self.levels[i + 1] = nil + end +end - cache[bufnr] = { - result = fn(bufnr, ...), - last_tick = tick, - } +---@param srow integer +---@param erow integer +function FoldInfo:remove_range(srow, erow) + for i = erow - 1, srow, -1 do + table.remove(self.levels, i + 1) + table.remove(self.levels0, i + 1) + table.remove(self.start_counts, i + 1) + table.remove(self.stop_counts, i + 1) + end +end - return cache[bufnr].result +---@param srow integer +---@param erow integer +function FoldInfo:add_range(srow, erow) + for i = srow, erow - 1 do + table.insert(self.levels, i + 1, '-1') + table.insert(self.levels0, i + 1, -1) + table.insert(self.start_counts, i + 1, nil) + table.insert(self.stop_counts, i + 1, nil) end end ----@param bufnr integer ----@param capture string ----@param query_name string ----@param callback fun(id: integer, node:TSNode, metadata: TSMetadata) -local function iter_matches_with_capture(bufnr, capture, query_name, callback) - local parser = vim.treesitter.get_parser(bufnr) +---@param lnum integer +function FoldInfo:add_start(lnum) + self.start_counts[lnum] = (self.start_counts[lnum] or 0) + 1 +end - if not parser then - return - end +---@param lnum integer +function FoldInfo:add_stop(lnum) + self.stop_counts[lnum] = (self.stop_counts[lnum] or 0) + 1 +end - parser:for_each_tree(function(tree, lang_tree) - local lang = lang_tree:lang() - local query = vim.treesitter.query.get_query(lang, query_name) - if query then - local root = tree:root() - local start, _, stop = root:range() - for _, match, metadata in query:iter_matches(root, bufnr, start, stop) do - for id, node in pairs(match) do - if query.captures[id] == capture then - callback(id, node, metadata) - end - end - end - end - end) +---@param lnum integer +---@return integer +function FoldInfo:get_start(lnum) + return self.start_counts[lnum] or 0 +end + +---@param lnum integer +---@return integer +function FoldInfo:get_stop(lnum) + return self.stop_counts[lnum] or 0 end ---@private --- TODO(lewis6991): copied from languagetree.lua. Consolidate ---@param node TSNode ----@param id integer ---@param metadata TSMetadata ----@return Range -local function get_range_from_metadata(node, id, metadata) - if metadata[id] and metadata[id].range then - return metadata[id].range --[[@as Range]] +---@return Range4 +local function get_range_from_metadata(node, metadata) + if metadata and metadata.range then + return metadata.range --[[@as Range4]] end return { node:range() } end --- This is cached on buf tick to avoid computing that multiple times --- Especially not for every line in the file when `zx` is hit ----@param bufnr integer ----@return table -local folds_levels = memoize_by_changedtick(function(bufnr) +local function trim_level(level) local max_fold_level = vim.wo.foldnestmax - local function trim_level(level) - if level > max_fold_level then - return max_fold_level - end - return level + if level > max_fold_level then + return max_fold_level end + return level +end - -- start..stop is an inclusive range - local start_counts = {} ---@type table - local stop_counts = {} ---@type table +---@param bufnr integer +---@param info FoldInfo +---@param srow integer? +---@param erow integer? +local function get_folds_levels(bufnr, info, srow, erow) + srow = srow or 0 + erow = erow or api.nvim_buf_line_count(bufnr) + + info:invalidate_range(srow, erow) local prev_start = -1 local prev_stop = -1 - local min_fold_lines = vim.wo.foldminlines + vim.treesitter.get_parser(bufnr):for_each_tree(function(tree, ltree) + local query = vim.treesitter.query.get_query(ltree:lang(), 'folds') + if not query then + return + end + + -- erow in query is end-exclusive + local q_erow = erow and erow + 1 or -1 - iter_matches_with_capture(bufnr, 'fold', 'folds', function(id, node, metadata) - local range = get_range_from_metadata(node, id, metadata) - local start, stop, stop_col = range[1], range[3], range[4] + for id, node, metadata in query:iter_captures(tree:root(), bufnr, srow or 0, q_erow) do + if query.captures[id] == 'fold' then + local range = get_range_from_metadata(node, metadata[id]) + local start, _, stop, stop_col = Range.unpack4(range) - if stop_col == 0 then - stop = stop - 1 - end + if stop_col == 0 then + stop = stop - 1 + end - local fold_length = stop - start + 1 + 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 > min_fold_lines and not (start == prev_start and stop == prev_stop) then - start_counts[start] = (start_counts[start] or 0) + 1 - stop_counts[stop] = (stop_counts[stop] or 0) + 1 - prev_start = start - prev_stop = stop + -- 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 + info:add_start(start + 1) + info:add_stop(stop + 1) + prev_start = start + prev_stop = stop + end + end end end) - ---@type table - local levels = {} - local current_level = 0 + local current_level = info.levels0[srow] or 0 -- We now have the list of fold opening and closing, fill the gaps and mark where fold start - for lnum = 0, api.nvim_buf_line_count(bufnr) do + for lnum = srow + 1, erow + 1 do local last_trimmed_level = trim_level(current_level) - current_level = current_level + (start_counts[lnum] or 0) + current_level = current_level + info:get_start(lnum) + info.levels0[lnum] = current_level + local trimmed_level = trim_level(current_level) - current_level = current_level - (stop_counts[lnum] or 0) + current_level = current_level - info:get_stop(lnum) -- Determine if it's the start/end of a fold -- NB: vim's fold-expr interface does not have a mechanism to indicate that @@ -148,11 +166,61 @@ local folds_levels = memoize_by_changedtick(function(bufnr) prefix = '>' end - levels[lnum + 1] = prefix .. tostring(trimmed_level) + info.levels[lnum] = prefix .. tostring(trimmed_level) + end +end + +local M = {} + +---@type table +local foldinfos = {} + +local function recompute_folds() + if api.nvim_get_mode().mode == 'i' then + -- foldUpdate() is guarded in insert mode. So update folds on InsertLeave + api.nvim_create_autocmd('InsertLeave', { + once = true, + callback = vim._foldupdate, + }) + return end - return levels -end) + vim._foldupdate() +end + +---@param bufnr integer +---@param foldinfo FoldInfo +---@param tree_changes Range4[] +local function on_changedtree(bufnr, foldinfo, tree_changes) + -- For some reason, queries seem to use the old buffer state in on_bytes. + -- Get around this by scheduling and manually updating folds. + vim.schedule(function() + for _, change in ipairs(tree_changes) do + local srow, _, erow = Range.unpack4(change) + get_folds_levels(bufnr, foldinfo, srow, erow) + end + recompute_folds() + end) +end + +---@param bufnr integer +---@param foldinfo FoldInfo +---@param start_row integer +---@param old_row integer +---@param new_row integer +local function on_bytes(bufnr, foldinfo, start_row, old_row, new_row) + local end_row_old = start_row + old_row + local end_row_new = start_row + new_row + if new_row < old_row then + foldinfo:remove_range(end_row_old, end_row_new) + elseif new_row > old_row then + foldinfo:add_range(start_row, end_row_new) + vim.schedule(function() + get_folds_levels(bufnr, foldinfo, start_row, end_row_new) + recompute_folds() + end) + end +end ---@param lnum integer|nil ---@return string @@ -165,9 +233,27 @@ function M.foldexpr(lnum) return '0' end - local levels = folds_levels(bufnr) or {} + if not foldinfos[bufnr] then + foldinfos[bufnr] = FoldInfo.new() + get_folds_levels(bufnr, foldinfos[bufnr]) + + local parser = vim.treesitter.get_parser(bufnr) + parser:register_cbs({ + on_changedtree = function(tree_changes) + on_changedtree(bufnr, foldinfos[bufnr], tree_changes) + end, + + on_bytes = function(_, _, start_row, _, _, old_row, _, _, new_row, _, _) + on_bytes(bufnr, foldinfos[bufnr], start_row, old_row, new_row) + end, + + on_detach = function() + foldinfos[bufnr] = nil + end, + }) + end - return levels[lnum] or '0' + return foldinfos[bufnr].levels[lnum] or '0' end return M -- cgit From 762a06c6bcfbcc1e40ba670bae10bacdbb973524 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Fri, 10 Mar 2023 16:16:49 +0000 Subject: feat!(treesitter): do not return changes from LanguageTree:parse() Never return the changes an only notify them using the `on_changedtree` callback. It is not guaranteed for a plugin that it'll be the first one to call `tree:parse()` and thus get the changes. Closes #19915 --- runtime/lua/vim/treesitter/languagetree.lua | 11 ++--------- 1 file changed, 2 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 c89419085f..26321cd1f4 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -228,7 +228,6 @@ end --- determine if any child languages should be created. --- ---@return TSTree[] ----@return table|nil Change list function LanguageTree:parse() if self:is_valid() then self:_log('valid') @@ -302,18 +301,12 @@ function LanguageTree:parse() }) self:for_each_child(function(child) - local _, child_changes = child:parse() - - -- Propagate any child changes so they are included in the - -- the change list for the callback. - if child_changes then - vim.list_extend(changes, child_changes) - end + child:parse() end) self._valid = true - return self._trees, changes + return self._trees end --- Invokes the callback for each |LanguageTree| and its children recursively -- cgit From 9d70fe062ca01ac0673faa6ccbb88345916aeea7 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Fri, 10 Mar 2023 16:10:05 +0000 Subject: feat(treesitter)!: consolidate query util functions - And address more type errors. - Removed the `concat` option from `get_node_text` since it was applied inconsistently and made typing awkward. --- runtime/lua/vim/treesitter/_fold.lua | 15 +---- runtime/lua/vim/treesitter/_range.lua | 15 ++++- runtime/lua/vim/treesitter/languagetree.lua | 55 +++++----------- runtime/lua/vim/treesitter/query.lua | 97 ++++++++++++++--------------- 4 files changed, 79 insertions(+), 103 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/_fold.lua b/runtime/lua/vim/treesitter/_fold.lua index 435cb9fdb6..fd2c707d17 100644 --- a/runtime/lua/vim/treesitter/_fold.lua +++ b/runtime/lua/vim/treesitter/_fold.lua @@ -1,4 +1,5 @@ local Range = require('vim.treesitter._range') +local Query = require('vim.treesitter.query') local api = vim.api @@ -74,18 +75,6 @@ function FoldInfo:get_stop(lnum) return self.stop_counts[lnum] or 0 end ----@private ---- TODO(lewis6991): copied from languagetree.lua. Consolidate ----@param node TSNode ----@param metadata TSMetadata ----@return Range4 -local function get_range_from_metadata(node, metadata) - if metadata and metadata.range then - return metadata.range --[[@as Range4]] - end - return { node:range() } -end - local function trim_level(level) local max_fold_level = vim.wo.foldnestmax if level > max_fold_level then @@ -118,7 +107,7 @@ local function get_folds_levels(bufnr, info, srow, erow) for id, node, metadata in query:iter_captures(tree:root(), bufnr, srow or 0, q_erow) do if query.captures[id] == 'fold' then - local range = get_range_from_metadata(node, metadata[id]) + local range = Query.get_range(node, bufnr, metadata[id]) local start, _, stop, stop_col = Range.unpack4(range) if stop_col == 0 then diff --git a/runtime/lua/vim/treesitter/_range.lua b/runtime/lua/vim/treesitter/_range.lua index 02918da23f..0017a567ec 100644 --- a/runtime/lua/vim/treesitter/_range.lua +++ b/runtime/lua/vim/treesitter/_range.lua @@ -2,8 +2,19 @@ local api = vim.api local M = {} ----@alias Range4 {[1]: integer, [2]: integer, [3]: integer, [4]: integer} ----@alias Range6 {[1]: integer, [2]: integer, [3]: integer, [4]: integer, [5]: integer, [6]: integer} +---@class Range4 +---@field [1] integer start row +---@field [2] integer start column +---@field [3] integer end row +---@field [4] integer end column + +---@class Range6 +---@field [1] integer start row +---@field [2] integer start column +---@field [3] integer start bytes +---@field [4] integer end row +---@field [5] integer end column +---@field [6] integer end bytes ---@private ---@param a_row integer diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index c89419085f..0bb0601241 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -455,7 +455,7 @@ end --- nodes, which is useful for templating languages like ERB and EJS. --- ---@private ----@param new_regions Range4[][] List of regions this tree should manage and parse. +---@param new_regions Range6[][] List of regions this tree should manage and parse. function LanguageTree:set_included_regions(new_regions) -- Transform the tables from 4 element long to 6 element long (with byte offset) for _, region in ipairs(new_regions) do @@ -483,18 +483,6 @@ function LanguageTree:included_regions() return self._regions end ----@private ----@param node TSNode ----@param source integer|string ----@param metadata TSMetadata ----@return Range6 -local function get_range_from_metadata(node, source, metadata) - if metadata and metadata.range then - return Range.add_bytes(source, metadata.range --[[@as Range4|Range6]]) - end - return { node:range(true) } -end - ---@private --- TODO(lewis6991): cleanup of the node_range interface ---@param node TSNode @@ -502,7 +490,7 @@ end ---@param metadata TSMetadata ---@return Range6[] local function get_node_ranges(node, source, metadata, include_children) - local range = get_range_from_metadata(node, source, metadata) + local range = query.get_range(node, source, metadata) if include_children then return { range } @@ -565,31 +553,18 @@ local function add_injection(t, tree_index, pattern, lang, combined, ranges) table.insert(t[tree_index][lang][pattern].regions, ranges) end ----@private ----Get node text ---- ----Note: `query.get_node_text` returns string|string[]|nil so use this simple alias function ----to annotate it returns string. ---- ----TODO(lewis6991): use [at]overload annotations on `query.get_node_text` ----@param node TSNode ----@param source integer|string ----@param metadata table ----@return string -local function get_node_text(node, source, metadata) - return query.get_node_text(node, source, { metadata = metadata }) --[[@as string]] -end - ---@private --- Extract injections according to: --- https://tree-sitter.github.io/tree-sitter/syntax-highlighting#language-injection ---@param match table ----@param metadata table +---@param metadata TSMetadata ---@return string, boolean, Range4[] function LanguageTree:_get_injection(match, metadata) local ranges = {} ---@type Range4[] local combined = metadata['injection.combined'] ~= nil - local lang = metadata['injection.language'] ---@type string + local lang = metadata['injection.language'] + assert(type(lang) == 'string') + local include_children = metadata['injection.include-children'] ~= nil for id, node in pairs(match) do @@ -597,7 +572,7 @@ function LanguageTree:_get_injection(match, metadata) -- Lang should override any other language tag if name == 'injection.language' then - lang = get_node_text(node, self._source, metadata[id]) + lang = query.get_node_text(node, self._source, { metadata = metadata[id] }) elseif name == 'injection.content' then ranges = get_node_ranges(node, self._source, metadata[id], include_children) end @@ -608,11 +583,11 @@ end ---@private ---@param match table ----@param metadata table +---@param metadata TSMetadata ---@return string, boolean, Range4[] function LanguageTree:_get_injection_deprecated(match, metadata) local lang = nil ---@type string - local ranges = {} ---@type Range4[] + local ranges = {} ---@type Range6[] local combined = metadata.combined ~= nil -- Directives can configure how injections are captured as well as actual node captures. @@ -630,8 +605,10 @@ function LanguageTree:_get_injection_deprecated(match, metadata) end end - if metadata.language then - lang = metadata.language ---@type string + local mlang = metadata.language + if mlang ~= nil then + assert(type(mlang) == 'string') + lang = mlang end -- You can specify the content and language together @@ -642,11 +619,11 @@ function LanguageTree:_get_injection_deprecated(match, metadata) -- Lang should override any other language tag if name == 'language' and not lang then - lang = get_node_text(node, self._source, metadata[id]) + lang = query.get_node_text(node, self._source, { metadata = metadata[id] }) elseif name == 'combined' then combined = true elseif name == 'content' and #ranges == 0 then - ranges[#ranges + 1] = get_range_from_metadata(node, self._source, metadata[id]) + ranges[#ranges + 1] = query.get_range(node, self._source, metadata[id]) -- Ignore any tags that start with "_" -- Allows for other tags to be used in matches elseif string.sub(name, 1, 1) ~= '_' then @@ -655,7 +632,7 @@ function LanguageTree:_get_injection_deprecated(match, metadata) end if #ranges == 0 then - ranges[#ranges + 1] = get_range_from_metadata(node, self._source, metadata[id]) + ranges[#ranges + 1] = query.get_range(node, self._source, metadata[id]) end end end diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index e7cf42283d..70af4f7bce 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -1,6 +1,8 @@ local a = vim.api local language = require('vim.treesitter.language') +local Range = require('vim.treesitter._range') + ---@class Query ---@field captures string[] List of captures used in query ---@field info TSQueryInfo Contains used queries, predicates, directives @@ -56,35 +58,13 @@ local function add_included_lang(base_langs, lang, ilang) end ---@private ----@param buf (integer) ----@param range (table) ----@param concat (boolean) ----@returns (string[]|string|nil) -local function buf_range_get_text(buf, range, concat) - local lines - local start_row, start_col, end_row, end_col = unpack(range) - local eof_row = a.nvim_buf_line_count(buf) - if start_row >= eof_row then - return nil - end - - if end_col == 0 then - lines = a.nvim_buf_get_lines(buf, start_row, end_row, true) - end_col = -1 - else - lines = a.nvim_buf_get_lines(buf, start_row, end_row + 1, true) - end - - if #lines > 0 then - if #lines == 1 then - lines[1] = string.sub(lines[1], start_col + 1, end_col) - else - lines[1] = string.sub(lines[1], start_col + 1) - lines[#lines] = string.sub(lines[#lines], 1, end_col) - end - end - - return concat and table.concat(lines, '\n') or lines +---@param buf integer +---@param range Range6 +---@returns string +local function buf_range_get_text(buf, range) + local start_row, start_col, end_row, end_col = Range.unpack4(range) + local lines = a.nvim_buf_get_text(buf, start_row, start_col, end_row, end_col, {}) + return table.concat(lines, '\n') end --- Gets the list of files used to make up a query @@ -256,14 +236,28 @@ function M.parse_query(lang, query) local cached = query_cache[lang][query] if cached then return cached - else - local self = setmetatable({}, Query) - self.query = vim._ts_parse_query(lang, query) - self.info = self.query:inspect() - self.captures = self.info.captures - query_cache[lang][query] = self - return self end + + local self = setmetatable({}, Query) + self.query = vim._ts_parse_query(lang, query) + self.info = self.query:inspect() + self.captures = self.info.captures + query_cache[lang][query] = self + return self +end + +---Get the range of a |TSNode|. Can also supply {source} and {metadata} +---to get the range with directives applied. +---@param node TSNode +---@param source integer|string|nil Buffer or string from which the {node} is extracted +---@param metadata TSMetadata|nil +---@return Range6 +function M.get_range(node, source, metadata) + if metadata and metadata.range then + assert(source) + return Range.add_bytes(source, metadata.range) + end + return { node:range(true) } end --- Gets the text corresponding to a given node @@ -271,24 +265,22 @@ end ---@param node TSNode ---@param source (integer|string) Buffer or string from which the {node} is extracted ---@param opts (table|nil) Optional parameters. ---- - concat: (boolean) Concatenate result in a string (default true) --- - metadata (table) Metadata of a specific capture. This would be --- set to `metadata[capture_id]` when using |vim.treesitter.add_directive()|. ----@return (string[]|string|nil) +---@return string function M.get_node_text(node, source, opts) opts = opts or {} - -- TODO(lewis6991): concat only works when source is number. - local concat = vim.F.if_nil(opts.concat, true) local metadata = opts.metadata or {} if metadata.text then return metadata.text elseif type(source) == 'number' then - return metadata.range and buf_range_get_text(source, metadata.range, concat) - or buf_range_get_text(source, { node:range() }, concat) - elseif type(source) == 'string' then - return source:sub(select(3, node:start()) + 1, select(3, node:end_())) + local range = M.get_range(node, source, metadata) + return buf_range_get_text(source, range) end + + ---@cast source string + return source:sub(select(3, node:start()) + 1, select(3, node:end_())) end ---@alias TSMatch table @@ -312,7 +304,7 @@ local predicate_handlers = { str = predicate[3] else -- (#eq? @aa @bb) - str = M.get_node_text(match[predicate[3]], source) --[[@as string]] + str = M.get_node_text(match[predicate[3]], source) end if node_text ~= str or str == nil then @@ -328,7 +320,7 @@ local predicate_handlers = { return true end local regex = predicate[3] - return string.find(M.get_node_text(node, source) --[[@as string]], regex) ~= nil + return string.find(M.get_node_text(node, source), regex) ~= nil end, ['match?'] = (function() @@ -366,7 +358,7 @@ local predicate_handlers = { if not node then return true end - local node_text = M.get_node_text(node, source) --[[@as string]] + local node_text = M.get_node_text(node, source) for i = 3, #predicate do if string.find(node_text, predicate[i], 1, true) then @@ -404,9 +396,9 @@ local predicate_handlers = { predicate_handlers['vim-match?'] = predicate_handlers['match?'] ---@class TSMetadata +---@field range Range4|Range6 ---@field [integer] TSMetadata ---@field [string] integer|string ----@field range Range4 ---@alias TSDirective fun(match: TSMatch, _, _, predicate: (string|integer)[], metadata: TSMetadata) @@ -465,13 +457,20 @@ local directive_handlers = { assert(#pred == 4) local id = pred[2] + assert(type(id) == 'number') + local node = match[id] local text = M.get_node_text(node, bufnr, { metadata = metadata[id] }) or '' if not metadata[id] then metadata[id] = {} end - metadata[id].text = text:gsub(pred[3], pred[4]) + + local pattern, replacement = pred[3], pred[3] + assert(type(pattern) == 'string') + assert(type(replacement) == 'string') + + metadata[id].text = text:gsub(pattern, replacement) end, } -- cgit From 58bbc2ea0b3dfed13471e8cc0447d7598be24276 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Fri, 10 Mar 2023 16:40:27 +0000 Subject: refactor(treesitter): add Range type aliase for Range4|Range6 --- runtime/lua/vim/treesitter/_range.lua | 14 ++++++++------ runtime/lua/vim/treesitter/languagetree.lua | 15 ++++++--------- runtime/lua/vim/treesitter/query.lua | 12 ++++++++++-- 3 files changed, 24 insertions(+), 17 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/_range.lua b/runtime/lua/vim/treesitter/_range.lua index 0017a567ec..f4db5016ac 100644 --- a/runtime/lua/vim/treesitter/_range.lua +++ b/runtime/lua/vim/treesitter/_range.lua @@ -16,6 +16,8 @@ local M = {} ---@field [5] integer end column ---@field [6] integer end bytes +---@alias Range Range4|Range6 + ---@private ---@param a_row integer ---@param a_col integer @@ -85,8 +87,8 @@ function M.validate(r) end ---@private ----@param r1 Range4|Range6 ----@param r2 Range4|Range6 +---@param r1 Range +---@param r2 Range ---@return boolean function M.intercepts(r1, r2) local srow_1, scol_1, erow_1, ecol_1 = M.unpack4(r1) @@ -106,7 +108,7 @@ function M.intercepts(r1, r2) end ---@private ----@param r Range4|Range6 +---@param r Range ---@return integer, integer, integer, integer function M.unpack4(r) local off_1 = #r == 6 and 1 or 0 @@ -121,8 +123,8 @@ function M.unpack6(r) end ---@private ----@param r1 Range4|Range6 ----@param r2 Range4|Range6 +---@param r1 Range +---@param r2 Range ---@return boolean whether r1 contains r2 function M.contains(r1, r2) local srow_1, scol_1, erow_1, ecol_1 = M.unpack4(r1) @@ -143,7 +145,7 @@ end ---@private ---@param source integer|string ----@param range Range4|Range6 +---@param range Range ---@return Range6 function M.add_bytes(source, range) if type(range) == 'table' and #range == 6 then diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index 0bb0601241..6869fae92c 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -484,7 +484,6 @@ function LanguageTree:included_regions() end ---@private ---- TODO(lewis6991): cleanup of the node_range interface ---@param node TSNode ---@param source string|integer ---@param metadata TSMetadata @@ -530,7 +529,7 @@ end ---@param pattern integer ---@param lang string ---@param combined boolean ----@param ranges Range4[] +---@param ranges Range6[] local function add_injection(t, tree_index, pattern, lang, combined, ranges) assert(type(lang) == 'string') @@ -558,13 +557,11 @@ end --- https://tree-sitter.github.io/tree-sitter/syntax-highlighting#language-injection ---@param match table ---@param metadata TSMetadata ----@return string, boolean, Range4[] +---@return string?, boolean, Range6[] function LanguageTree:_get_injection(match, metadata) - local ranges = {} ---@type Range4[] + local ranges = {} ---@type Range6[] local combined = metadata['injection.combined'] ~= nil - local lang = metadata['injection.language'] - assert(type(lang) == 'string') - + local lang = metadata['injection.language'] --[[@as string?]] local include_children = metadata['injection.include-children'] ~= nil for id, node in pairs(match) do @@ -584,7 +581,7 @@ end ---@private ---@param match table ---@param metadata TSMetadata ----@return string, boolean, Range4[] +---@return string, boolean, Range6[] function LanguageTree:_get_injection_deprecated(match, metadata) local lang = nil ---@type string local ranges = {} ---@type Range6[] @@ -910,7 +907,7 @@ end ---@private ---@param tree TSTree ----@param range Range4 +---@param range Range ---@return boolean local function tree_contains(tree, range) return Range.contains({ tree:root():range() }, range) diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index 70af4f7bce..f4e038b2d8 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -59,10 +59,18 @@ end ---@private ---@param buf integer ----@param range Range6 +---@param range Range ---@returns string local function buf_range_get_text(buf, range) local start_row, start_col, end_row, end_col = Range.unpack4(range) + if end_col == 0 then + if start_row == end_row then + start_col = -1 + start_row = start_row - 1 + end + end_col = -1 + end_row = end_row - 1 + end local lines = a.nvim_buf_get_text(buf, start_row, start_col, end_row, end_col, {}) return table.concat(lines, '\n') end @@ -396,7 +404,7 @@ local predicate_handlers = { predicate_handlers['vim-match?'] = predicate_handlers['match?'] ---@class TSMetadata ----@field range Range4|Range6 +---@field range Range ---@field [integer] TSMetadata ---@field [string] integer|string -- cgit From 35799a6629f10cc49e79381e61038b3a4ca3bb23 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Mon, 13 Mar 2023 10:44:43 +0000 Subject: fix(treesitter): foldexpr (#22652) The ranges passed to foldinfo.remove_range were in the wrong order. --- runtime/lua/vim/treesitter/_fold.lua | 3 ++- 1 file changed, 2 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 fd2c707d17..90f4394fcc 100644 --- a/runtime/lua/vim/treesitter/_fold.lua +++ b/runtime/lua/vim/treesitter/_fold.lua @@ -200,8 +200,9 @@ end local function on_bytes(bufnr, foldinfo, start_row, old_row, new_row) local end_row_old = start_row + old_row local end_row_new = start_row + new_row + if new_row < old_row then - foldinfo:remove_range(end_row_old, end_row_new) + foldinfo:remove_range(end_row_new, end_row_old) elseif new_row > old_row then foldinfo:add_range(start_row, end_row_new) vim.schedule(function() -- cgit From 4e4203f71b0b9bb2ca4ad9abd2fbf4ea1deaf9a6 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Thu, 23 Mar 2023 11:23:51 +0000 Subject: fix(treesitter): annotations - Begin using `@package` in place of `@private` for functions that are accessed internally but outside their defined class. - Rename Node -> TSP.Node --- runtime/lua/vim/treesitter/_fold.lua | 20 ++++++++++++++------ runtime/lua/vim/treesitter/_meta.lua | 3 ++- runtime/lua/vim/treesitter/highlighter.lua | 23 ++++++++++------------- runtime/lua/vim/treesitter/languagetree.lua | 11 +++++------ runtime/lua/vim/treesitter/playground.lua | 26 +++++++++++++------------- 5 files changed, 44 insertions(+), 39 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/_fold.lua b/runtime/lua/vim/treesitter/_fold.lua index 90f4394fcc..3e67e400c2 100644 --- a/runtime/lua/vim/treesitter/_fold.lua +++ b/runtime/lua/vim/treesitter/_fold.lua @@ -3,7 +3,7 @@ local Query = require('vim.treesitter.query') local api = vim.api ----@class FoldInfo +---@class TS.FoldInfo ---@field levels table ---@field levels0 table ---@field private start_counts table @@ -11,6 +11,7 @@ local api = vim.api local FoldInfo = {} FoldInfo.__index = FoldInfo +---@private function FoldInfo.new() return setmetatable({ start_counts = {}, @@ -20,6 +21,7 @@ function FoldInfo.new() }, FoldInfo) end +---@package ---@param srow integer ---@param erow integer function FoldInfo:invalidate_range(srow, erow) @@ -31,6 +33,7 @@ function FoldInfo:invalidate_range(srow, erow) end end +---@package ---@param srow integer ---@param erow integer function FoldInfo:remove_range(srow, erow) @@ -42,6 +45,7 @@ function FoldInfo:remove_range(srow, erow) end end +---@package ---@param srow integer ---@param erow integer function FoldInfo:add_range(srow, erow) @@ -53,22 +57,26 @@ function FoldInfo:add_range(srow, erow) end end +---@package ---@param lnum integer function FoldInfo:add_start(lnum) self.start_counts[lnum] = (self.start_counts[lnum] or 0) + 1 end +---@package ---@param lnum integer function FoldInfo:add_stop(lnum) self.stop_counts[lnum] = (self.stop_counts[lnum] or 0) + 1 end +---@packag ---@param lnum integer ---@return integer function FoldInfo:get_start(lnum) return self.start_counts[lnum] or 0 end +---@package ---@param lnum integer ---@return integer function FoldInfo:get_stop(lnum) @@ -84,7 +92,7 @@ local function trim_level(level) end ---@param bufnr integer ----@param info FoldInfo +---@param info TS.FoldInfo ---@param srow integer? ---@param erow integer? local function get_folds_levels(bufnr, info, srow, erow) @@ -161,7 +169,7 @@ end local M = {} ----@type table +---@type table local foldinfos = {} local function recompute_folds() @@ -178,7 +186,7 @@ local function recompute_folds() end ---@param bufnr integer ----@param foldinfo FoldInfo +---@param foldinfo TS.FoldInfo ---@param tree_changes Range4[] local function on_changedtree(bufnr, foldinfo, tree_changes) -- For some reason, queries seem to use the old buffer state in on_bytes. @@ -193,7 +201,7 @@ local function on_changedtree(bufnr, foldinfo, tree_changes) end ---@param bufnr integer ----@param foldinfo FoldInfo +---@param foldinfo TS.FoldInfo ---@param start_row integer ---@param old_row integer ---@param new_row integer @@ -212,13 +220,13 @@ local function on_bytes(bufnr, foldinfo, start_row, old_row, new_row) end end +---@package ---@param lnum integer|nil ---@return string function M.foldexpr(lnum) lnum = lnum or vim.v.lnum local bufnr = api.nvim_get_current_buf() - ---@diagnostic disable-next-line:invisible if not vim.treesitter._has_parser(bufnr) or not lnum then return '0' end diff --git a/runtime/lua/vim/treesitter/_meta.lua b/runtime/lua/vim/treesitter/_meta.lua index 72823ccf26..4d0f43d030 100644 --- a/runtime/lua/vim/treesitter/_meta.lua +++ b/runtime/lua/vim/treesitter/_meta.lua @@ -3,7 +3,8 @@ ---@class TSNode ---@field id fun(self: TSNode): integer ---@field tree fun(self: TSNode): TSTree ----@field range fun(self: TSNode, include_bytes: boolean?): integer, integer, integer, integer, integer, integer +---@field range fun(self: TSNode, include_bytes: false?): integer, integer, integer, integer +---@field range fun(self: TSNode, include_bytes: true): integer, integer, integer, integer, integer, integer ---@field start fun(self: TSNode): integer, integer, integer ---@field end_ fun(self: TSNode): integer, integer, integer ---@field type fun(self: TSNode): string diff --git a/runtime/lua/vim/treesitter/highlighter.lua b/runtime/lua/vim/treesitter/highlighter.lua index e3deaf6ba6..e24b3ba5df 100644 --- a/runtime/lua/vim/treesitter/highlighter.lua +++ b/runtime/lua/vim/treesitter/highlighter.lua @@ -53,12 +53,12 @@ function TSHighlighterQuery.new(lang, query_string) return self end ----@private +---@package function TSHighlighterQuery:query() return self._query end ----@private +---@package --- --- Creates a highlighter for `tree`. --- @@ -76,16 +76,14 @@ function TSHighlighter.new(tree, opts) opts = opts or {} ---@type { queries: table } self.tree = tree tree:register_cbs({ - ---@diagnostic disable:invisible on_changedtree = function(...) self:on_changedtree(...) end, on_bytes = function(...) self:on_bytes(...) end, - on_detach = function(...) - ---@diagnostic disable-next-line:redundant-parameter - self:on_detach(...) + on_detach = function() + self:on_detach() end, }) @@ -147,7 +145,7 @@ function TSHighlighter:destroy() end end ----@private +---@package ---@param tstree TSTree ---@return TSHighlightState function TSHighlighter:get_highlight_state(tstree) @@ -166,19 +164,19 @@ function TSHighlighter:reset_highlight_state() self._highlight_states = {} end ----@private +---@package ---@param start_row integer ---@param new_end integer function TSHighlighter:on_bytes(_, _, start_row, _, _, _, _, _, new_end) a.nvim__buf_redraw_range(self.bufnr, start_row, start_row + new_end + 1) end ----@private +---@package function TSHighlighter:on_detach() self:destroy() end ----@private +---@package ---@param changes integer[][]? function TSHighlighter:on_changedtree(changes) for _, ch in ipairs(changes or {}) do @@ -188,7 +186,7 @@ end --- Gets the query used for @param lang -- ----@private +---@package ---@param lang string Language used by the highlighter. ---@return TSHighlighterQuery function TSHighlighter:get_query(lang) @@ -205,7 +203,6 @@ end ---@param line integer ---@param is_spell_nav boolean local function on_line_impl(self, buf, line, is_spell_nav) - ---@diagnostic disable:invisible self.tree:for_each_tree(function(tstree, tree) if not tstree then return @@ -268,7 +265,7 @@ local function on_line_impl(self, buf, line, is_spell_nav) state.next_row = start_row end end - end, true) + end) end ---@private diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index bdfe281a5b..ae41062ab2 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -70,7 +70,7 @@ local LanguageTree = {} LanguageTree.__index = LanguageTree ---- @private +--- @package --- --- |LanguageTree| contains a tree of parsers: the root treesitter parser for {lang} and any --- "injected" language parsers, which themselves may inject other languages, recursively. @@ -700,7 +700,7 @@ function LanguageTree:_do_callback(cb_name, ...) end end ----@private +---@package function LanguageTree:_edit( start_byte, end_byte_old, @@ -776,7 +776,7 @@ function LanguageTree:_edit( end) end ----@private +---@package ---@param bufnr integer ---@param changed_tick integer ---@param start_row integer @@ -822,7 +822,6 @@ function LanguageTree:_on_bytes( -- Edit trees together BEFORE emitting a bytes callback. ---@private self:for_each_child(function(child) - ---@diagnostic disable-next-line:invisible child:_edit( start_byte, start_byte + old_byte, @@ -852,12 +851,12 @@ function LanguageTree:_on_bytes( ) end ----@private +---@package function LanguageTree:_on_reload() self:invalidate(true) end ----@private +---@package function LanguageTree:_on_detach(...) self:invalidate(true) self:_do_callback('detach', ...) diff --git a/runtime/lua/vim/treesitter/playground.lua b/runtime/lua/vim/treesitter/playground.lua index 7f181c23fd..35f06f5caf 100644 --- a/runtime/lua/vim/treesitter/playground.lua +++ b/runtime/lua/vim/treesitter/playground.lua @@ -5,11 +5,11 @@ local api = vim.api ---@field opts table Options table with the following keys: --- - anon (boolean): If true, display anonymous nodes --- - lang (boolean): If true, display the language alongside each node ----@field nodes Node[] ----@field named Node[] +---@field nodes TSP.Node[] +---@field named TSP.Node[] local TSPlayground = {} ---- ----@class Node + +---@class TSP.Node ---@field id integer Node id ---@field text string Node text ---@field named boolean True if this is a named (non-anonymous) node @@ -36,9 +36,9 @@ local TSPlayground = {} ---@param node TSNode Starting node to begin traversal |tsnode| ---@param depth integer Current recursion depth ---@param lang string Language of the tree currently being traversed ----@param injections table Mapping of node ids to root nodes of injected language trees (see +---@param injections table Mapping of node ids to root nodes of injected language trees (see --- explanation above) ----@param tree Node[] Output table containing a list of tables each representing a node in the tree +---@param tree TSP.Node[] Output table containing a list of tables each representing a node in the tree ---@private local function traverse(node, depth, lang, injections, tree) local injection = injections[node:id()] @@ -87,7 +87,7 @@ end ---@return TSPlayground|nil ---@return string|nil Error message, if any --- ----@private +---@package function TSPlayground:new(bufnr, lang) local ok, parser = pcall(vim.treesitter.get_parser, bufnr or 0, lang) if not ok then @@ -114,7 +114,7 @@ function TSPlayground:new(bufnr, lang) local nodes = traverse(root, 0, parser:lang(), injections, {}) - local named = {} ---@type Node[] + local named = {} ---@type TSP.Node[] for _, v in ipairs(nodes) do if v.named then named[#named + 1] = v @@ -154,7 +154,7 @@ end --- Write the contents of this Playground into {bufnr}. --- ---@param bufnr integer Buffer number to write into. ----@private +---@package function TSPlayground:draw(bufnr) vim.bo[bufnr].modifiable = true local lines = {} ---@type string[] @@ -195,8 +195,8 @@ end --- The node number is dependent on whether or not anonymous nodes are displayed. --- ---@param i integer Node number to get ----@return Node ----@private +---@return TSP.Node +---@package function TSPlayground:get(i) local t = self.opts.anon and self.nodes or self.named return t[i] @@ -204,10 +204,10 @@ end --- Iterate over all of the nodes in this Playground object. --- ----@return (fun(): integer, Node) Iterator over all nodes in this Playground +---@return (fun(): integer, TSP.Node) Iterator over all nodes in this Playground ---@return table ---@return integer ----@private +---@package function TSPlayground:iter() return ipairs(self.opts.anon and self.nodes or self.named) end -- cgit From cbbf8bd666c8419fdab80a0887948c8a36279c19 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Fri, 24 Mar 2023 14:43:14 +0000 Subject: feat(treesitter)!: deprecate top level indexes to modules (#22761) The following top level Treesitter functions have been moved: - vim.treesitter.inspect_language() -> vim.treesitter.language.inspect() - vim.treesitter.get_query_files() -> vim.treesitter.query.get_files() - vim.treesitter.set_query() -> vim.treesitter.query.set() - vim.treesitter.query.set_query() -> vim.treesitter.query.set() - vim.treesitter.get_query() -> vim.treesitter.query.get() - vim.treesitter.query.get_query() -> vim.treesitter.query.get() - vim.treesitter.parse_query() -> vim.treesitter.query.parse() - vim.treesitter.query.parse_query() -> vim.treesitter.query.parse() - vim.treesitter.add_predicate() -> vim.treesitter.query.add_predicate() - vim.treesitter.add_directive() -> vim.treesitter.query.add_directive() - vim.treesitter.list_predicates() -> vim.treesitter.query.list_predicates() - vim.treesitter.list_directives() -> vim.treesitter.query.list_directives() - vim.treesitter.query.get_range() -> vim.treesitter.get_range() - vim.treesitter.query.get_node_text() -> vim.treesitter.get_node_text() --- runtime/lua/vim/treesitter/highlighter.lua | 6 +- runtime/lua/vim/treesitter/language.lua | 13 ++- runtime/lua/vim/treesitter/languagetree.lua | 14 ++-- runtime/lua/vim/treesitter/query.lua | 123 +++++++++++++--------------- 4 files changed, 77 insertions(+), 79 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/highlighter.lua b/runtime/lua/vim/treesitter/highlighter.lua index e24b3ba5df..729cd34090 100644 --- a/runtime/lua/vim/treesitter/highlighter.lua +++ b/runtime/lua/vim/treesitter/highlighter.lua @@ -1,5 +1,5 @@ local a = vim.api -local query = require('vim.treesitter.query') +local query = vim.treesitter.query ---@alias TSHlIter fun(): integer, TSNode, TSMetadata @@ -45,9 +45,9 @@ function TSHighlighterQuery.new(lang, query_string) }) if query_string then - self._query = query.parse_query(lang, query_string) + self._query = query.parse(lang, query_string) else - self._query = query.get_query(lang, 'highlights') + self._query = query.get(lang, 'highlights') end return self diff --git a/runtime/lua/vim/treesitter/language.lua b/runtime/lua/vim/treesitter/language.lua index 47375fd5e6..974d66ec05 100644 --- a/runtime/lua/vim/treesitter/language.lua +++ b/runtime/lua/vim/treesitter/language.lua @@ -1,5 +1,6 @@ local a = vim.api +---@class TSLanguageModule local M = {} ---@type table @@ -111,9 +112,19 @@ end --- ---@param lang string Language ---@return table -function M.inspect_language(lang) +function M.inspect(lang) M.add(lang) return vim._ts_inspect_language(lang) end +---@deprecated +function M.inspect_language(...) + vim.deprecate( + 'vim.treesitter.language.inspect_language()', + 'vim.treesitter.language.inspect()', + '0.10' + ) + return M.inspect(...) +end + return M diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index ae41062ab2..82e507551d 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -99,8 +99,8 @@ function LanguageTree.new(source, lang, opts) _regions = {}, _trees = {}, _opts = opts, - _injection_query = injections[lang] and query.parse_query(lang, injections[lang]) - or query.get_query(lang, 'injections'), + _injection_query = injections[lang] and query.parse(lang, injections[lang]) + or query.get(lang, 'injections'), _valid = false, _parser = vim._create_ts_parser(lang), _callbacks = { @@ -482,7 +482,7 @@ end ---@param metadata TSMetadata ---@return Range6[] local function get_node_ranges(node, source, metadata, include_children) - local range = query.get_range(node, source, metadata) + local range = vim.treesitter.get_range(node, source, metadata) if include_children then return { range } @@ -562,7 +562,7 @@ function LanguageTree:_get_injection(match, metadata) -- Lang should override any other language tag if name == 'injection.language' then - lang = query.get_node_text(node, self._source, { metadata = metadata[id] }) + lang = vim.treesitter.get_node_text(node, self._source, { metadata = metadata[id] }) elseif name == 'injection.content' then ranges = get_node_ranges(node, self._source, metadata[id], include_children) end @@ -609,11 +609,11 @@ function LanguageTree:_get_injection_deprecated(match, metadata) -- Lang should override any other language tag if name == 'language' and not lang then - lang = query.get_node_text(node, self._source, { metadata = metadata[id] }) + lang = vim.treesitter.get_node_text(node, self._source, { metadata = metadata[id] }) elseif name == 'combined' then combined = true elseif name == 'content' and #ranges == 0 then - ranges[#ranges + 1] = query.get_range(node, self._source, metadata[id]) + ranges[#ranges + 1] = vim.treesitter.get_range(node, self._source, metadata[id]) -- Ignore any tags that start with "_" -- Allows for other tags to be used in matches elseif string.sub(name, 1, 1) ~= '_' then @@ -622,7 +622,7 @@ function LanguageTree:_get_injection_deprecated(match, metadata) end if #ranges == 0 then - ranges[#ranges + 1] = query.get_range(node, self._source, metadata[id]) + ranges[#ranges + 1] = vim.treesitter.get_range(node, self._source, metadata[id]) end end end diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index f4e038b2d8..8ccd6da8a7 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -1,8 +1,6 @@ local a = vim.api local language = require('vim.treesitter.language') -local Range = require('vim.treesitter._range') - ---@class Query ---@field captures string[] List of captures used in query ---@field info TSQueryInfo Contains used queries, predicates, directives @@ -14,6 +12,7 @@ Query.__index = Query ---@field captures table ---@field patterns table +---@class TSQueryModule local M = {} ---@private @@ -57,22 +56,14 @@ local function add_included_lang(base_langs, lang, ilang) return false end ----@private ----@param buf integer ----@param range Range ----@returns string -local function buf_range_get_text(buf, range) - local start_row, start_col, end_row, end_col = Range.unpack4(range) - if end_col == 0 then - if start_row == end_row then - start_col = -1 - start_row = start_row - 1 - end - end_col = -1 - end_row = end_row - 1 - end - local lines = a.nvim_buf_get_text(buf, start_row, start_col, end_row, end_col, {}) - return table.concat(lines, '\n') +---@deprecated +function M.get_query_files(...) + vim.deprecate( + 'vim.treesitter.query.get_query_files()', + 'vim.treesitter.query.get_files()', + '0.10' + ) + return M.get_files(...) end --- Gets the list of files used to make up a query @@ -81,7 +72,7 @@ end ---@param query_name string Name of the query to load (e.g., "highlights") ---@param is_included (boolean|nil) Internal parameter, most of the time left as `nil` ---@return string[] query_files List of files to load for given query and language -function M.get_query_files(lang, query_name, is_included) +function M.get_files(lang, query_name, is_included) local query_path = string.format('queries/%s/%s.scm', lang, query_name) local lang_files = dedupe_files(a.nvim_get_runtime_file(query_path, true)) @@ -153,7 +144,7 @@ function M.get_query_files(lang, query_name, is_included) local query_files = {} for _, base_lang in ipairs(base_langs) do - local base_files = M.get_query_files(base_lang, query_name, true) + local base_files = M.get_files(base_lang, query_name, true) vim.list_extend(query_files, base_files) end vim.list_extend(query_files, { base_query }) @@ -175,7 +166,7 @@ local function read_query_files(filenames) return table.concat(contents, '') end --- The explicitly set queries from |vim.treesitter.query.set_query()| +-- The explicitly set queries from |vim.treesitter.query.set()| ---@type table> local explicit_queries = setmetatable({}, { __index = function(t, k) @@ -186,6 +177,12 @@ local explicit_queries = setmetatable({}, { end, }) +---@deprecated +function M.set_query(...) + vim.deprecate('vim.treesitter.query.set_query()', 'vim.treesitter.query.set()', '0.10') + M.set(...) +end + --- Sets the runtime query named {query_name} for {lang} --- --- This allows users to override any runtime files and/or configuration @@ -194,8 +191,17 @@ local explicit_queries = setmetatable({}, { ---@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_query(lang, query_name, text) - explicit_queries[lang][query_name] = M.parse_query(lang, text) +function M.set(lang, query_name, text) + explicit_queries[lang][query_name] = M.parse(lang, text) +end + +---@deprecated +---@param lang string Language to use for the query +---@param query_name string Name of the query (e.g. "highlights") +--- +---@return Query|nil Parsed query +function M.get_query(lang, query_name) + return M.get(lang, query_name) end --- Returns the runtime query {query_name} for {lang}. @@ -204,16 +210,16 @@ end ---@param query_name string Name of the query (e.g. "highlights") --- ---@return Query|nil Parsed query -function M.get_query(lang, query_name) +function M.get(lang, query_name) if explicit_queries[lang][query_name] then return explicit_queries[lang][query_name] end - local query_files = M.get_query_files(lang, query_name) + local query_files = M.get_files(lang, query_name) local query_string = read_query_files(query_files) if #query_string > 0 then - return M.parse_query(lang, query_string) + return M.parse(lang, query_string) end end @@ -222,6 +228,12 @@ local query_cache = vim.defaulttable(function() return setmetatable({}, { __mode = 'v' }) end) +---@deprecated +function M.parse_query(...) + vim.deprecate('vim.treesitter.query.parse_query()', 'vim.treesitter.query.parse()', '0.10') + return M.parse(...) +end + --- Parse {query} as a string. (If the query is in a file, the caller --- should read the contents into a string before calling). --- @@ -239,7 +251,7 @@ end) ---@param query string Query in s-expr syntax --- ---@return Query Parsed query -function M.parse_query(lang, query) +function M.parse(lang, query) language.add(lang) local cached = query_cache[lang][query] if cached then @@ -254,41 +266,16 @@ function M.parse_query(lang, query) return self end ----Get the range of a |TSNode|. Can also supply {source} and {metadata} ----to get the range with directives applied. ----@param node TSNode ----@param source integer|string|nil Buffer or string from which the {node} is extracted ----@param metadata TSMetadata|nil ----@return Range6 -function M.get_range(node, source, metadata) - if metadata and metadata.range then - assert(source) - return Range.add_bytes(source, metadata.range) - end - return { node:range(true) } +---@deprecated +function M.get_range(...) + vim.deprecate('vim.treesitter.query.get_range()', 'vim.treesitter.get_range()', '0.10') + return vim.treesitter.get_range(...) end ---- Gets the text corresponding to a given node ---- ----@param node TSNode ----@param source (integer|string) Buffer or string from which the {node} is extracted ----@param opts (table|nil) Optional parameters. ---- - metadata (table) Metadata of a specific capture. This would be ---- set to `metadata[capture_id]` when using |vim.treesitter.add_directive()|. ----@return string -function M.get_node_text(node, source, opts) - opts = opts or {} - local metadata = opts.metadata or {} - - if metadata.text then - return metadata.text - elseif type(source) == 'number' then - local range = M.get_range(node, source, metadata) - return buf_range_get_text(source, range) - end - - ---@cast source string - return source:sub(select(3, node:start()) + 1, select(3, node:end_())) +---@deprecated +function M.get_node_text(...) + vim.deprecate('vim.treesitter.query.get_node_text()', 'vim.treesitter.get_node_text()', '0.10') + return vim.treesitter.get_node_text(...) end ---@alias TSMatch table @@ -304,7 +291,7 @@ local predicate_handlers = { if not node then return true end - local node_text = M.get_node_text(node, source) + local node_text = vim.treesitter.get_node_text(node, source) local str ---@type string if type(predicate[3]) == 'string' then @@ -312,7 +299,7 @@ local predicate_handlers = { str = predicate[3] else -- (#eq? @aa @bb) - str = M.get_node_text(match[predicate[3]], source) + str = vim.treesitter.get_node_text(match[predicate[3]], source) end if node_text ~= str or str == nil then @@ -328,7 +315,7 @@ local predicate_handlers = { return true end local regex = predicate[3] - return string.find(M.get_node_text(node, source), regex) ~= nil + return string.find(vim.treesitter.get_node_text(node, source), regex) ~= nil end, ['match?'] = (function() @@ -357,7 +344,7 @@ local predicate_handlers = { end ---@diagnostic disable-next-line no-unknown local regex = compiled_vim_regexes[pred[3]] - return regex:match_str(M.get_node_text(node, source)) + return regex:match_str(vim.treesitter.get_node_text(node, source)) end end)(), @@ -366,7 +353,7 @@ local predicate_handlers = { if not node then return true end - local node_text = M.get_node_text(node, source) + local node_text = vim.treesitter.get_node_text(node, source) for i = 3, #predicate do if string.find(node_text, predicate[i], 1, true) then @@ -382,7 +369,7 @@ local predicate_handlers = { if not node then return true end - local node_text = M.get_node_text(node, source) + local node_text = vim.treesitter.get_node_text(node, source) -- Since 'predicate' will not be used by callers of this function, use it -- to store a string set built from the list of words to check against. @@ -468,7 +455,7 @@ local directive_handlers = { assert(type(id) == 'number') local node = match[id] - local text = M.get_node_text(node, bufnr, { metadata = metadata[id] }) or '' + local text = vim.treesitter.get_node_text(node, bufnr, { metadata = metadata[id] }) or '' if not metadata[id] then metadata[id] = {} @@ -486,7 +473,7 @@ local directive_handlers = { --- ---@param name string Name of the predicate, without leading # ---@param handler function(match:table, pattern:string, bufnr:integer, predicate:string[]) ---- - see |vim.treesitter.add_directive()| for argument meanings +--- - see |vim.treesitter.query.add_directive()| for argument meanings ---@param force boolean|nil function M.add_predicate(name, handler, force) if predicate_handlers[name] and not force then -- cgit From ac7397f4a06e451fedde86fb4eba0038d0d75e68 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Fri, 24 Mar 2023 16:31:30 +0000 Subject: fix(treesitter): add missing deprecate --- runtime/lua/vim/treesitter/_fold.lua | 5 ++--- runtime/lua/vim/treesitter/query.lua | 9 +++------ 2 files changed, 5 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 3e67e400c2..6547ab936e 100644 --- a/runtime/lua/vim/treesitter/_fold.lua +++ b/runtime/lua/vim/treesitter/_fold.lua @@ -1,5 +1,4 @@ local Range = require('vim.treesitter._range') -local Query = require('vim.treesitter.query') local api = vim.api @@ -105,7 +104,7 @@ local function get_folds_levels(bufnr, info, srow, erow) local prev_stop = -1 vim.treesitter.get_parser(bufnr):for_each_tree(function(tree, ltree) - local query = vim.treesitter.query.get_query(ltree:lang(), 'folds') + local query = vim.treesitter.query.get(ltree:lang(), 'folds') if not query then return end @@ -115,7 +114,7 @@ local function get_folds_levels(bufnr, info, srow, erow) for id, node, metadata in query:iter_captures(tree:root(), bufnr, srow or 0, q_erow) do if query.captures[id] == 'fold' then - local range = Query.get_range(node, bufnr, metadata[id]) + local range = vim.treesitter.get_range(node, bufnr, metadata[id]) local start, _, stop, stop_col = Range.unpack4(range) if stop_col == 0 then diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index 8ccd6da8a7..25623c1498 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -196,12 +196,9 @@ function M.set(lang, query_name, text) end ---@deprecated ----@param lang string Language to use for the query ----@param query_name string Name of the query (e.g. "highlights") ---- ----@return Query|nil Parsed query -function M.get_query(lang, query_name) - return M.get(lang, query_name) +function M.get_query(...) + vim.deprecate('vim.treesitter.query.get_query()', 'vim.treesitter.query.get()', '0.10') + return M.get(...) end --- Returns the runtime query {query_name} for {lang}. -- cgit From 61e54f26361b2e7d08eabde9a4cbf42aaa41683b Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Thu, 30 Mar 2023 10:26:28 +0100 Subject: feat: add `vim.treesitter.language.get_filetypes()` (#22643) --- runtime/lua/vim/treesitter/language.lua | 57 +++++++++++++++++++---------- runtime/lua/vim/treesitter/languagetree.lua | 7 +--- 2 files changed, 38 insertions(+), 26 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/language.lua b/runtime/lua/vim/treesitter/language.lua index 974d66ec05..b1c788e6ba 100644 --- a/runtime/lua/vim/treesitter/language.lua +++ b/runtime/lua/vim/treesitter/language.lua @@ -6,9 +6,25 @@ local M = {} ---@type table local ft_to_lang = {} ----@param filetype string ----@return string|nil +--- Get the filetypes associated with the parser named {lang}. +--- @param lang string Name of parser +--- @return string[] filetypes +function M.get_filetypes(lang) + local r = {} ---@type string[] + for ft, p in pairs(ft_to_lang) do + if p == lang then + r[#r + 1] = ft + end + end + return r +end + +--- @param filetype string +--- @return string|nil function M.get_lang(filetype) + if filetype == '' then + return + end return ft_to_lang[filetype] end @@ -35,16 +51,14 @@ end ---@field filetype? string|string[] ---@field symbol_name? string ---- Asserts that a parser for the language {lang} is installed. +--- Load parser with name {lang} --- --- Parsers are searched in the `parser` runtime directory, or the provided {path} --- ----@param lang string Language the parser should parse (alphanumerical and `_` only) +---@param lang string Name of the parser (alphanumerical and `_` only) ---@param opts (table|nil) Options: ---- - filetype (string|string[]) Filetype(s) that lang can be parsed with. ---- Note this is not strictly the same as lang since a single lang can ---- parse multiple filetypes. ---- Defaults to lang. +--- - filetype (string|string[]) Default filetype the parser should be associated with. +--- Defaults to {lang}. --- - path (string|nil) Optional path the parser is located at --- - symbol_name (string|nil) Internal symbol name for the language to load function M.add(lang, opts) @@ -61,7 +75,7 @@ function M.add(lang, opts) filetype = { filetype, { 'string', 'table' }, true }, }) - M.register(lang, filetype or lang) + M.register(lang, filetype) if vim._ts_has_language(lang) then return @@ -83,23 +97,26 @@ function M.add(lang, opts) vim._ts_add_language(path, lang, symbol_name) end ---- Register a lang to be used for a filetype (or list of filetypes). ----@param lang string Language to register ----@param filetype string|string[] Filetype(s) to associate with lang +--- @private +--- @param x string|string[] +--- @return string[] +local function ensure_list(x) + if type(x) == 'table' then + return x + end + return { x } +end + +--- Register a parser named {lang} to be used for {filetype}(s). +--- @param lang string Name of parser +--- @param filetype string|string[] Filetype(s) to associate with lang function M.register(lang, filetype) vim.validate({ lang = { lang, 'string' }, filetype = { filetype, { 'string', 'table' } }, }) - local filetypes ---@type string[] - if type(filetype) == 'string' then - filetypes = { filetype } - else - filetypes = filetype - end - - for _, f in ipairs(filetypes) do + for _, f in ipairs(ensure_list(filetype)) do if f ~= '' then ft_to_lang[f] = lang end diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index 82e507551d..922e4881ca 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -76,7 +76,7 @@ LanguageTree.__index = LanguageTree --- "injected" language parsers, which themselves may inject other languages, recursively. --- ---@param source (integer|string) Buffer or text string to parse ----@param lang string Root language of this tree +---@param lang string|nil Root language of this tree ---@param opts (table|nil) Optional arguments: --- - injections table Map of language to injection query strings. Overrides the --- built-in runtime file searching for language injections. @@ -86,11 +86,6 @@ function LanguageTree.new(source, lang, opts) ---@type LanguageTreeOpts opts = opts or {} - if opts.queries then - a.nvim_err_writeln("'queries' is no longer supported. Use 'injections' now") - opts.injections = opts.queries - end - local injections = opts.injections or {} local self = setmetatable({ _source = source, -- cgit From d7f7450017b9b05303698a6cda54303ef22c63b3 Mon Sep 17 00:00:00 2001 From: Christian Clason Date: Fri, 31 Mar 2023 17:09:00 +0200 Subject: refactor(treesitter)!: rename help parser to vimdoc --- runtime/lua/vim/treesitter/language.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/language.lua b/runtime/lua/vim/treesitter/language.lua index b1c788e6ba..5b74bb6200 100644 --- a/runtime/lua/vim/treesitter/language.lua +++ b/runtime/lua/vim/treesitter/language.lua @@ -4,7 +4,9 @@ local a = vim.api local M = {} ---@type table -local ft_to_lang = {} +local ft_to_lang = { + help = 'vimdoc', +} --- Get the filetypes associated with the parser named {lang}. --- @param lang string Name of parser -- cgit From 6a4ebf894fa39bfb09695a129a3300cb99408542 Mon Sep 17 00:00:00 2001 From: Sizhe Zhao Date: Sat, 1 Apr 2023 21:47:20 +0800 Subject: fix(health): stop using deprecated ts.language.inspect_language() (#22850) --- runtime/lua/vim/treesitter/health.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/health.lua b/runtime/lua/vim/treesitter/health.lua index fd1188fde4..dabf2cdf6c 100644 --- a/runtime/lua/vim/treesitter/health.lua +++ b/runtime/lua/vim/treesitter/health.lua @@ -22,7 +22,7 @@ function M.check() ) ) else - local lang = ts.language.inspect_language(parsername) + local lang = ts.language.inspect(parsername) health.report_ok( string.format('Parser: %-10s ABI: %d, path: %s', parsername, lang._abi_version, parser) ) -- cgit From 469e6bfc56aa18350bfab13bef8a51b02a5b3c65 Mon Sep 17 00:00:00 2001 From: danilax999 <75566563+danilax999@users.noreply.github.com> Date: Tue, 4 Apr 2023 14:26:21 +0300 Subject: fix(treesitter): use capture metadata range if exists use `treesitter.get_range` instead of inline expression --- runtime/lua/vim/treesitter/highlighter.lua | 3 ++- 1 file changed, 2 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 729cd34090..d3cc1b698c 100644 --- a/runtime/lua/vim/treesitter/highlighter.lua +++ b/runtime/lua/vim/treesitter/highlighter.lua @@ -236,7 +236,8 @@ local function on_line_impl(self, buf, line, is_spell_nav) break end - local start_row, start_col, end_row, end_col = node:range() + local range = vim.treesitter.get_range(node, buf, metadata[capture]) + local start_row, start_col, _, end_row, end_col, _ = unpack(range) local hl = highlighter_query.hl_cache[capture] local capture_name = highlighter_query:query().captures[capture] -- cgit From 090ade4af6344a7bc4ee56a8052c0739c0428c04 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Tue, 4 Apr 2023 12:58:16 +0100 Subject: refactor(treesitter): delegate region calculation to treesitter (#22576) --- runtime/lua/vim/treesitter/languagetree.lua | 94 ++++++++++++----------------- 1 file changed, 38 insertions(+), 56 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index 922e4881ca..cf0ecbd839 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -57,7 +57,9 @@ local Range = require('vim.treesitter._range') ---@field private _injection_query Query Queries defining injected languages ---@field private _opts table Options ---@field private _parser TSParser Parser for language ----@field private _regions Range6[][] List of regions this tree should manage and parse +---@field private _regions Range6[][]? +---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 ---@field private _source (integer|string) Buffer or string to parse ---@field private _trees TSTree[] Reference to parsed tree (one for each language) @@ -91,7 +93,6 @@ function LanguageTree.new(source, lang, opts) _source = source, _lang = lang, _children = {}, - _regions = {}, _trees = {}, _opts = opts, _injection_query = injections[lang] and query.parse(lang, injections[lang]) @@ -237,27 +238,21 @@ function LanguageTree:parse() --- At least 1 region is invalid if not self:is_valid(true) then - local function _parsetree(index) - local parse_time, tree, tree_changes = - tcall(self._parser.parse, self._parser, self._trees[index], self._source) - - self:_do_callback('changedtree', tree_changes, tree) - self._trees[index] = tree - vim.list_extend(changes, tree_changes) - - total_parse_time = total_parse_time + parse_time - regions_parsed = regions_parsed + 1 - end - - if #self._regions > 0 then - for i, ranges in ipairs(self._regions) do - if not self._valid or not self._valid[i] then - self._parser:set_included_ranges(ranges) - _parsetree(i) - end + -- If there are no ranges, set to an empty list + -- so the included ranges in the parser are cleared. + for i, ranges in ipairs(self:included_regions()) do + if not self._valid or not self._valid[i] then + self._parser:set_included_ranges(ranges) + local parse_time, tree, tree_changes = + tcall(self._parser.parse, self._parser, self._trees[i], self._source) + + self:_do_callback('changedtree', tree_changes, tree) + self._trees[i] = tree + vim.list_extend(changes, tree_changes) + + total_parse_time = total_parse_time + parse_time + regions_parsed = regions_parsed + 1 end - else - _parsetree(1) end end @@ -403,7 +398,7 @@ function LanguageTree:_iter_regions(fn) local all_valid = true - for i, region in ipairs(self._regions) do + for i, region in ipairs(self:included_regions()) do if self._valid[i] == nil then self._valid[i] = true end @@ -454,7 +449,7 @@ function LanguageTree:set_included_regions(new_regions) end end - if #self._regions ~= #new_regions then + if #self:included_regions() ~= #new_regions then self._trees = {} self:invalidate() else @@ -462,13 +457,28 @@ function LanguageTree:set_included_regions(new_regions) return vim.deep_equal(new_regions[i], region) end) end + self._regions = new_regions end ---Gets the set of included regions ---@return integer[][] function LanguageTree:included_regions() - return self._regions + if self._regions then + return self._regions + end + + if #self._trees == 0 then + return { {} } + end + + local regions = {} ---@type Range6[][] + for i, _ in ipairs(self._trees) do + regions[i] = self._trees[i]:included_ranges(true) + end + + self._regions = regions + return regions end ---@private @@ -721,6 +731,8 @@ function LanguageTree:_edit( ) end + self._regions = nil + local changed_range = { start_row, start_col, @@ -730,42 +742,12 @@ function LanguageTree:_edit( end_byte_old, } - local new_range = { - start_row, - start_col, - start_byte, - end_row_new, - end_col_new, - end_byte_new, - } - - if #self._regions == 0 then - self._valid = false - end - -- Validate regions after editing the tree self:_iter_regions(function(_, region) - for i, r in ipairs(region) do + for _, r in ipairs(region) do if Range.intercepts(r, changed_range) then return false end - - -- Range after change. Adjust - if Range.cmp_pos.gt(r[1], r[2], changed_range[4], changed_range[5]) then - local byte_offset = new_range[6] - changed_range[6] - local row_offset = new_range[4] - changed_range[4] - - -- Update the range to avoid invalidation in set_included_regions() - -- which will compare the regions against the parsed injection regions - region[i] = { - r[1] + row_offset, - r[2], - r[3] + byte_offset, - r[4] + row_offset, - r[5], - r[6] + byte_offset, - } - end end return true end) -- cgit From a5c572bd446a89be2dccb2f7479ff1b017074640 Mon Sep 17 00:00:00 2001 From: dundargoc <33953936+dundargoc@users.noreply.github.com> Date: Tue, 4 Apr 2023 19:07:33 +0200 Subject: docs: fix typos Co-authored-by: Gregory Anders Co-authored-by: Raphael Co-authored-by: C.D. MacEachern Co-authored-by: himanoa --- 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 6547ab936e..7df93d1b2e 100644 --- a/runtime/lua/vim/treesitter/_fold.lua +++ b/runtime/lua/vim/treesitter/_fold.lua @@ -68,7 +68,7 @@ function FoldInfo:add_stop(lnum) self.stop_counts[lnum] = (self.stop_counts[lnum] or 0) + 1 end ----@packag +---@package ---@param lnum integer ---@return integer function FoldInfo:get_start(lnum) -- cgit From b1de4820b7b1a527f4d0cf9a20192d92bea1d9c4 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Sat, 1 Apr 2023 12:55:04 +0100 Subject: refactor(treesitter): move inspect_tree impl --- runtime/lua/vim/treesitter/playground.lua | 225 +++++++++++++++++++++++++++++- 1 file changed, 224 insertions(+), 1 deletion(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/playground.lua b/runtime/lua/vim/treesitter/playground.lua index 35f06f5caf..7eead14579 100644 --- a/runtime/lua/vim/treesitter/playground.lua +++ b/runtime/lua/vim/treesitter/playground.lua @@ -1,5 +1,8 @@ local api = vim.api +---@class TSPlaygroundModule +local M = {} + ---@class TSPlayground ---@field ns integer API namespace ---@field opts table Options table with the following keys: @@ -212,4 +215,224 @@ function TSPlayground:iter() return ipairs(self.opts.anon and self.nodes or self.named) end -return TSPlayground +--- @class InspectTreeOpts +--- @field lang string? The language of the source buffer. If omitted, the +--- filetype of the source buffer is used. +--- @field bufnr integer? Buffer to draw the tree into. If omitted, a new +--- buffer is created. +--- @field winid integer? Window id to display the tree buffer in. If omitted, +--- a new window is created with {command}. +--- @field command string? Vimscript command to create the window. Default +--- value is "60vnew". Only used when {winid} is nil. +--- @field title (string|fun(bufnr:integer):string|nil) Title of the window. If a +--- function, it accepts the buffer number of the source +--- buffer as its only argument and should return a string. + +--- @param opts InspectTreeOpts +function M.inspect_tree(opts) + vim.validate({ + opts = { opts, 't', true }, + }) + + opts = opts or {} + + local buf = api.nvim_get_current_buf() + local win = api.nvim_get_current_win() + local pg = assert(TSPlayground:new(buf, opts.lang)) + + -- Close any existing playground window + if vim.b[buf].playground then + local w = vim.b[buf].playground + if api.nvim_win_is_valid(w) then + api.nvim_win_close(w, true) + end + end + + local w = opts.winid + if not w then + vim.cmd(opts.command or '60vnew') + w = api.nvim_get_current_win() + end + + local b = opts.bufnr + if b then + api.nvim_win_set_buf(w, b) + else + b = api.nvim_win_get_buf(w) + end + + vim.b[buf].playground = w + + vim.wo[w].scrolloff = 5 + vim.wo[w].wrap = false + vim.wo[w].foldmethod = 'manual' -- disable folding + vim.bo[b].buflisted = false + vim.bo[b].buftype = 'nofile' + vim.bo[b].bufhidden = 'wipe' + vim.bo[b].filetype = 'query' + + local title --- @type string? + local opts_title = opts.title + if not opts_title then + local bufname = api.nvim_buf_get_name(buf) + title = string.format('Syntax tree for %s', vim.fn.fnamemodify(bufname, ':.')) + elseif type(opts_title) == 'function' then + title = opts_title(buf) + end + + assert(type(title) == 'string', 'Window title must be a string') + api.nvim_buf_set_name(b, title) + + pg:draw(b) + + api.nvim_buf_clear_namespace(buf, pg.ns, 0, -1) + api.nvim_buf_set_keymap(b, 'n', '', '', { + desc = 'Jump to the node under the cursor in the source buffer', + callback = function() + local row = api.nvim_win_get_cursor(w)[1] + local pos = pg:get(row) + api.nvim_set_current_win(win) + api.nvim_win_set_cursor(win, { pos.lnum + 1, pos.col }) + end, + }) + api.nvim_buf_set_keymap(b, 'n', 'a', '', { + desc = 'Toggle anonymous nodes', + callback = function() + local row, col = unpack(api.nvim_win_get_cursor(w)) + local curnode = pg:get(row) + while curnode and not curnode.named do + row = row - 1 + curnode = pg:get(row) + end + + pg.opts.anon = not pg.opts.anon + pg:draw(b) + + if not curnode then + return + end + + local id = curnode.id + for i, node in pg:iter() do + if node.id == id then + api.nvim_win_set_cursor(w, { i, col }) + break + end + end + end, + }) + api.nvim_buf_set_keymap(b, 'n', 'I', '', { + desc = 'Toggle language display', + callback = function() + pg.opts.lang = not pg.opts.lang + pg:draw(b) + end, + }) + + local group = api.nvim_create_augroup('treesitter/playground', {}) + + api.nvim_create_autocmd('CursorMoved', { + group = group, + buffer = b, + callback = function() + api.nvim_buf_clear_namespace(buf, pg.ns, 0, -1) + local row = api.nvim_win_get_cursor(w)[1] + local pos = pg:get(row) + api.nvim_buf_set_extmark(buf, pg.ns, pos.lnum, pos.col, { + end_row = pos.end_lnum, + end_col = math.max(0, pos.end_col), + hl_group = 'Visual', + }) + + local topline, botline = vim.fn.line('w0', win), vim.fn.line('w$', win) + + -- Move the cursor if highlighted range is completely out of view + if pos.lnum < topline and pos.end_lnum < topline then + api.nvim_win_set_cursor(win, { pos.end_lnum + 1, 0 }) + elseif pos.lnum > botline and pos.end_lnum > botline then + api.nvim_win_set_cursor(win, { pos.lnum + 1, 0 }) + end + end, + }) + + api.nvim_create_autocmd('CursorMoved', { + group = group, + buffer = buf, + callback = function() + if not api.nvim_buf_is_loaded(b) then + return true + end + + api.nvim_buf_clear_namespace(b, pg.ns, 0, -1) + + local cursor_node = vim.treesitter.get_node({ + bufnr = buf, + lang = opts.lang, + ignore_injections = false, + }) + if not cursor_node then + return + end + + local cursor_node_id = cursor_node:id() + for i, v in pg:iter() do + if v.id == cursor_node_id then + local start = v.depth + local end_col = start + #v.text + api.nvim_buf_set_extmark(b, pg.ns, i - 1, start, { + end_col = end_col, + hl_group = 'Visual', + }) + api.nvim_win_set_cursor(w, { i, 0 }) + break + end + end + end, + }) + + api.nvim_create_autocmd({ 'TextChanged', 'InsertLeave' }, { + group = group, + buffer = buf, + callback = function() + if not api.nvim_buf_is_loaded(b) then + return true + end + + pg = assert(TSPlayground:new(buf, opts.lang)) + pg:draw(b) + end, + }) + + api.nvim_create_autocmd('BufLeave', { + group = group, + buffer = b, + callback = function() + api.nvim_buf_clear_namespace(buf, pg.ns, 0, -1) + end, + }) + + api.nvim_create_autocmd('BufLeave', { + group = group, + buffer = buf, + callback = function() + if not api.nvim_buf_is_loaded(b) then + return true + end + + api.nvim_buf_clear_namespace(b, pg.ns, 0, -1) + end, + }) + + api.nvim_create_autocmd('BufHidden', { + group = group, + buffer = buf, + once = true, + callback = function() + if api.nvim_win_is_valid(w) then + api.nvim_win_close(w, true) + end + end, + }) +end + +return M -- cgit From e826d09c18ab8840592b6cdbbcfe3e311a047174 Mon Sep 17 00:00:00 2001 From: dundargoc <33953936+dundargoc@users.noreply.github.com> Date: Tue, 4 Apr 2023 23:37:46 +0200 Subject: fix(windows): consistent normalization in fs.find vim.fs.find(".luacheckrc") ``` c:\\projects\\neovim/.luacheckrc # before c:/projects/neovim/.luacheckrc # after ``` Co-authored-by: kylo252 <59826753+kylo252@users.noreply.github.com> --- runtime/lua/vim/treesitter/playground.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/playground.lua b/runtime/lua/vim/treesitter/playground.lua index 7eead14579..2c0a0d1aa6 100644 --- a/runtime/lua/vim/treesitter/playground.lua +++ b/runtime/lua/vim/treesitter/playground.lua @@ -265,7 +265,7 @@ function M.inspect_tree(opts) vim.wo[w].scrolloff = 5 vim.wo[w].wrap = false - vim.wo[w].foldmethod = 'manual' -- disable folding + vim.wo[w].foldmethod = 'manual' -- disable folding vim.bo[b].buflisted = false vim.bo[b].buftype = 'nofile' vim.bo[b].bufhidden = 'wipe' -- cgit From 34ac75b32927328a0c691c5bda987c0fdb5ce9eb Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Wed, 5 Apr 2023 17:19:53 +0100 Subject: refactor: rename local API alias from a to api Problem: Codebase inconsistently binds vim.api onto a or api. Solution: Use api everywhere. a as an identifier is too short to have at the module level. --- runtime/lua/vim/treesitter/highlighter.lua | 18 +++++++++--------- runtime/lua/vim/treesitter/language.lua | 4 ++-- runtime/lua/vim/treesitter/languagetree.lua | 12 ++++++------ runtime/lua/vim/treesitter/query.lua | 8 ++++---- 4 files changed, 21 insertions(+), 21 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/highlighter.lua b/runtime/lua/vim/treesitter/highlighter.lua index d3cc1b698c..ac2a929487 100644 --- a/runtime/lua/vim/treesitter/highlighter.lua +++ b/runtime/lua/vim/treesitter/highlighter.lua @@ -1,4 +1,4 @@ -local a = vim.api +local api = vim.api local query = vim.treesitter.query ---@alias TSHlIter fun(): integer, TSNode, TSMetadata @@ -25,7 +25,7 @@ TSHighlighter.active = TSHighlighter.active or {} local TSHighlighterQuery = {} TSHighlighterQuery.__index = TSHighlighterQuery -local ns = a.nvim_create_namespace('treesitter/highlighter') +local ns = api.nvim_create_namespace('treesitter/highlighter') ---@private function TSHighlighterQuery.new(lang, query_string) @@ -36,7 +36,7 @@ function TSHighlighterQuery.new(lang, query_string) local name = self._query.captures[capture] local id = 0 if not vim.startswith(name, '_') then - id = a.nvim_get_hl_id_by_name('@' .. name .. '.' .. lang) + id = api.nvim_get_hl_id_by_name('@' .. name .. '.' .. lang) end rawset(table, capture, id) @@ -121,7 +121,7 @@ function TSHighlighter.new(tree, opts) vim.cmd.runtime({ 'syntax/synload.vim', bang = true }) end - a.nvim_buf_call(self.bufnr, function() + api.nvim_buf_call(self.bufnr, function() vim.opt_local.spelloptions:append('noplainbuffer') end) @@ -140,7 +140,7 @@ 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 - a.nvim_exec_autocmds('FileType', { group = 'syntaxset', buffer = self.bufnr }) + api.nvim_exec_autocmds('FileType', { group = 'syntaxset', buffer = self.bufnr }) end end end @@ -168,7 +168,7 @@ end ---@param start_row integer ---@param new_end integer function TSHighlighter:on_bytes(_, _, start_row, _, _, _, _, _, new_end) - a.nvim__buf_redraw_range(self.bufnr, start_row, start_row + new_end + 1) + api.nvim__buf_redraw_range(self.bufnr, start_row, start_row + new_end + 1) end ---@package @@ -180,7 +180,7 @@ end ---@param changes integer[][]? function TSHighlighter:on_changedtree(changes) for _, ch in ipairs(changes or {}) do - a.nvim__buf_redraw_range(self.bufnr, ch[1], ch[3] + 1) + api.nvim__buf_redraw_range(self.bufnr, ch[1], ch[3] + 1) end end @@ -252,7 +252,7 @@ local function on_line_impl(self, buf, line, is_spell_nav) local spell_pri_offset = capture_name == 'nospell' and 1 or 0 if hl and end_row >= line and (not is_spell_nav or spell ~= nil) then - a.nvim_buf_set_extmark(buf, ns, start_row, start_col, { + api.nvim_buf_set_extmark(buf, ns, start_row, start_col, { end_line = end_row, end_col = end_col, hl_group = hl, @@ -323,7 +323,7 @@ function TSHighlighter._on_win(_, _win, buf, _topline) return true end -a.nvim_set_decoration_provider(ns, { +api.nvim_set_decoration_provider(ns, { on_buf = TSHighlighter._on_buf, on_win = TSHighlighter._on_win, on_line = TSHighlighter._on_line, diff --git a/runtime/lua/vim/treesitter/language.lua b/runtime/lua/vim/treesitter/language.lua index 5b74bb6200..b616d4d70b 100644 --- a/runtime/lua/vim/treesitter/language.lua +++ b/runtime/lua/vim/treesitter/language.lua @@ -1,4 +1,4 @@ -local a = vim.api +local api = vim.api ---@class TSLanguageModule local M = {} @@ -89,7 +89,7 @@ function M.add(lang, opts) end local fname = 'parser/' .. lang .. '.*' - local paths = a.nvim_get_runtime_file(fname, false) + local paths = api.nvim_get_runtime_file(fname, false) if #paths == 0 then error("no parser for '" .. lang .. "' language, see :help treesitter-parsers") end diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index cf0ecbd839..703d2a1f6d 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -32,7 +32,7 @@ --- a plugin that does any kind of analysis on a tree should use a timer to throttle too frequent --- updates. -local a = vim.api +local api = vim.api local query = require('vim.treesitter.query') local language = require('vim.treesitter.language') local Range = require('vim.treesitter._range') @@ -141,16 +141,16 @@ function LanguageTree:_log(...) local prefix = string.format('%s:%d: [%s:%d] ', info.name, info.currentline, self:lang(), nregions) - a.nvim_out_write(prefix) + api.nvim_out_write(prefix) for _, x in ipairs(args) do if type(x) == 'string' then - a.nvim_out_write(x) + api.nvim_out_write(x) else - a.nvim_out_write(vim.inspect(x, { newline = ' ', indent = '' })) + api.nvim_out_write(vim.inspect(x, { newline = ' ', indent = '' })) end - a.nvim_out_write(' ') + api.nvim_out_write(' ') end - a.nvim_out_write('\n') + api.nvim_out_write('\n') end --- Invalidates this parser and all its children diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index 25623c1498..5b87e6ac31 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -1,4 +1,4 @@ -local a = vim.api +local api = vim.api local language = require('vim.treesitter.language') ---@class Query @@ -74,7 +74,7 @@ end ---@return string[] query_files List of files to load for given query and language function M.get_files(lang, query_name, is_included) local query_path = string.format('queries/%s/%s.scm', lang, query_name) - local lang_files = dedupe_files(a.nvim_get_runtime_file(query_path, true)) + local lang_files = dedupe_files(api.nvim_get_runtime_file(query_path, true)) if #lang_files == 0 then return {} @@ -635,7 +635,7 @@ end ---@return (fun(): integer, TSNode, TSMetadata): capture id, capture node, metadata function Query:iter_captures(node, source, start, stop) if type(source) == 'number' and source == 0 then - source = vim.api.nvim_get_current_buf() + source = api.nvim_get_current_buf() end start, stop = value_or_node_range(start, stop, node) @@ -690,7 +690,7 @@ end ---@return (fun(): integer, table, table): pattern id, match, metadata function Query:iter_matches(node, source, start, stop) if type(source) == 'number' and source == 0 then - source = vim.api.nvim_get_current_buf() + source = api.nvim_get_current_buf() end start, stop = value_or_node_range(start, stop, node) -- cgit From e29bc03c046b3a137c2e36b4d34c119b277d62b2 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Thu, 6 Apr 2023 15:16:44 +0100 Subject: fix(treesitter): do not track ranges of the root tree (#22912) Fixes #22911 --- runtime/lua/vim/treesitter/languagetree.lua | 10 +++++++++- 1 file changed, 9 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 703d2a1f6d..4aa07d1b96 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -57,6 +57,7 @@ local Range = require('vim.treesitter._range') ---@field private _injection_query Query Queries defining injected languages ---@field private _opts table Options ---@field private _parser TSParser Parser for language +---@field private _has_regions boolean ---@field private _regions Range6[][]? ---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() @@ -440,6 +441,8 @@ end ---@private ---@param new_regions Range6[][] 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 @@ -468,7 +471,8 @@ function LanguageTree:included_regions() return self._regions end - if #self._trees == 0 then + if not self._has_regions or #self._trees == 0 then + -- treesitter.c will default empty ranges to { -1, -1, -1, -1, -1, -1} return { {} } end @@ -744,6 +748,10 @@ function LanguageTree:_edit( -- Validate regions after editing the tree self:_iter_regions(function(_, region) + if #region == 0 then + -- empty region, use the full source + return false + end for _, r in ipairs(region) do if Range.intercepts(r, changed_range) then return false -- cgit From ccc0980f86c6ef9a86b0e5a3a691f37cea8eb776 Mon Sep 17 00:00:00 2001 From: Scott Ming Date: Tue, 11 Apr 2023 16:26:03 +0800 Subject: fix(treesitter): Use the correct replacement args for #gsub! directive (#23015) fix(treesitter): use the correct replacement args for #gsub! directive --- 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 5b87e6ac31..8a747ba14c 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -458,7 +458,7 @@ local directive_handlers = { metadata[id] = {} end - local pattern, replacement = pred[3], pred[3] + local pattern, replacement = pred[3], pred[4] assert(type(pattern) == 'string') assert(type(replacement) == 'string') -- cgit From c08b03076167837cff9eb66c19440d727e6dad31 Mon Sep 17 00:00:00 2001 From: dundargoc <33953936+dundargoc@users.noreply.github.com> Date: Sat, 15 Apr 2023 23:40:48 +0200 Subject: refactor: deprecate checkhealth functions The following functions are deprecated and will be removed in Nvim v0.11: - health#report_start() - health#report_info() - health#report_ok() - health#report_warn() - health#report_error() - vim.health.report_start() - vim.health.report_info() - vim.health.report_ok() - vim.health.report_warn() - vim.health.report_error() Users should instead use these: - vim.health.start() - vim.health.info() - vim.health.ok() - vim.health.warn() - vim.health.error() --- runtime/lua/vim/treesitter/health.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/health.lua b/runtime/lua/vim/treesitter/health.lua index dabf2cdf6c..ed1161e97f 100644 --- a/runtime/lua/vim/treesitter/health.lua +++ b/runtime/lua/vim/treesitter/health.lua @@ -6,14 +6,14 @@ local health = require('vim.health') function M.check() local parsers = vim.api.nvim_get_runtime_file('parser/*', true) - health.report_info(string.format('Nvim runtime ABI version: %d', ts.language_version)) + health.info(string.format('Nvim runtime ABI version: %d', ts.language_version)) for _, parser in pairs(parsers) do local parsername = vim.fn.fnamemodify(parser, ':t:r') local is_loadable, err_or_nil = pcall(ts.language.add, parsername) if not is_loadable then - health.report_error( + health.error( string.format( 'Parser "%s" failed to load (path: %s): %s', parsername, @@ -23,7 +23,7 @@ function M.check() ) else local lang = ts.language.inspect(parsername) - health.report_ok( + health.ok( string.format('Parser: %-10s ABI: %d, path: %s', parsername, lang._abi_version, parser) ) end -- cgit From ab2811746eb72e06309a9877dbe6cb70d9cd3b12 Mon Sep 17 00:00:00 2001 From: William <50717946+BIKA-C@users.noreply.github.com> Date: Thu, 20 Apr 2023 06:42:49 -0700 Subject: fix(treesitter playground): fix the wrong range of a node displayed i… (#23209) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix(treesitter playground): wrong range of a node displayed in playground The call parameters order of the function `get_range_str` is flipped for the last two arguments compared to the declaration. --- runtime/lua/vim/treesitter/playground.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/playground.lua b/runtime/lua/vim/treesitter/playground.lua index 2c0a0d1aa6..c512710810 100644 --- a/runtime/lua/vim/treesitter/playground.lua +++ b/runtime/lua/vim/treesitter/playground.lua @@ -147,7 +147,7 @@ local decor_ns = api.nvim_create_namespace('ts.playground') ---@param end_lnum integer ---@param end_col integer ---@return string -local function get_range_str(lnum, col, end_col, end_lnum) +local function get_range_str(lnum, col, end_lnum, end_col) if lnum == end_lnum then return string.format('[%d:%d - %d]', lnum + 1, col + 1, end_col) end -- cgit From c194acbfc479d8e5839fa629363f93f6550d035c Mon Sep 17 00:00:00 2001 From: Stephan Seitz Date: Sat, 29 Apr 2023 18:22:26 +0200 Subject: feat(treesitter): add query_linter from nvim-treesitter/playground (#22784) Co-authored-by: clason Co-authored-by: lewis6991 --- runtime/lua/vim/treesitter/_query_linter.lua | 302 +++++++++++++++++++++++++++ runtime/lua/vim/treesitter/playground.lua | 1 + runtime/lua/vim/treesitter/query.lua | 29 +++ 3 files changed, 332 insertions(+) create mode 100644 runtime/lua/vim/treesitter/_query_linter.lua (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/_query_linter.lua b/runtime/lua/vim/treesitter/_query_linter.lua new file mode 100644 index 0000000000..62f28d3097 --- /dev/null +++ b/runtime/lua/vim/treesitter/_query_linter.lua @@ -0,0 +1,302 @@ +local namespace = vim.api.nvim_create_namespace('vim.treesitter.query_linter') +-- those node names exist for every language +local BUILT_IN_NODE_NAMES = { '_', 'ERROR' } + +local M = {} + +--- @class QueryLinterNormalizedOpts +--- @field langs string[] +--- @field clear boolean + +--- @private +--- Caches parse results for queries for each language. +--- Entries of parse_cache[lang][query_text] will either be true for successful parse or contain the +--- error message of the parse +--- @type table> +local parse_cache = {} + +--- Contains language dependent context for the query linter +--- @class QueryLinterLanguageContext +--- @field lang string? Current `lang` of the targeted parser +--- @field parser_info table? Parser info returned by vim.treesitter.language.inspect +--- @field is_first_lang boolean Whether this is the first language of a linter run checking queries for multiple `langs` + +--- @private +--- Adds a diagnostic for node in the query buffer +--- @param diagnostics Diagnostic[] +--- @param node TSNode +--- @param buf integer +--- @param lint string +--- @param lang string? +local function add_lint_for_node(diagnostics, node, buf, lint, lang) + local node_text = vim.treesitter.get_node_text(node, buf):gsub('\n', ' ') + --- @type string + local message = lint .. ': ' .. node_text + local error_range = { node:range() } + diagnostics[#diagnostics + 1] = { + lnum = error_range[1], + end_lnum = error_range[3], + col = error_range[2], + end_col = error_range[4], + severity = vim.diagnostic.ERROR, + message = message, + source = lang, + } +end + +--- @private +--- Determines the target language of a query file by its path: /.scm +--- @param buf integer +--- @return string? +local function guess_query_lang(buf) + local filename = vim.api.nvim_buf_get_name(buf) + if filename ~= '' then + local ok, query_lang = pcall(vim.fn.fnamemodify, filename, ':p:h:t') + if ok then + return query_lang + end + end +end + +--- @private +--- @param buf integer +--- @param opts QueryLinterOpts|QueryLinterNormalizedOpts|nil +--- @return QueryLinterNormalizedOpts +local function normalize_opts(buf, opts) + opts = opts or {} + if not opts.langs then + opts.langs = guess_query_lang(buf) + end + + if type(opts.langs) ~= 'table' then + --- @diagnostic disable-next-line:assign-type-mismatch + opts.langs = { opts.langs } + end + + --- @cast opts QueryLinterNormalizedOpts + opts.langs = opts.langs or {} + return opts +end + +local lint_query = [[;; query + (program [(named_node) (list) (grouping)] @toplevel) + (named_node + name: _ @node.named) + (anonymous_node + name: _ @node.anonymous) + (field_definition + name: (identifier) @field) + (predicate + name: (identifier) @predicate.name + type: (predicate_type) @predicate.type) + (ERROR) @error +]] + +--- @private +--- @param node TSNode +--- @param buf integer +--- @param lang string +--- @param diagnostics Diagnostic[] +local function check_toplevel(node, buf, lang, diagnostics) + local query_text = vim.treesitter.get_node_text(node, buf) + + if not parse_cache[lang] then + parse_cache[lang] = {} + end + + local lang_cache = parse_cache[lang] + + if lang_cache[query_text] == nil then + local ok, err = pcall(vim.treesitter.query.parse, lang, query_text) + + if not ok and type(err) == 'string' then + err = err:match('.-:%d+: (.+)') + end + + lang_cache[query_text] = ok or err + end + + local cache_entry = lang_cache[query_text] + + if type(cache_entry) == 'string' then + add_lint_for_node(diagnostics, node, buf, cache_entry, lang) + end +end + +--- @private +--- @param node TSNode +--- @param buf integer +--- @param lang string +--- @param parser_info table +--- @param diagnostics Diagnostic[] +local function check_field(node, buf, lang, parser_info, diagnostics) + local field_name = vim.treesitter.get_node_text(node, buf) + if not vim.tbl_contains(parser_info.fields, field_name) then + add_lint_for_node(diagnostics, node, buf, 'Invalid field', lang) + end +end + +--- @private +--- @param node TSNode +--- @param buf integer +--- @param lang string +--- @param parser_info (table) +--- @param diagnostics Diagnostic[] +local function check_node(node, buf, lang, parser_info, diagnostics) + local node_type = vim.treesitter.get_node_text(node, buf) + local is_named = node_type:sub(1, 1) ~= '"' + + if not is_named then + node_type = node_type:gsub('"(.*)".*$', '%1'):gsub('\\(.)', '%1') + end + + local found = vim.tbl_contains(BUILT_IN_NODE_NAMES, node_type) + or vim.tbl_contains(parser_info.symbols, function(s) + return vim.deep_equal(s, { node_type, is_named }) + end, { predicate = true }) + + if not found then + add_lint_for_node(diagnostics, node, buf, 'Invalid node type', lang) + end +end + +--- @private +--- @param node TSNode +--- @param buf integer +--- @param is_predicate boolean +--- @return string +local function get_predicate_name(node, buf, is_predicate) + local name = vim.treesitter.get_node_text(node, buf) + if is_predicate then + if vim.startswith(name, 'not-') then + --- @type string + name = name:sub(string.len('not-') + 1) + end + return name .. '?' + end + return name .. '!' +end + +--- @private +--- @param predicate_node TSNode +--- @param predicate_type_node TSNode +--- @param buf integer +--- @param lang string? +--- @param diagnostics Diagnostic[] +local function check_predicate(predicate_node, predicate_type_node, buf, lang, diagnostics) + local type_string = vim.treesitter.get_node_text(predicate_type_node, buf) + + -- Quirk of the query grammar that directives are also predicates! + if type_string == '?' then + if + not vim.tbl_contains( + vim.treesitter.query.list_predicates(), + get_predicate_name(predicate_node, buf, true) + ) + then + add_lint_for_node(diagnostics, predicate_node, buf, 'Unknown predicate', lang) + end + elseif type_string == '!' then + if + not vim.tbl_contains( + vim.treesitter.query.list_directives(), + get_predicate_name(predicate_node, buf, false) + ) + then + add_lint_for_node(diagnostics, predicate_node, buf, 'Unknown directive', lang) + end + end +end + +--- @private +--- @param buf integer +--- @param match table +--- @param query Query +--- @param lang_context QueryLinterLanguageContext +--- @param diagnostics Diagnostic[] +local function lint_match(buf, match, query, lang_context, diagnostics) + local predicate --- @type TSNode + local predicate_type --- @type TSNode + local lang = lang_context.lang + local parser_info = lang_context.parser_info + + for id, node in pairs(match) do + local cap_id = query.captures[id] + + -- perform language-independent checks only for first lang + if lang_context.is_first_lang then + if cap_id == 'error' then + add_lint_for_node(diagnostics, node, buf, 'Syntax error') + elseif cap_id == 'predicate.name' then + predicate = node + elseif cap_id == 'predicate.type' then + predicate_type = node + end + end + + -- other checks rely on Neovim parser introspection + if lang and parser_info then + if cap_id == 'toplevel' then + check_toplevel(node, buf, lang, diagnostics) + elseif cap_id == 'field' then + check_field(node, buf, lang, parser_info, diagnostics) + elseif cap_id == 'node.named' or cap_id == 'node.anonymous' then + check_node(node, buf, lang, parser_info, diagnostics) + end + end + end + + if predicate and predicate_type then + check_predicate(predicate, predicate_type, buf, lang, diagnostics) + end +end + +--- @private +--- @param buf integer Buffer to lint +--- @param opts QueryLinterOpts|QueryLinterNormalizedOpts|nil Options for linting +function M.lint(buf, opts) + if buf == 0 then + buf = vim.api.nvim_get_current_buf() + end + + local diagnostics = {} + local query = vim.treesitter.query.parse('query', lint_query) + + opts = normalize_opts(buf, opts) + + -- perform at least one iteration even with no langs to perform language independent checks + for i = 1, math.max(1, #opts.langs) do + local lang = opts.langs[i] + + --- @type boolean, (table|nil) + local ok, parser_info = pcall(vim.treesitter.language.inspect, lang) + if not ok then + parser_info = nil + end + + local parser = vim.treesitter.get_parser(buf) + parser:parse() + parser:for_each_tree(function(tree, ltree) + if ltree:lang() == 'query' then + for _, match, _ in query:iter_matches(tree:root(), buf, 0, -1) do + local lang_context = { + lang = lang, + parser_info = parser_info, + is_first_lang = i == 1, + } + lint_match(buf, match, query, lang_context, diagnostics) + end + end + end) + end + + vim.diagnostic.set(namespace, buf, diagnostics) +end + +--- @private +--- @param buf integer +function M.clear(buf) + vim.diagnostic.reset(namespace, buf) +end + +return M diff --git a/runtime/lua/vim/treesitter/playground.lua b/runtime/lua/vim/treesitter/playground.lua index c512710810..8293c1bd0a 100644 --- a/runtime/lua/vim/treesitter/playground.lua +++ b/runtime/lua/vim/treesitter/playground.lua @@ -269,6 +269,7 @@ function M.inspect_tree(opts) vim.bo[b].buflisted = false vim.bo[b].buftype = 'nofile' vim.bo[b].bufhidden = 'wipe' + vim.b[b].disable_query_linter = true vim.bo[b].filetype = 'query' local title --- @type string? diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index 8a747ba14c..492bfd1ffb 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -714,4 +714,33 @@ function Query:iter_matches(node, source, start, stop) return iter end +---@class QueryLinterOpts +---@field langs (string|string[]|nil) +---@field clear (boolean) + +--- Lint treesitter queries using installed parser, or clear lint errors. +--- +--- Use |treesitter-parsers| in runtimepath to check the query file in {buf} for errors: +--- +--- - verify that used nodes are valid identifiers in the grammar. +--- - verify that predicates and directives are valid. +--- - verify that top-level s-expressions are valid. +--- +--- The found diagnostics are reported using |diagnostic-api|. +--- By default, the parser used for verification is determined by the containing folder +--- of the query file, e.g., if the path is `**/lua/highlights.scm`, the parser for the +--- `lua` language will be used. +---@param buf (integer) Buffer handle +---@param opts (QueryLinterOpts|nil) Optional keyword arguments: +--- - langs (string|string[]|nil) Language(s) to use for checking the query. +--- If multiple languages are specified, queries are validated for all of them +--- - clear (boolean) if `true`, just clear current lint errors +function M.lint(buf, opts) + if opts and opts.clear then + require('vim.treesitter._query_linter').clear(buf) + else + require('vim.treesitter._query_linter').lint(buf, opts) + end +end + return M -- cgit From 668f16bac779ac52d7bd9452e6001a7a6d1e9965 Mon Sep 17 00:00:00 2001 From: Christian Clason Date: Sun, 30 Apr 2023 11:01:54 +0200 Subject: feat(treesitter): upstream query omnifunc from playground (#23394) and set by default in `ftplugin/query.lua` --- runtime/lua/vim/treesitter/_query_linter.lua | 60 ++++++++++++++++++++++++++-- runtime/lua/vim/treesitter/query.lua | 12 +++++- 2 files changed, 68 insertions(+), 4 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 62f28d3097..ecdee5fc95 100644 --- a/runtime/lua/vim/treesitter/_query_linter.lua +++ b/runtime/lua/vim/treesitter/_query_linter.lua @@ -1,4 +1,6 @@ -local namespace = vim.api.nvim_create_namespace('vim.treesitter.query_linter') +local api = vim.api + +local namespace = api.nvim_create_namespace('vim.treesitter.query_linter') -- those node names exist for every language local BUILT_IN_NODE_NAMES = { '_', 'ERROR' } @@ -49,7 +51,7 @@ end --- @param buf integer --- @return string? local function guess_query_lang(buf) - local filename = vim.api.nvim_buf_get_name(buf) + local filename = api.nvim_buf_get_name(buf) if filename ~= '' then local ok, query_lang = pcall(vim.fn.fnamemodify, filename, ':p:h:t') if ok then @@ -256,7 +258,7 @@ end --- @param opts QueryLinterOpts|QueryLinterNormalizedOpts|nil Options for linting function M.lint(buf, opts) if buf == 0 then - buf = vim.api.nvim_get_current_buf() + buf = api.nvim_get_current_buf() end local diagnostics = {} @@ -299,4 +301,56 @@ function M.clear(buf) vim.diagnostic.reset(namespace, buf) end +--- @private +--- @param findstart integer +--- @param base string +function M.omnifunc(findstart, base) + if findstart == 1 then + local result = + api.nvim_get_current_line():sub(1, api.nvim_win_get_cursor(0)[2]):find('["#%-%w]*$') + return result - 1 + end + + local buf = api.nvim_get_current_buf() + local query_lang = guess_query_lang(buf) + + local ok, parser_info = pcall(vim.treesitter.language.inspect, query_lang) + if not ok then + return -2 + end + + local items = {} + for _, f in pairs(parser_info.fields) do + if f:find(base, 1, true) then + table.insert(items, f .. ':') + end + end + for _, p in pairs(vim.treesitter.query.list_predicates()) do + local text = '#' .. p + local found = text:find(base, 1, true) + if found and found <= 2 then -- with or without '#' + table.insert(items, text) + end + text = '#not-' .. p + found = text:find(base, 1, true) + if found and found <= 2 then -- with or without '#' + table.insert(items, text) + end + end + for _, p in pairs(vim.treesitter.query.list_directives()) do + local text = '#' .. p + local found = text:find(base, 1, true) + if found and found <= 2 then -- with or without '#' + table.insert(items, text) + end + end + for _, s in pairs(parser_info.symbols) do + local text = s[2] and s[1] or '"' .. s[1]:gsub([[\]], [[\\]]) .. '"' + if text:find(base, 1, true) then + table.insert(items, text) + end + end + return { words = items, refresh = 'always' } +end + return M diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index 492bfd1ffb..93841bb31e 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -728,7 +728,7 @@ end --- --- The found diagnostics are reported using |diagnostic-api|. --- By default, the parser used for verification is determined by the containing folder ---- of the query file, e.g., if the path is `**/lua/highlights.scm`, the parser for the +--- of the query file, e.g., if the path ends in `/lua/highlights.scm`, the parser for the --- `lua` language will be used. ---@param buf (integer) Buffer handle ---@param opts (QueryLinterOpts|nil) Optional keyword arguments: @@ -743,4 +743,14 @@ function M.lint(buf, opts) end end +--- Omnifunc for completing node names and predicates in treesitter queries. +--- +--- Use via +---
lua
+---   vim.bo.omnifunc = 'v:lua.vim.treesitter.query.omnifunc'
+--- 
+function M.omnifunc(findstart, base) + return require('vim.treesitter._query_linter').omnifunc(findstart, base) +end + return M -- cgit From 19a793545f15bb7e0bac2fc8f705c600e8f9c9bb Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Sun, 30 Apr 2023 16:11:38 +0100 Subject: fix(treesitter): redraw added/removed injections properly (#23287) When injections are added or removed make sure to: - invoke 'changedtree' callbacks for when new trees are added. - invoke 'changedtree' callbacks for when trees are invalidated - redraw regions when languagetree children are removed --- runtime/lua/vim/treesitter/highlighter.lua | 20 ++++--- runtime/lua/vim/treesitter/languagetree.lua | 81 +++++++++++++++++++---------- 2 files changed, 68 insertions(+), 33 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/highlighter.lua b/runtime/lua/vim/treesitter/highlighter.lua index ac2a929487..4bb764c5c6 100644 --- a/runtime/lua/vim/treesitter/highlighter.lua +++ b/runtime/lua/vim/treesitter/highlighter.lua @@ -76,9 +76,6 @@ function TSHighlighter.new(tree, opts) opts = opts or {} ---@type { queries: table } self.tree = tree tree:register_cbs({ - on_changedtree = function(...) - self:on_changedtree(...) - end, on_bytes = function(...) self:on_bytes(...) end, @@ -87,6 +84,17 @@ function TSHighlighter.new(tree, opts) end, }) + tree:register_cbs({ + on_changedtree = function(...) + self:on_changedtree(...) + end, + on_child_removed = function(child) + child:for_each_tree(function(t) + self:on_changedtree(t:included_ranges(true)) + end) + end, + }, true) + self.bufnr = tree:source() --[[@as integer]] self.edit_count = 0 self.redraw_count = 0 @@ -177,10 +185,10 @@ function TSHighlighter:on_detach() end ---@package ----@param changes integer[][]? +---@param changes Range6[][] function TSHighlighter:on_changedtree(changes) - for _, ch in ipairs(changes or {}) do - api.nvim__buf_redraw_range(self.bufnr, ch[1], ch[3] + 1) + for _, ch in ipairs(changes) do + api.nvim__buf_redraw_range(self.bufnr, ch[1], ch[4] + 1) end end diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index 4aa07d1b96..19cea32367 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -51,8 +51,18 @@ local Range = require('vim.treesitter._range') ---| 'on_child_added' ---| 'on_child_removed' +--- @type table +local TSCallbackNames = { + on_changedtree = 'changedtree', + on_bytes = 'bytes', + on_detach = 'detach', + on_child_added = 'child_added', + on_child_removed = 'child_removed', +} + ---@class LanguageTree ---@field private _callbacks table Callback handlers +---@field package _callbacks_rec table Callback handlers (recursive) ---@field private _children table Injected languages ---@field private _injection_query Query Queries defining injected languages ---@field private _opts table Options @@ -79,7 +89,7 @@ LanguageTree.__index = LanguageTree --- "injected" language parsers, which themselves may inject other languages, recursively. --- ---@param source (integer|string) Buffer or text string to parse ----@param lang string|nil Root language of this tree +---@param lang string Root language of this tree ---@param opts (table|nil) Optional arguments: --- - injections table Map of language to injection query strings. Overrides the --- built-in runtime file searching for language injections. @@ -100,15 +110,15 @@ function LanguageTree.new(source, lang, opts) or query.get(lang, 'injections'), _valid = false, _parser = vim._create_ts_parser(lang), - _callbacks = { - changedtree = {}, - bytes = {}, - detach = {}, - child_added = {}, - child_removed = {}, - }, + _callbacks = {}, + _callbacks_rec = {}, }, LanguageTree) + for _, name in pairs(TSCallbackNames) do + self._callbacks[name] = {} + self._callbacks_rec[name] = {} + end + return self end @@ -121,6 +131,7 @@ local function tcall(f, ...) local start = vim.loop.hrtime() ---@diagnostic disable-next-line local r = { f(...) } + --- @type number local duration = (vim.loop.hrtime() - start) / 1000000 return duration, unpack(r) end @@ -161,6 +172,9 @@ function LanguageTree:invalidate(reload) -- buffer was reloaded, reparse all trees if reload then + for _, t in ipairs(self._trees) do + self:_do_callback('changedtree', t:included_ranges(true), t) + end self._trees = {} end @@ -245,9 +259,12 @@ function LanguageTree:parse() if not self._valid or not self._valid[i] then self._parser:set_included_ranges(ranges) local parse_time, tree, tree_changes = - tcall(self._parser.parse, self._parser, self._trees[i], self._source) + tcall(self._parser.parse, self._parser, self._trees[i], self._source, true) + + -- Pass ranges if this is an initial parse + local cb_changes = self._trees[i] and tree_changes or ranges - self:_do_callback('changedtree', tree_changes, tree) + self:_do_callback('changedtree', cb_changes, tree) self._trees[i] = tree vim.list_extend(changes, tree_changes) @@ -341,7 +358,14 @@ function LanguageTree:add_child(lang) self:remove_child(lang) end - self._children[lang] = LanguageTree.new(self._source, lang, self._opts) + local child = LanguageTree.new(self._source, lang, self._opts) + + -- Inherit recursive callbacks + for nm, cb in pairs(self._callbacks_rec) do + vim.list_extend(child._callbacks_rec[nm], cb) + end + + self._children[lang] = child self:invalidate() self:_do_callback('child_added', self._children[lang]) @@ -453,6 +477,10 @@ function LanguageTree:set_included_regions(new_regions) end if #self:included_regions() ~= #new_regions then + -- TODO(lewis6991): inefficient; invalidate trees incrementally + for _, t in ipairs(self._trees) do + self:_do_callback('changedtree', t:included_ranges(true), t) + end self._trees = {} self:invalidate() else @@ -707,6 +735,9 @@ function LanguageTree:_do_callback(cb_name, ...) for _, cb in ipairs(self._callbacks[cb_name]) do cb(...) end + for _, cb in ipairs(self._callbacks_rec[cb_name]) do + cb(...) + end end ---@package @@ -855,30 +886,26 @@ end --- changed. --- - `on_child_added` : emitted when a child is added to the tree. --- - `on_child_removed` : emitted when a child is removed from the tree. -function LanguageTree:register_cbs(cbs) +--- @param recursive? boolean Apply callbacks recursively for all children. Any new children will +--- also inherit the callbacks. +function LanguageTree:register_cbs(cbs, recursive) ---@cast cbs table if not cbs then return end - if cbs.on_changedtree then - table.insert(self._callbacks.changedtree, cbs.on_changedtree) - end - - if cbs.on_bytes then - table.insert(self._callbacks.bytes, cbs.on_bytes) - end + local callbacks = recursive and self._callbacks_rec or self._callbacks - if cbs.on_detach then - table.insert(self._callbacks.detach, cbs.on_detach) - end - - if cbs.on_child_added then - table.insert(self._callbacks.child_added, cbs.on_child_added) + for name, cbname in pairs(TSCallbackNames) do + if cbs[name] then + table.insert(callbacks[cbname], cbs[name]) + end end - if cbs.on_child_removed then - table.insert(self._callbacks.child_removed, cbs.on_child_removed) + if recursive then + self:for_each_child(function(child) + child:register_cbs(cbs, true) + end) end end -- cgit From 26cc946226d96bf6b474d850b961e1060346c96f Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Mon, 1 May 2023 10:32:29 +0100 Subject: fix(treesitter): foldexpr tweaks Some small general fixes found working on developing async parsing. --- runtime/lua/vim/treesitter/_fold.lua | 40 ++++++++++++++++++++++++----- runtime/lua/vim/treesitter/languagetree.lua | 2 +- 2 files changed, 34 insertions(+), 8 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/_fold.lua b/runtime/lua/vim/treesitter/_fold.lua index 7df93d1b2e..51e60bf495 100644 --- a/runtime/lua/vim/treesitter/_fold.lua +++ b/runtime/lua/vim/treesitter/_fold.lua @@ -1,3 +1,5 @@ +local ts = vim.treesitter + local Range = require('vim.treesitter._range') local api = vim.api @@ -90,21 +92,45 @@ local function trim_level(level) return level end +--- If a parser doesn't have any ranges explicitly set, treesitter will +--- return a range with end_row and end_bytes with a value of UINT32_MAX, +--- so clip end_row to the max buffer line. +--- +--- TODO(lewis6991): Handle this generally +--- +--- @param bufnr integer +--- @param erow integer? +--- @return integer +local function normalise_erow(bufnr, erow) + local max_erow = api.nvim_buf_line_count(bufnr) - 1 + return math.min(erow or max_erow, max_erow) +end + ---@param bufnr integer ---@param info TS.FoldInfo ---@param srow integer? ---@param erow integer? local function get_folds_levels(bufnr, info, srow, erow) + if not api.nvim_buf_is_valid(bufnr) then + return false + end + srow = srow or 0 - erow = erow or api.nvim_buf_line_count(bufnr) + erow = normalise_erow(bufnr, erow) info:invalidate_range(srow, erow) local prev_start = -1 local prev_stop = -1 - vim.treesitter.get_parser(bufnr):for_each_tree(function(tree, ltree) - local query = vim.treesitter.query.get(ltree:lang(), 'folds') + local parser = ts.get_parser(bufnr) + + if not parser:is_valid() then + return + end + + parser:for_each_tree(function(tree, ltree) + local query = ts.query.get(ltree:lang(), 'folds') if not query then return end @@ -112,9 +138,9 @@ local function get_folds_levels(bufnr, info, srow, erow) -- erow in query is end-exclusive local q_erow = erow and erow + 1 or -1 - for id, node, metadata in query:iter_captures(tree:root(), bufnr, srow or 0, q_erow) do + for id, node, metadata in query:iter_captures(tree:root(), bufnr, srow, q_erow) do if query.captures[id] == 'fold' then - local range = vim.treesitter.get_range(node, bufnr, metadata[id]) + local range = ts.get_range(node, bufnr, metadata[id]) local start, _, stop, stop_col = Range.unpack4(range) if stop_col == 0 then @@ -226,7 +252,7 @@ function M.foldexpr(lnum) lnum = lnum or vim.v.lnum local bufnr = api.nvim_get_current_buf() - if not vim.treesitter._has_parser(bufnr) or not lnum then + if not ts._has_parser(bufnr) or not lnum then return '0' end @@ -234,7 +260,7 @@ function M.foldexpr(lnum) foldinfos[bufnr] = FoldInfo.new() get_folds_levels(bufnr, foldinfos[bufnr]) - local parser = vim.treesitter.get_parser(bufnr) + local parser = ts.get_parser(bufnr) parser:register_cbs({ on_changedtree = function(tree_changes) on_changedtree(bufnr, foldinfos[bufnr], tree_changes) diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index 19cea32367..1adf6759fa 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -262,7 +262,7 @@ function LanguageTree:parse() tcall(self._parser.parse, self._parser, self._trees[i], self._source, true) -- Pass ranges if this is an initial parse - local cb_changes = self._trees[i] and tree_changes or ranges + local cb_changes = self._trees[i] and tree_changes or tree:included_ranges(true) self:_do_callback('changedtree', cb_changes, tree) self._trees[i] = tree -- cgit From fba18a3b62310f4535d979a05288101b9af2ef50 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Tue, 2 May 2023 10:07:18 +0100 Subject: fix(treesitter): do not calc folds on unloaded buffers Fixes #23423 --- runtime/lua/vim/treesitter/_fold.lua | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/_fold.lua b/runtime/lua/vim/treesitter/_fold.lua index 51e60bf495..edceb8217a 100644 --- a/runtime/lua/vim/treesitter/_fold.lua +++ b/runtime/lua/vim/treesitter/_fold.lua @@ -111,10 +111,6 @@ end ---@param srow integer? ---@param erow integer? local function get_folds_levels(bufnr, info, srow, erow) - if not api.nvim_buf_is_valid(bufnr) then - return false - end - srow = srow or 0 erow = normalise_erow(bufnr, erow) @@ -210,13 +206,25 @@ local function recompute_folds() vim._foldupdate() end +--- Schedule a function only if bufnr is loaded +---@param bufnr integer +---@param fn function +local function schedule_if_loaded(bufnr, fn) + vim.schedule(function() + if not api.nvim_buf_is_loaded(bufnr) then + return + end + fn() + end) +end + ---@param bufnr integer ---@param foldinfo TS.FoldInfo ---@param tree_changes Range4[] local function on_changedtree(bufnr, foldinfo, tree_changes) -- For some reason, queries seem to use the old buffer state in on_bytes. -- Get around this by scheduling and manually updating folds. - vim.schedule(function() + schedule_if_loaded(bufnr, function() for _, change in ipairs(tree_changes) do local srow, _, erow = Range.unpack4(change) get_folds_levels(bufnr, foldinfo, srow, erow) @@ -238,7 +246,7 @@ local function on_bytes(bufnr, foldinfo, start_row, old_row, new_row) foldinfo:remove_range(end_row_new, end_row_old) elseif new_row > old_row then foldinfo:add_range(start_row, end_row_new) - vim.schedule(function() + schedule_if_loaded(bufnr, function() get_folds_levels(bufnr, foldinfo, start_row, end_row_new) recompute_folds() end) -- cgit From 3ba930844c302dc43d32a30ed453667409596c4a Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Tue, 2 May 2023 22:27:14 +0100 Subject: perf(treesitter): insert/remove items efficiently (#23443) --- runtime/lua/vim/treesitter/_fold.lua | 63 +++++++++++++++++++++++++++++------- 1 file changed, 52 insertions(+), 11 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/_fold.lua b/runtime/lua/vim/treesitter/_fold.lua index edceb8217a..f6425d7cb9 100644 --- a/runtime/lua/vim/treesitter/_fold.lua +++ b/runtime/lua/vim/treesitter/_fold.lua @@ -34,15 +34,58 @@ function FoldInfo:invalidate_range(srow, erow) end 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 function FoldInfo:remove_range(srow, erow) - for i = erow - 1, srow, -1 do - table.remove(self.levels, i + 1) - table.remove(self.levels0, i + 1) - table.remove(self.start_counts, i + 1) - table.remove(self.stop_counts, i + 1) + list_remove(self.levels, srow + 1, erow) + list_remove(self.levels0, srow + 1, erow) + list_remove(self.start_counts, srow + 1, erow) + list_remove(self.stop_counts, 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 end @@ -50,12 +93,10 @@ end ---@param srow integer ---@param erow integer function FoldInfo:add_range(srow, erow) - for i = srow, erow - 1 do - table.insert(self.levels, i + 1, '-1') - table.insert(self.levels0, i + 1, -1) - table.insert(self.start_counts, i + 1, nil) - table.insert(self.stop_counts, i + 1, nil) - end + list_insert(self.levels, srow + 1, erow, '-1') + list_insert(self.levels0, srow + 1, erow, -1) + list_insert(self.start_counts, srow + 1, erow, nil) + list_insert(self.stop_counts, srow + 1, erow, nil) end ---@package -- cgit From af040c3a079f6e25db0ad6b908aa1327f67deb82 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Thu, 11 May 2023 11:13:32 +0100 Subject: feat(treesitter): add support for setting query depths --- runtime/lua/vim/treesitter/_meta.lua | 14 ++++++++------ runtime/lua/vim/treesitter/query.lua | 8 ++++++-- 2 files changed, 14 insertions(+), 8 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/_meta.lua b/runtime/lua/vim/treesitter/_meta.lua index 4d0f43d030..c1009f5f5d 100644 --- a/runtime/lua/vim/treesitter/_meta.lua +++ b/runtime/lua/vim/treesitter/_meta.lua @@ -31,17 +31,19 @@ local TSNode = {} ---@param query userdata ---@param captures true ----@param start integer ----@param end_ integer +---@param start? integer +---@param end_? integer +---@param opts? table ---@return fun(): integer, TSNode, any -function TSNode:_rawquery(query, captures, start, end_) end +function TSNode:_rawquery(query, captures, start, end_, opts) end ---@param query userdata ---@param captures false ----@param start integer ----@param end_ integer +---@param start? integer +---@param end_? integer +---@param opts? table ---@return fun(): string, any -function TSNode:_rawquery(query, captures, start, end_) end +function TSNode:_rawquery(query, captures, start, end_, opts) end ---@class TSParser ---@field parse fun(self: TSParser, tree: TSTree?, source: integer|string, include_bytes: boolean?): TSTree, integer[] diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index 93841bb31e..e6a117557a 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -686,16 +686,20 @@ end ---@param source (integer|string) Source buffer or string to search ---@param start integer Starting line for the search ---@param stop integer Stopping line for the search (end-exclusive) +---@param opts table|nil Options: +--- - max_start_depth (integer) if non-zero, sets the maximum start depth +--- for each match. This is used to prevent traversing too deep into a tree. +--- Requires treesitter >= 0.20.9. --- ---@return (fun(): integer, table, table): pattern id, match, metadata -function Query:iter_matches(node, source, start, stop) +function Query:iter_matches(node, source, start, stop, opts) if type(source) == 'number' and source == 0 then source = api.nvim_get_current_buf() end start, stop = value_or_node_range(start, stop, node) - local raw_iter = node:_rawquery(self.query, false, start, stop) + local raw_iter = node:_rawquery(self.query, false, start, stop, opts) ---@cast raw_iter fun(): string, any local function iter() local pattern, match = raw_iter() -- cgit From 32dc484ec9ec2d86a5fc7127e37f1ef115b9be76 Mon Sep 17 00:00:00 2001 From: Christian Clason Date: Sat, 13 May 2023 13:29:11 +0200 Subject: fix(treesitter): support subfiletypes in get_lang (#23605) --- runtime/lua/vim/treesitter/language.lua | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/language.lua b/runtime/lua/vim/treesitter/language.lua index b616d4d70b..08c297c9ad 100644 --- a/runtime/lua/vim/treesitter/language.lua +++ b/runtime/lua/vim/treesitter/language.lua @@ -27,6 +27,11 @@ function M.get_lang(filetype) if filetype == '' then return end + if ft_to_lang[filetype] then + return ft_to_lang[filetype] + end + -- support subfiletypes like html.glimmer + filetype = vim.split(filetype, '.', { plain = true })[1] return ft_to_lang[filetype] end -- cgit From 08991b078267e5de0a19a136d00d4f71ad651a32 Mon Sep 17 00:00:00 2001 From: dundargoc <33953936+dundargoc@users.noreply.github.com> Date: Sat, 13 May 2023 21:33:22 +0200 Subject: docs: small fixes Co-authored-by: Christian Clason Co-authored-by: Gregory Anders Co-authored-by: HiPhish Co-authored-by: Julio B Co-authored-by: T727 <74924917+T-727@users.noreply.github.com> Co-authored-by: camoz Co-authored-by: champignoom <66909116+champignoom@users.noreply.github.com> --- runtime/lua/vim/treesitter/languagetree.lua | 6 ++++-- runtime/lua/vim/treesitter/query.lua | 4 ++-- 2 files changed, 6 insertions(+), 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 1adf6759fa..0efe3af85c 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -882,10 +882,12 @@ end ---@param cbs table An |nvim_buf_attach()|-like table argument with the following handlers: --- - `on_bytes` : see |nvim_buf_attach()|, but this will be called _after_ the parsers callback. --- - `on_changedtree` : a callback that will be called every time the tree has syntactical changes. ---- It will only be passed one argument, which is a table of the ranges (as node ranges) that ---- changed. +--- It will be passed two arguments: a table of the ranges (as node ranges) that +--- changed and the changed tree. --- - `on_child_added` : emitted when a child is added to the tree. --- - `on_child_removed` : emitted when a child is removed from the tree. +--- - `on_detach` : emitted when the buffer is detached, see |nvim_buf_detach_event|. +--- Takes one argument, the number of the buffer. --- @param recursive? boolean Apply callbacks recursively for all children. Any new children will --- also inherit the callbacks. function LanguageTree:register_cbs(cbs, recursive) diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index e6a117557a..75e5bf8870 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -609,10 +609,10 @@ end --- --- {source} is needed 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_row} and {end_row} can be used to limit +--- 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 {end} row values are used from the given node. +--- viewport). When omitted, the {start} and {stop} row values are used from the given node. --- --- The iterator returns three values: a numeric id identifying the capture, --- the captured node, and metadata from any directives processing the match. -- cgit From 9ff59517cbf309d31f979a49b7dc82b237ecfcc4 Mon Sep 17 00:00:00 2001 From: Christian Clason Date: Sat, 13 May 2023 12:56:21 +0200 Subject: fix(treesitter): update c queries --- runtime/lua/vim/treesitter/query.lua | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index 75e5bf8870..73b561c777 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -382,6 +382,39 @@ local predicate_handlers = { return string_set[node_text] end, + + ['has-ancestor?'] = function(match, _, _, predicate) + local node = match[predicate[2]] + if not node then + return true + end + + local ancestor_types = {} + for _, type in ipairs({ unpack(predicate, 3) }) do + ancestor_types[type] = true + end + + node = node:parent() + while node do + if ancestor_types[node:type()] then + return true + end + node = node:parent() + end + return false + end, + + ['has-parent?'] = function(match, _, _, predicate) + local node = match[predicate[2]] + if not node then + return true + end + + if vim.list_contains({ unpack(predicate, 3) }, node:parent():type()) then + return true + end + return false + end, } -- As we provide lua-match? also expose vim-match? -- cgit From 6b19170d44ca56cf65542ee184d2bc89c6d622a9 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Tue, 16 May 2023 16:41:47 +0100 Subject: fix(treesitter): correctly calculate bytes for text sources (#23655) Fixes #20419 --- runtime/lua/vim/treesitter/_range.lua | 38 ++++++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 12 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/_range.lua b/runtime/lua/vim/treesitter/_range.lua index f4db5016ac..35081c6400 100644 --- a/runtime/lua/vim/treesitter/_range.lua +++ b/runtime/lua/vim/treesitter/_range.lua @@ -143,6 +143,29 @@ function M.contains(r1, r2) return true end +--- @param source integer|string +--- @param index integer +--- @return integer +local function get_offset(source, index) + if index == 0 then + return 0 + end + + if type(source) == 'number' then + return api.nvim_buf_get_offset(source, index) + end + + local byte = 0 + local next_offset = source:gmatch('()\n') + local line = 1 + while line <= index do + byte = next_offset() --[[@as integer]] + line = line + 1 + end + + return byte +end + ---@private ---@param source integer|string ---@param range Range @@ -152,19 +175,10 @@ function M.add_bytes(source, range) return range --[[@as Range6]] end - local start_row, start_col, end_row, end_col = range[1], range[2], range[3], range[4] - local start_byte = 0 - local end_byte = 0 + local start_row, start_col, end_row, end_col = M.unpack4(range) -- TODO(vigoux): proper byte computation here, and account for EOL ? - if type(source) == 'number' then - -- Easy case, this is a buffer parser - start_byte = api.nvim_buf_get_offset(source, start_row) + start_col - end_byte = api.nvim_buf_get_offset(source, end_row) + end_col - elseif type(source) == 'string' then - -- string parser, single `\n` delimited string - start_byte = vim.fn.byteidx(source, start_col) - end_byte = vim.fn.byteidx(source, end_col) - end + local start_byte = get_offset(source, start_row) + start_col + local end_byte = get_offset(source, end_row) + end_col return { start_row, start_col, start_byte, end_row, end_col, end_byte } end -- cgit From 189fb6203262340e7a59e782be970bcd8ae28e61 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Wed, 17 May 2023 11:42:18 +0100 Subject: feat(treesitter): improved logging (#23638) - Add bindings to Treesitter ts_parser_set_logger and ts_parser_logger - Add logfile with path STDPATH('log')/treesitter.c - Rework existing LanguageTree loggin to use logfile - Begin implementing log levels for vim.g.__ts_debug --- runtime/lua/vim/treesitter/_meta.lua | 4 ++ runtime/lua/vim/treesitter/languagetree.lua | 67 +++++++++++++++++++++++++---- 2 files changed, 62 insertions(+), 9 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/_meta.lua b/runtime/lua/vim/treesitter/_meta.lua index c1009f5f5d..9ca4b560c6 100644 --- a/runtime/lua/vim/treesitter/_meta.lua +++ b/runtime/lua/vim/treesitter/_meta.lua @@ -45,6 +45,8 @@ function TSNode:_rawquery(query, captures, start, end_, opts) end ---@return fun(): string, any function TSNode:_rawquery(query, captures, start, end_, opts) end +---@alias TSLoggerCallback fun(logtype: 'parse'|'lex', msg: string) + ---@class TSParser ---@field parse fun(self: TSParser, tree: TSTree?, source: integer|string, include_bytes: boolean?): TSTree, integer[] ---@field reset fun(self: TSParser) @@ -52,6 +54,8 @@ function TSNode:_rawquery(query, captures, start, end_, opts) end ---@field set_included_ranges fun(self: TSParser, ranges: Range6[]) ---@field set_timeout fun(self: TSParser, timeout: integer) ---@field timeout fun(self: TSParser): integer +---@field _set_logger fun(self: TSParser, lex: boolean, parse: boolean, cb: TSLoggerCallback) +---@field _logger fun(self: TSParser): TSLoggerCallback ---@class TSTree ---@field root fun(self: TSTree): TSNode diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index 0efe3af85c..6c780f33c4 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -31,8 +31,17 @@ --- shouldn't be done directly in the change callback anyway as they will be very frequent. Rather --- a plugin that does any kind of analysis on a tree should use a timer to throttle too frequent --- updates. +--- + +-- Debugging: +-- +-- vim.g.__ts_debug levels: +-- - 1. Messages from languagetree.lua +-- - 2. Parse messages from treesitter +-- - 2. Lex messages from treesitter +-- +-- Log file can be found in stdpath('log')/treesitter.log -local api = vim.api local query = require('vim.treesitter.query') local language = require('vim.treesitter.language') local Range = require('vim.treesitter._range') @@ -75,6 +84,8 @@ local TSCallbackNames = { ---@field private _source (integer|string) Buffer or string to parse ---@field private _trees TSTree[] Reference to parsed tree (one for each language) ---@field private _valid boolean|table If the parsed tree is valid +---@field private _logger? fun(logtype: string, msg: string) +---@field private _logfile? file* local LanguageTree = {} ---@class LanguageTreeOpts @@ -114,6 +125,10 @@ function LanguageTree.new(source, lang, opts) _callbacks_rec = {}, }, LanguageTree) + if vim.g.__ts_debug and type(vim.g.__ts_debug) == 'number' then + self:_set_logger() + end + for _, name in pairs(TSCallbackNames) do self._callbacks[name] = {} self._callbacks_rec[name] = {} @@ -122,6 +137,33 @@ function LanguageTree.new(source, lang, opts) return self end +function LanguageTree:_set_logger() + local source = self:source() + source = type(source) == 'string' and 'text' or tostring(source) + + local lang = self:lang() + + local logfilename = vim.fs._join_paths(vim.fn.stdpath('log'), 'treesitter.log') + + local logfile, openerr = io.open(logfilename, 'a+') + + if not logfile or openerr then + error(string.format('Could not open file (%s) for logging: %s', logfilename, openerr)) + return + end + + self._logfile = logfile + + self._logger = function(logtype, msg) + self._logfile:write(string.format('%s:%s:(%s) %s\n', source, lang, logtype, msg)) + self._logfile:flush() + end + + local log_lex = vim.g.__ts_debug >= 3 + local log_parse = vim.g.__ts_debug >= 2 + self._parser:_set_logger(log_lex, log_parse, self._logger) +end + ---@private ---Measure execution time of a function ---@generic R1, R2, R3 @@ -139,7 +181,11 @@ end ---@private ---@vararg any function LanguageTree:_log(...) - if vim.g.__ts_debug == nil then + if not self._logger then + return + end + + if not vim.g.__ts_debug or vim.g.__ts_debug < 1 then return end @@ -150,19 +196,17 @@ function LanguageTree:_log(...) local info = debug.getinfo(2, 'nl') local nregions = #self:included_regions() - local prefix = - string.format('%s:%d: [%s:%d] ', info.name, info.currentline, self:lang(), nregions) + local prefix = string.format('%s:%d: (#regions=%d) ', info.name, info.currentline, nregions) - api.nvim_out_write(prefix) + local msg = { prefix } for _, x in ipairs(args) do if type(x) == 'string' then - api.nvim_out_write(x) + msg[#msg + 1] = x else - api.nvim_out_write(vim.inspect(x, { newline = ' ', indent = '' })) + msg[#msg + 1] = vim.inspect(x, { newline = ' ', indent = '' }) end - api.nvim_out_write(' ') end - api.nvim_out_write('\n') + self._logger('nvim', table.concat(msg, ' ')) end --- Invalidates this parser and all its children @@ -876,6 +920,11 @@ end function LanguageTree:_on_detach(...) self:invalidate(true) self:_do_callback('detach', ...) + if self._logfile then + self._logger('nvim', 'detaching') + self._logger = nil + self._logfile:close() + end end --- Registers callbacks for the |LanguageTree|. -- cgit From ef64e225f6f6c01280aa8472bebe812016f357bf Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Thu, 18 May 2023 10:52:01 +0100 Subject: fix(treesitter): allow foldexpr without highlights (#23672) Ref nvim-treesitter/nvim-treesitter#4748 --- runtime/lua/vim/treesitter/_fold.lua | 4 ++-- 1 file changed, 2 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 f6425d7cb9..a8f8c7967e 100644 --- a/runtime/lua/vim/treesitter/_fold.lua +++ b/runtime/lua/vim/treesitter/_fold.lua @@ -301,7 +301,8 @@ function M.foldexpr(lnum) lnum = lnum or vim.v.lnum local bufnr = api.nvim_get_current_buf() - if not ts._has_parser(bufnr) or not lnum then + local parser = vim.F.npcall(ts.get_parser, bufnr) + if not parser then return '0' end @@ -309,7 +310,6 @@ function M.foldexpr(lnum) foldinfos[bufnr] = FoldInfo.new() get_folds_levels(bufnr, foldinfos[bufnr]) - local parser = ts.get_parser(bufnr) parser:register_cbs({ on_changedtree = function(tree_changes) on_changedtree(bufnr, foldinfos[bufnr], tree_changes) -- cgit From 44d4ae448d0ab357a3aade773ea58c66c3c969cc Mon Sep 17 00:00:00 2001 From: Matthieu Coudron Date: Fri, 26 May 2023 21:51:18 +0200 Subject: fix: function was renamed (#23772) --- 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 6c780f33c4..244d88f3e0 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -143,7 +143,7 @@ function LanguageTree:_set_logger() local lang = self:lang() - local logfilename = vim.fs._join_paths(vim.fn.stdpath('log'), 'treesitter.log') + local logfilename = vim.fs.joinpath(vim.fn.stdpath('log'), 'treesitter.log') local logfile, openerr = io.open(logfilename, 'a+') -- cgit From 2db719f6c2b677fcbc197b02fe52764a851523b2 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Sat, 3 Jun 2023 11:06:00 +0100 Subject: feat(lua): rename vim.loop -> vim.uv (#22846) --- 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 244d88f3e0..cabfa8ccc0 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -170,11 +170,11 @@ end ---@param f fun(): R1, R2, R2 ---@return integer, R1, R2, R3 local function tcall(f, ...) - local start = vim.loop.hrtime() + local start = vim.uv.hrtime() ---@diagnostic disable-next-line local r = { f(...) } --- @type number - local duration = (vim.loop.hrtime() - start) / 1000000 + local duration = (vim.uv.hrtime() - start) / 1000000 return duration, unpack(r) end -- cgit From c48b1421af28d0317c807bca00c7e2fff97d9ad0 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Tue, 6 Jun 2023 08:23:20 -0700 Subject: refactor!: rename "playground" => "dev" #23919 Problem: "playground" is new jargon that overlaps with existing concepts: "dev" (`:help dev`) and "view" (also "scratch" `:help scratch-buffer`) . Solution: We should consistently use "dev" as the namespace for where "developer tools" live. For purposes of a "throwaway sandbox object", we can use the name "view". - Rename `TSPlayground` => `TSView` - Rename `playground.lua` => `dev.lua` --- runtime/lua/vim/treesitter/dev.lua | 441 ++++++++++++++++++++++++++++++ runtime/lua/vim/treesitter/playground.lua | 439 ----------------------------- 2 files changed, 441 insertions(+), 439 deletions(-) create mode 100644 runtime/lua/vim/treesitter/dev.lua delete mode 100644 runtime/lua/vim/treesitter/playground.lua (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/dev.lua b/runtime/lua/vim/treesitter/dev.lua new file mode 100644 index 0000000000..99cd147658 --- /dev/null +++ b/runtime/lua/vim/treesitter/dev.lua @@ -0,0 +1,441 @@ +local api = vim.api + +---@class TSDevModule +local M = {} + +---@class TSTreeView +---@field ns integer API namespace +---@field opts table Options table with the following keys: +--- - anon (boolean): If true, display anonymous nodes +--- - lang (boolean): If true, display the language alongside each node +---@field nodes TSP.Node[] +---@field named TSP.Node[] +local TSTreeView = {} + +---@class TSP.Node +---@field id integer Node id +---@field text string Node text +---@field named boolean True if this is a named (non-anonymous) node +---@field depth integer Depth of the node within the tree +---@field lnum integer Beginning line number of this node in the source buffer +---@field col integer Beginning column number of this node in the source buffer +---@field end_lnum integer Final line number of this node in the source buffer +---@field end_col integer Final column number of this node in the source buffer +---@field lang string Source language of this node +---@field root TSNode + +--- Traverse all child nodes starting at {node}. +--- +--- This is a recursive function. The {depth} parameter indicates the current recursion level. +--- {lang} is a string indicating the language of the tree currently being traversed. Each traversed +--- node is added to {tree}. When recursion completes, {tree} is an array of all nodes in the order +--- they were visited. +--- +--- {injections} is a table mapping node ids from the primary tree to language tree injections. Each +--- injected language has a series of trees nested within the primary language's tree, and the root +--- node of each of these trees is contained within a node in the primary tree. The {injections} +--- table maps nodes in the primary tree to root nodes of injected trees. +--- +---@param node TSNode Starting node to begin traversal |tsnode| +---@param depth integer Current recursion depth +---@param lang string Language of the tree currently being traversed +---@param injections table Mapping of node ids to root nodes of injected language trees (see +--- explanation above) +---@param tree TSP.Node[] Output table containing a list of tables each representing a node in the tree +---@private +local function traverse(node, depth, lang, injections, tree) + local injection = injections[node:id()] + if injection then + traverse(injection.root, depth, injection.lang, injections, tree) + end + + for child, field in node:iter_children() do + local type = child:type() + local lnum, col, end_lnum, end_col = child:range() + local named = child:named() + local text ---@type string + if named then + if field then + text = string.format('%s: (%s)', field, type) + else + text = string.format('(%s)', type) + end + else + text = string.format('"%s"', type:gsub('\n', '\\n')) + end + + table.insert(tree, { + id = child:id(), + text = text, + named = named, + depth = depth, + lnum = lnum, + col = col, + end_lnum = end_lnum, + end_col = end_col, + lang = lang, + }) + + traverse(child, depth + 1, lang, injections, tree) + end + + return tree +end + +--- Create a new treesitter view. +--- +---@param bufnr integer Source buffer number +---@param lang string|nil Language of source buffer +--- +---@return TSTreeView|nil +---@return string|nil Error message, if any +--- +---@package +function TSTreeView:new(bufnr, lang) + local ok, parser = pcall(vim.treesitter.get_parser, bufnr or 0, lang) + if not ok then + return nil, 'No parser available for the given buffer' + end + + -- For each child tree (injected language), find the root of the tree and locate the node within + -- the primary tree that contains that root. Add a mapping from the node in the primary tree to + -- the root in the child tree to the {injections} table. + local root = parser:parse()[1]:root() + local injections = {} ---@type table + parser:for_each_child(function(child, lang_) + child:for_each_tree(function(tree) + local r = tree:root() + local node = root:named_descendant_for_range(r:range()) + if node then + injections[node:id()] = { + lang = lang_, + root = r, + } + end + end) + end) + + local nodes = traverse(root, 0, parser:lang(), injections, {}) + + local named = {} ---@type TSP.Node[] + for _, v in ipairs(nodes) do + if v.named then + named[#named + 1] = v + end + end + + local t = { + ns = api.nvim_create_namespace(''), + nodes = nodes, + named = named, + opts = { + anon = false, + lang = false, + }, + } + + setmetatable(t, self) + self.__index = self + return t +end + +local decor_ns = api.nvim_create_namespace('ts.dev') + +---@private +---@param lnum integer +---@param col integer +---@param end_lnum integer +---@param end_col integer +---@return string +local function get_range_str(lnum, col, end_lnum, end_col) + if lnum == end_lnum then + return string.format('[%d:%d - %d]', lnum + 1, col + 1, end_col) + end + return string.format('[%d:%d - %d:%d]', lnum + 1, col + 1, end_lnum + 1, end_col) +end + +--- Write the contents of this View into {bufnr}. +--- +---@param bufnr integer Buffer number to write into. +---@package +function TSTreeView:draw(bufnr) + vim.bo[bufnr].modifiable = true + local lines = {} ---@type string[] + local lang_hl_marks = {} ---@type table[] + + for _, item in self:iter() do + local range_str = get_range_str(item.lnum, item.col, item.end_lnum, item.end_col) + local lang_str = self.opts.lang and string.format(' %s', item.lang) or '' + local line = + string.format('%s%s ; %s%s', string.rep(' ', item.depth), item.text, range_str, lang_str) + + if self.opts.lang then + lang_hl_marks[#lang_hl_marks + 1] = { + col = #line - #lang_str, + end_col = #line, + } + end + + lines[#lines + 1] = line + end + + api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) + + api.nvim_buf_clear_namespace(bufnr, decor_ns, 0, -1) + + for i, m in ipairs(lang_hl_marks) do + api.nvim_buf_set_extmark(bufnr, decor_ns, i - 1, m.col, { + hl_group = 'Title', + end_col = m.end_col, + }) + end + + vim.bo[bufnr].modifiable = false +end + +--- Get node {i} from this View. +--- +--- The node number is dependent on whether or not anonymous nodes are displayed. +--- +---@param i integer Node number to get +---@return TSP.Node +---@package +function TSTreeView:get(i) + local t = self.opts.anon and self.nodes or self.named + return t[i] +end + +--- Iterate over all of the nodes in this View. +--- +---@return (fun(): integer, TSP.Node) Iterator over all nodes in this View +---@return table +---@return integer +---@package +function TSTreeView:iter() + return ipairs(self.opts.anon and self.nodes or self.named) +end + +--- @class InspectTreeOpts +--- @field lang string? The language of the source buffer. If omitted, the +--- filetype of the source buffer is used. +--- @field bufnr integer? Buffer to draw the tree into. If omitted, a new +--- buffer is created. +--- @field winid integer? Window id to display the tree buffer in. If omitted, +--- a new window is created with {command}. +--- @field command string? Vimscript command to create the window. Default +--- value is "60vnew". Only used when {winid} is nil. +--- @field title (string|fun(bufnr:integer):string|nil) Title of the window. If a +--- function, it accepts the buffer number of the source +--- buffer as its only argument and should return a string. + +--- @private +--- +--- @param opts InspectTreeOpts +function M.inspect_tree(opts) + vim.validate({ + opts = { opts, 't', true }, + }) + + opts = opts or {} + + local buf = api.nvim_get_current_buf() + local win = api.nvim_get_current_win() + local pg = assert(TSTreeView:new(buf, opts.lang)) + + -- Close any existing dev window + if vim.b[buf].dev then + local w = vim.b[buf].dev + if api.nvim_win_is_valid(w) then + api.nvim_win_close(w, true) + end + end + + local w = opts.winid + if not w then + vim.cmd(opts.command or '60vnew') + w = api.nvim_get_current_win() + end + + local b = opts.bufnr + if b then + api.nvim_win_set_buf(w, b) + else + b = api.nvim_win_get_buf(w) + end + + vim.b[buf].dev = w + + vim.wo[w].scrolloff = 5 + vim.wo[w].wrap = false + vim.wo[w].foldmethod = 'manual' -- disable folding + vim.bo[b].buflisted = false + vim.bo[b].buftype = 'nofile' + vim.bo[b].bufhidden = 'wipe' + vim.b[b].disable_query_linter = true + vim.bo[b].filetype = 'query' + + local title --- @type string? + local opts_title = opts.title + if not opts_title then + local bufname = api.nvim_buf_get_name(buf) + title = string.format('Syntax tree for %s', vim.fn.fnamemodify(bufname, ':.')) + elseif type(opts_title) == 'function' then + title = opts_title(buf) + end + + assert(type(title) == 'string', 'Window title must be a string') + api.nvim_buf_set_name(b, title) + + pg:draw(b) + + api.nvim_buf_clear_namespace(buf, pg.ns, 0, -1) + api.nvim_buf_set_keymap(b, 'n', '', '', { + desc = 'Jump to the node under the cursor in the source buffer', + callback = function() + local row = api.nvim_win_get_cursor(w)[1] + local pos = pg:get(row) + api.nvim_set_current_win(win) + api.nvim_win_set_cursor(win, { pos.lnum + 1, pos.col }) + end, + }) + api.nvim_buf_set_keymap(b, 'n', 'a', '', { + desc = 'Toggle anonymous nodes', + callback = function() + local row, col = unpack(api.nvim_win_get_cursor(w)) + local curnode = pg:get(row) + while curnode and not curnode.named do + row = row - 1 + curnode = pg:get(row) + end + + pg.opts.anon = not pg.opts.anon + pg:draw(b) + + if not curnode then + return + end + + local id = curnode.id + for i, node in pg:iter() do + if node.id == id then + api.nvim_win_set_cursor(w, { i, col }) + break + end + end + end, + }) + api.nvim_buf_set_keymap(b, 'n', 'I', '', { + desc = 'Toggle language display', + callback = function() + pg.opts.lang = not pg.opts.lang + pg:draw(b) + end, + }) + + local group = api.nvim_create_augroup('treesitter/dev', {}) + + api.nvim_create_autocmd('CursorMoved', { + group = group, + buffer = b, + callback = function() + api.nvim_buf_clear_namespace(buf, pg.ns, 0, -1) + local row = api.nvim_win_get_cursor(w)[1] + local pos = pg:get(row) + api.nvim_buf_set_extmark(buf, pg.ns, pos.lnum, pos.col, { + end_row = pos.end_lnum, + end_col = math.max(0, pos.end_col), + hl_group = 'Visual', + }) + + local topline, botline = vim.fn.line('w0', win), vim.fn.line('w$', win) + + -- Move the cursor if highlighted range is completely out of view + if pos.lnum < topline and pos.end_lnum < topline then + api.nvim_win_set_cursor(win, { pos.end_lnum + 1, 0 }) + elseif pos.lnum > botline and pos.end_lnum > botline then + api.nvim_win_set_cursor(win, { pos.lnum + 1, 0 }) + end + end, + }) + + api.nvim_create_autocmd('CursorMoved', { + group = group, + buffer = buf, + callback = function() + if not api.nvim_buf_is_loaded(b) then + return true + end + + api.nvim_buf_clear_namespace(b, pg.ns, 0, -1) + + local cursor_node = vim.treesitter.get_node({ + bufnr = buf, + lang = opts.lang, + ignore_injections = false, + }) + if not cursor_node then + return + end + + local cursor_node_id = cursor_node:id() + for i, v in pg:iter() do + if v.id == cursor_node_id then + local start = v.depth + local end_col = start + #v.text + api.nvim_buf_set_extmark(b, pg.ns, i - 1, start, { + end_col = end_col, + hl_group = 'Visual', + }) + api.nvim_win_set_cursor(w, { i, 0 }) + break + end + end + end, + }) + + api.nvim_create_autocmd({ 'TextChanged', 'InsertLeave' }, { + group = group, + buffer = buf, + callback = function() + if not api.nvim_buf_is_loaded(b) then + return true + end + + pg = assert(TSTreeView:new(buf, opts.lang)) + pg:draw(b) + end, + }) + + api.nvim_create_autocmd('BufLeave', { + group = group, + buffer = b, + callback = function() + api.nvim_buf_clear_namespace(buf, pg.ns, 0, -1) + end, + }) + + api.nvim_create_autocmd('BufLeave', { + group = group, + buffer = buf, + callback = function() + if not api.nvim_buf_is_loaded(b) then + return true + end + + api.nvim_buf_clear_namespace(b, pg.ns, 0, -1) + end, + }) + + api.nvim_create_autocmd('BufHidden', { + group = group, + buffer = buf, + once = true, + callback = function() + if api.nvim_win_is_valid(w) then + api.nvim_win_close(w, true) + end + end, + }) +end + +return M diff --git a/runtime/lua/vim/treesitter/playground.lua b/runtime/lua/vim/treesitter/playground.lua deleted file mode 100644 index 8293c1bd0a..0000000000 --- a/runtime/lua/vim/treesitter/playground.lua +++ /dev/null @@ -1,439 +0,0 @@ -local api = vim.api - ----@class TSPlaygroundModule -local M = {} - ----@class TSPlayground ----@field ns integer API namespace ----@field opts table Options table with the following keys: ---- - anon (boolean): If true, display anonymous nodes ---- - lang (boolean): If true, display the language alongside each node ----@field nodes TSP.Node[] ----@field named TSP.Node[] -local TSPlayground = {} - ----@class TSP.Node ----@field id integer Node id ----@field text string Node text ----@field named boolean True if this is a named (non-anonymous) node ----@field depth integer Depth of the node within the tree ----@field lnum integer Beginning line number of this node in the source buffer ----@field col integer Beginning column number of this node in the source buffer ----@field end_lnum integer Final line number of this node in the source buffer ----@field end_col integer Final column number of this node in the source buffer ----@field lang string Source language of this node ----@field root TSNode - ---- Traverse all child nodes starting at {node}. ---- ---- This is a recursive function. The {depth} parameter indicates the current recursion level. ---- {lang} is a string indicating the language of the tree currently being traversed. Each traversed ---- node is added to {tree}. When recursion completes, {tree} is an array of all nodes in the order ---- they were visited. ---- ---- {injections} is a table mapping node ids from the primary tree to language tree injections. Each ---- injected language has a series of trees nested within the primary language's tree, and the root ---- node of each of these trees is contained within a node in the primary tree. The {injections} ---- table maps nodes in the primary tree to root nodes of injected trees. ---- ----@param node TSNode Starting node to begin traversal |tsnode| ----@param depth integer Current recursion depth ----@param lang string Language of the tree currently being traversed ----@param injections table Mapping of node ids to root nodes of injected language trees (see ---- explanation above) ----@param tree TSP.Node[] Output table containing a list of tables each representing a node in the tree ----@private -local function traverse(node, depth, lang, injections, tree) - local injection = injections[node:id()] - if injection then - traverse(injection.root, depth, injection.lang, injections, tree) - end - - for child, field in node:iter_children() do - local type = child:type() - local lnum, col, end_lnum, end_col = child:range() - local named = child:named() - local text ---@type string - if named then - if field then - text = string.format('%s: (%s)', field, type) - else - text = string.format('(%s)', type) - end - else - text = string.format('"%s"', type:gsub('\n', '\\n')) - end - - table.insert(tree, { - id = child:id(), - text = text, - named = named, - depth = depth, - lnum = lnum, - col = col, - end_lnum = end_lnum, - end_col = end_col, - lang = lang, - }) - - traverse(child, depth + 1, lang, injections, tree) - end - - return tree -end - ---- Create a new Playground object. ---- ----@param bufnr integer Source buffer number ----@param lang string|nil Language of source buffer ---- ----@return TSPlayground|nil ----@return string|nil Error message, if any ---- ----@package -function TSPlayground:new(bufnr, lang) - local ok, parser = pcall(vim.treesitter.get_parser, bufnr or 0, lang) - if not ok then - return nil, 'No parser available for the given buffer' - end - - -- For each child tree (injected language), find the root of the tree and locate the node within - -- the primary tree that contains that root. Add a mapping from the node in the primary tree to - -- the root in the child tree to the {injections} table. - local root = parser:parse()[1]:root() - local injections = {} ---@type table - parser:for_each_child(function(child, lang_) - child:for_each_tree(function(tree) - local r = tree:root() - local node = root:named_descendant_for_range(r:range()) - if node then - injections[node:id()] = { - lang = lang_, - root = r, - } - end - end) - end) - - local nodes = traverse(root, 0, parser:lang(), injections, {}) - - local named = {} ---@type TSP.Node[] - for _, v in ipairs(nodes) do - if v.named then - named[#named + 1] = v - end - end - - local t = { - ns = api.nvim_create_namespace(''), - nodes = nodes, - named = named, - opts = { - anon = false, - lang = false, - }, - } - - setmetatable(t, self) - self.__index = self - return t -end - -local decor_ns = api.nvim_create_namespace('ts.playground') - ----@private ----@param lnum integer ----@param col integer ----@param end_lnum integer ----@param end_col integer ----@return string -local function get_range_str(lnum, col, end_lnum, end_col) - if lnum == end_lnum then - return string.format('[%d:%d - %d]', lnum + 1, col + 1, end_col) - end - return string.format('[%d:%d - %d:%d]', lnum + 1, col + 1, end_lnum + 1, end_col) -end - ---- Write the contents of this Playground into {bufnr}. ---- ----@param bufnr integer Buffer number to write into. ----@package -function TSPlayground:draw(bufnr) - vim.bo[bufnr].modifiable = true - local lines = {} ---@type string[] - local lang_hl_marks = {} ---@type table[] - - for _, item in self:iter() do - local range_str = get_range_str(item.lnum, item.col, item.end_lnum, item.end_col) - local lang_str = self.opts.lang and string.format(' %s', item.lang) or '' - local line = - string.format('%s%s ; %s%s', string.rep(' ', item.depth), item.text, range_str, lang_str) - - if self.opts.lang then - lang_hl_marks[#lang_hl_marks + 1] = { - col = #line - #lang_str, - end_col = #line, - } - end - - lines[#lines + 1] = line - end - - api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) - - api.nvim_buf_clear_namespace(bufnr, decor_ns, 0, -1) - - for i, m in ipairs(lang_hl_marks) do - api.nvim_buf_set_extmark(bufnr, decor_ns, i - 1, m.col, { - hl_group = 'Title', - end_col = m.end_col, - }) - end - - vim.bo[bufnr].modifiable = false -end - ---- Get node {i} from this Playground object. ---- ---- The node number is dependent on whether or not anonymous nodes are displayed. ---- ----@param i integer Node number to get ----@return TSP.Node ----@package -function TSPlayground:get(i) - local t = self.opts.anon and self.nodes or self.named - return t[i] -end - ---- Iterate over all of the nodes in this Playground object. ---- ----@return (fun(): integer, TSP.Node) Iterator over all nodes in this Playground ----@return table ----@return integer ----@package -function TSPlayground:iter() - return ipairs(self.opts.anon and self.nodes or self.named) -end - ---- @class InspectTreeOpts ---- @field lang string? The language of the source buffer. If omitted, the ---- filetype of the source buffer is used. ---- @field bufnr integer? Buffer to draw the tree into. If omitted, a new ---- buffer is created. ---- @field winid integer? Window id to display the tree buffer in. If omitted, ---- a new window is created with {command}. ---- @field command string? Vimscript command to create the window. Default ---- value is "60vnew". Only used when {winid} is nil. ---- @field title (string|fun(bufnr:integer):string|nil) Title of the window. If a ---- function, it accepts the buffer number of the source ---- buffer as its only argument and should return a string. - ---- @param opts InspectTreeOpts -function M.inspect_tree(opts) - vim.validate({ - opts = { opts, 't', true }, - }) - - opts = opts or {} - - local buf = api.nvim_get_current_buf() - local win = api.nvim_get_current_win() - local pg = assert(TSPlayground:new(buf, opts.lang)) - - -- Close any existing playground window - if vim.b[buf].playground then - local w = vim.b[buf].playground - if api.nvim_win_is_valid(w) then - api.nvim_win_close(w, true) - end - end - - local w = opts.winid - if not w then - vim.cmd(opts.command or '60vnew') - w = api.nvim_get_current_win() - end - - local b = opts.bufnr - if b then - api.nvim_win_set_buf(w, b) - else - b = api.nvim_win_get_buf(w) - end - - vim.b[buf].playground = w - - vim.wo[w].scrolloff = 5 - vim.wo[w].wrap = false - vim.wo[w].foldmethod = 'manual' -- disable folding - vim.bo[b].buflisted = false - vim.bo[b].buftype = 'nofile' - vim.bo[b].bufhidden = 'wipe' - vim.b[b].disable_query_linter = true - vim.bo[b].filetype = 'query' - - local title --- @type string? - local opts_title = opts.title - if not opts_title then - local bufname = api.nvim_buf_get_name(buf) - title = string.format('Syntax tree for %s', vim.fn.fnamemodify(bufname, ':.')) - elseif type(opts_title) == 'function' then - title = opts_title(buf) - end - - assert(type(title) == 'string', 'Window title must be a string') - api.nvim_buf_set_name(b, title) - - pg:draw(b) - - api.nvim_buf_clear_namespace(buf, pg.ns, 0, -1) - api.nvim_buf_set_keymap(b, 'n', '', '', { - desc = 'Jump to the node under the cursor in the source buffer', - callback = function() - local row = api.nvim_win_get_cursor(w)[1] - local pos = pg:get(row) - api.nvim_set_current_win(win) - api.nvim_win_set_cursor(win, { pos.lnum + 1, pos.col }) - end, - }) - api.nvim_buf_set_keymap(b, 'n', 'a', '', { - desc = 'Toggle anonymous nodes', - callback = function() - local row, col = unpack(api.nvim_win_get_cursor(w)) - local curnode = pg:get(row) - while curnode and not curnode.named do - row = row - 1 - curnode = pg:get(row) - end - - pg.opts.anon = not pg.opts.anon - pg:draw(b) - - if not curnode then - return - end - - local id = curnode.id - for i, node in pg:iter() do - if node.id == id then - api.nvim_win_set_cursor(w, { i, col }) - break - end - end - end, - }) - api.nvim_buf_set_keymap(b, 'n', 'I', '', { - desc = 'Toggle language display', - callback = function() - pg.opts.lang = not pg.opts.lang - pg:draw(b) - end, - }) - - local group = api.nvim_create_augroup('treesitter/playground', {}) - - api.nvim_create_autocmd('CursorMoved', { - group = group, - buffer = b, - callback = function() - api.nvim_buf_clear_namespace(buf, pg.ns, 0, -1) - local row = api.nvim_win_get_cursor(w)[1] - local pos = pg:get(row) - api.nvim_buf_set_extmark(buf, pg.ns, pos.lnum, pos.col, { - end_row = pos.end_lnum, - end_col = math.max(0, pos.end_col), - hl_group = 'Visual', - }) - - local topline, botline = vim.fn.line('w0', win), vim.fn.line('w$', win) - - -- Move the cursor if highlighted range is completely out of view - if pos.lnum < topline and pos.end_lnum < topline then - api.nvim_win_set_cursor(win, { pos.end_lnum + 1, 0 }) - elseif pos.lnum > botline and pos.end_lnum > botline then - api.nvim_win_set_cursor(win, { pos.lnum + 1, 0 }) - end - end, - }) - - api.nvim_create_autocmd('CursorMoved', { - group = group, - buffer = buf, - callback = function() - if not api.nvim_buf_is_loaded(b) then - return true - end - - api.nvim_buf_clear_namespace(b, pg.ns, 0, -1) - - local cursor_node = vim.treesitter.get_node({ - bufnr = buf, - lang = opts.lang, - ignore_injections = false, - }) - if not cursor_node then - return - end - - local cursor_node_id = cursor_node:id() - for i, v in pg:iter() do - if v.id == cursor_node_id then - local start = v.depth - local end_col = start + #v.text - api.nvim_buf_set_extmark(b, pg.ns, i - 1, start, { - end_col = end_col, - hl_group = 'Visual', - }) - api.nvim_win_set_cursor(w, { i, 0 }) - break - end - end - end, - }) - - api.nvim_create_autocmd({ 'TextChanged', 'InsertLeave' }, { - group = group, - buffer = buf, - callback = function() - if not api.nvim_buf_is_loaded(b) then - return true - end - - pg = assert(TSPlayground:new(buf, opts.lang)) - pg:draw(b) - end, - }) - - api.nvim_create_autocmd('BufLeave', { - group = group, - buffer = b, - callback = function() - api.nvim_buf_clear_namespace(buf, pg.ns, 0, -1) - end, - }) - - api.nvim_create_autocmd('BufLeave', { - group = group, - buffer = buf, - callback = function() - if not api.nvim_buf_is_loaded(b) then - return true - end - - api.nvim_buf_clear_namespace(b, pg.ns, 0, -1) - end, - }) - - api.nvim_create_autocmd('BufHidden', { - group = group, - buffer = buf, - once = true, - callback = function() - if api.nvim_win_is_valid(w) then - api.nvim_win_close(w, true) - end - end, - }) -end - -return M -- cgit From ceb37ff1d83c6dc341dc43d3dc8c81f657bc9484 Mon Sep 17 00:00:00 2001 From: Stephan Seitz Date: Sun, 11 Jun 2023 23:46:22 +0200 Subject: refactor(treesitter): use npcall in _query_linter.lua #23985 --- runtime/lua/vim/treesitter/_query_linter.lua | 12 +++--------- 1 file changed, 3 insertions(+), 9 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 ecdee5fc95..3dd0177a81 100644 --- a/runtime/lua/vim/treesitter/_query_linter.lua +++ b/runtime/lua/vim/treesitter/_query_linter.lua @@ -53,10 +53,7 @@ end local function guess_query_lang(buf) local filename = api.nvim_buf_get_name(buf) if filename ~= '' then - local ok, query_lang = pcall(vim.fn.fnamemodify, filename, ':p:h:t') - if ok then - return query_lang - end + return vim.F.npcall(vim.fn.fnamemodify, filename, ':p:h:t') end end @@ -270,11 +267,8 @@ function M.lint(buf, opts) for i = 1, math.max(1, #opts.langs) do local lang = opts.langs[i] - --- @type boolean, (table|nil) - local ok, parser_info = pcall(vim.treesitter.language.inspect, lang) - if not ok then - parser_info = nil - end + --- @type (table|nil) + local parser_info = vim.F.npcall(vim.treesitter.language.inspect, lang) local parser = vim.treesitter.get_parser(buf) parser:parse() -- cgit From 8c9dab3e0d788d44c8a2fee83a6193f5955c814e Mon Sep 17 00:00:00 2001 From: Santos Gallegos Date: Sun, 18 Jun 2023 09:42:17 -0500 Subject: fix(treesitter): use vim.highlight.priorities instead of hardcoded 100 (#24052) Problem: Treesitter highlighting base priority cannot be customized. Solution: Use `vim.highlight.priorities.treesitter` instead of hard-coded value. --- runtime/lua/vim/treesitter/highlighter.lua | 4 +++- 1 file changed, 3 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 4bb764c5c6..d4db6bc404 100644 --- a/runtime/lua/vim/treesitter/highlighter.lua +++ b/runtime/lua/vim/treesitter/highlighter.lua @@ -260,12 +260,14 @@ local function on_line_impl(self, buf, line, is_spell_nav) local spell_pri_offset = capture_name == 'nospell' and 1 or 0 if hl and end_row >= line and (not is_spell_nav or spell ~= nil) then + local priority = (tonumber(metadata.priority) or vim.highlight.priorities.treesitter) + + spell_pri_offset api.nvim_buf_set_extmark(buf, ns, start_row, start_col, { end_line = end_row, end_col = end_col, hl_group = hl, ephemeral = true, - priority = (tonumber(metadata.priority) or 100) + spell_pri_offset, -- Low but leaves room below + priority = priority, conceal = metadata.conceal, spell = spell, }) -- cgit From c7e7f1d4b4b62c75bb54e652f25c6c6b8785a7f4 Mon Sep 17 00:00:00 2001 From: Jaehwang Jung Date: Wed, 28 Jun 2023 03:05:09 +0900 Subject: fix(treesitter): make foldexpr work without highlighting (#24167) Problem: Treesitter fold is not updated if treesitter hightlight is not active. More precisely, updating folds requires `LanguageTree:parse()`. Solution: Call `parse()` before computing folds and compute folds when lines are added/removed. This doesn't guarantee correctness of the folds, because some changes that don't add/remove line won't update the folds even if they should (e.g. adding pair of braces). But it is good enough for most cases, while not introducing big overhead. Also, if highlighting is active, it is likely that `TSHighlighter._on_buf` already ran `parse()` (or vice versa). --- runtime/lua/vim/treesitter/_fold.lua | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/_fold.lua b/runtime/lua/vim/treesitter/_fold.lua index a8f8c7967e..d308657237 100644 --- a/runtime/lua/vim/treesitter/_fold.lua +++ b/runtime/lua/vim/treesitter/_fold.lua @@ -162,9 +162,7 @@ local function get_folds_levels(bufnr, info, srow, erow) local parser = ts.get_parser(bufnr) - if not parser:is_valid() then - return - end + parser:parse() parser:for_each_tree(function(tree, ltree) local query = ts.query.get(ltree:lang(), 'folds') @@ -283,10 +281,12 @@ local function on_bytes(bufnr, foldinfo, start_row, old_row, new_row) local end_row_old = start_row + old_row local end_row_new = start_row + new_row - if new_row < old_row then - foldinfo:remove_range(end_row_new, end_row_old) - elseif new_row > old_row then - foldinfo:add_range(start_row, end_row_new) + if new_row ~= old_row then + if new_row < old_row then + foldinfo:remove_range(end_row_new, end_row_old) + else + foldinfo:add_range(start_row, end_row_new) + end schedule_if_loaded(bufnr, function() get_folds_levels(bufnr, foldinfo, start_row, end_row_new) recompute_folds() -- cgit From e85e7fc7bcccfa3b8f9e52ec4d6cb21591b50468 Mon Sep 17 00:00:00 2001 From: Jaehwang Jung Date: Wed, 28 Jun 2023 03:05:44 +0900 Subject: fix(treesitter): handle empty region when logging (#24173) --- runtime/lua/vim/treesitter/languagetree.lua | 3 +++ 1 file changed, 3 insertions(+) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index cabfa8ccc0..bf6333aaa4 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -447,6 +447,9 @@ end ---@private ---@param region Range6[] local function region_tostr(region) + if #region == 0 then + return '[]' + end local srow, scol = region[1][1], region[1][2] local erow, ecol = region[#region][4], region[#region][5] return string.format('[%d:%d-%d:%d]', srow, scol, erow, ecol) -- cgit From 11844dde81c41bded54f2383b57f8eef406f2736 Mon Sep 17 00:00:00 2001 From: Christian Clason Date: Sat, 1 Jul 2023 11:08:06 +0200 Subject: feat(treesitter): bundle markdown parser and queries (#22481) * bundle split Markdown parser from https://github.com/MDeiml/tree-sitter-markdown * add queries from https://github.com/nvim-treesitter/nvim-treesitter/tree/main * upstream `#trim!` and `#inject-language!` directives Co-authored-by: dundargoc --- runtime/lua/vim/treesitter/query.lua | 60 +++++++++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 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 73b561c777..7f90fa10e8 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -475,7 +475,6 @@ local directive_handlers = { metadata[capture_id].range = range end end, - -- Transform the content of the node -- Example: (#gsub! @_node ".*%.(.*)" "%1") ['gsub!'] = function(match, _, bufnr, pred, metadata) @@ -497,6 +496,65 @@ 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!'] = function(match, _, bufnr, pred, metadata) + local node = match[pred[2]] + if not node then + return + end + + 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 true do + -- As we only care when end_col == 0, always inspect one line above end_row. + local end_line = vim.api.nvim_buf_get_lines(bufnr, end_row - 1, end_row, true)[1] + + if end_line ~= '' then + break + end + + end_row = end_row - 1 + end + + -- If this produces an invalid range, we just skip it. + if start_row < end_row or (start_row == end_row and start_col <= end_col) then + metadata.range = { start_row, start_col, end_row, end_col } + end + end, + -- Set injection language from node text, interpreted first as language and then as filetype + -- Example: (#inject-language! @_lang) + ['inject-language!'] = function(match, _, bufnr, pred, metadata) + local id = pred[2] + local node = match[id] + if not node then + return + end + + -- TODO(clason): replace by refactored `ts.has_parser` API + local has_parser = function(lang) + return vim._ts_has_language(lang) + or #vim.api.nvim_get_runtime_file('parser/' .. lang .. '.*', false) > 0 + end + + local alias = vim.treesitter.get_node_text(node, bufnr, { metadata = metadata[id] }) + if not alias then + return + elseif has_parser(alias) then + metadata['injection.language'] = alias + else + local lang = vim.treesitter.language.get_lang(alias) + if lang and has_parser(lang) then + metadata['injection.language'] = lang + end + end + end, } --- Adds a new predicate to be used in queries -- cgit From 4fd852b8cb88ed035203d3f9ae2e6a8258244974 Mon Sep 17 00:00:00 2001 From: Jaehwang Jung Date: Mon, 3 Jul 2023 00:44:21 +0900 Subject: perf(treesitter): cache fold query (#24222) perf(treesitter): cache vim.treesitter.query.get Problem: vim.treesitter.query.get searches and reads query files every time it's called, if user hasn't overridden the query. So this can incur slowdown when called frequently. This can happen when using treesitter foldexpr. For example, when using `:h :range!` in markdown file to format fenced codeblock, on_changedtree in _fold.lua is triggered many times despite that the tree doesn't have syntactic changes (might be a bug in LanguageTree). (Incidentally, the resulting fold is incorrect due to a bug in `:h range!`.) on_changedtree calls vim.treesitter.query.get for each tree changes. In addition, it may request folds queries for injected languages without fold queries, such as markdown_inline. Solution: * Cache the result of vim.treesitter.query.get. * If query file was not found, fail quickly at later calls. --- runtime/lua/vim/treesitter/query.lua | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index 7f90fa10e8..7610ef7b7f 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -195,6 +195,12 @@ function M.set(lang, query_name, text) explicit_queries[lang][query_name] = M.parse(lang, text) end +--- `false` if query files didn't exist or were empty +---@type table> +local query_get_cache = vim.defaulttable(function() + return setmetatable({}, { __mode = 'v' }) +end) + ---@deprecated function M.get_query(...) vim.deprecate('vim.treesitter.query.get_query()', 'vim.treesitter.query.get()', '0.10') @@ -212,16 +218,28 @@ function M.get(lang, query_name) return explicit_queries[lang][query_name] end + local cached = query_get_cache[lang][query_name] + if cached then + return cached + elseif cached == false then + return nil + end + local query_files = M.get_files(lang, query_name) local query_string = read_query_files(query_files) - if #query_string > 0 then - return M.parse(lang, query_string) + if #query_string == 0 then + query_get_cache[lang][query_name] = false + return nil end + + local query = M.parse(lang, query_string) + query_get_cache[lang][query_name] = query + return query end ----@type {[string]: {[string]: Query}} -local query_cache = vim.defaulttable(function() +---@type table> +local query_parse_cache = vim.defaulttable(function() return setmetatable({}, { __mode = 'v' }) end) @@ -250,7 +268,7 @@ end ---@return Query Parsed query function M.parse(lang, query) language.add(lang) - local cached = query_cache[lang][query] + local cached = query_parse_cache[lang][query] if cached then return cached end @@ -259,7 +277,7 @@ function M.parse(lang, query) self.query = vim._ts_parse_query(lang, query) self.info = self.query:inspect() self.captures = self.info.captures - query_cache[lang][query] = self + query_parse_cache[lang][query] = self return self end -- cgit From c44d819ae1f29cd34ee3b2350b5c702caed949c3 Mon Sep 17 00:00:00 2001 From: Jaehwang Jung Date: Fri, 7 Jul 2023 19:12:46 +0900 Subject: fix(treesitter): update folds in all relevant windows (#24230) Problem: When using treesitter foldexpr, * :diffput/get open diff folds, and * folds are not updated in other windows that contain the updated buffer. Solution: Update folds in all windows that contain the updated buffer and use expr foldmethod. --- runtime/lua/vim/treesitter/_fold.lua | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/_fold.lua b/runtime/lua/vim/treesitter/_fold.lua index d308657237..a02d0a584d 100644 --- a/runtime/lua/vim/treesitter/_fold.lua +++ b/runtime/lua/vim/treesitter/_fold.lua @@ -232,20 +232,40 @@ local M = {} ---@type table local foldinfos = {} -local function recompute_folds() +--- 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). +--- +--- Nvim usually automatically updates folds when text changes, but it doesn't work here because +--- FoldInfo update is scheduled. So we do it manually. +local function foldupdate(bufnr) + local function do_update() + for _, win in ipairs(vim.fn.win_findbuf(bufnr)) do + api.nvim_win_call(win, function() + if vim.wo.foldmethod == 'expr' then + vim._foldupdate() + end + end) + end + end + if api.nvim_get_mode().mode == 'i' then -- foldUpdate() is guarded in insert mode. So update folds on InsertLeave api.nvim_create_autocmd('InsertLeave', { once = true, - callback = vim._foldupdate, + callback = do_update, }) return end - vim._foldupdate() + do_update() end ---- Schedule a function only if bufnr is loaded +--- Schedule a function only if bufnr is loaded. +--- We schedule fold level computation for the following reasons: +--- * queries seem to use the old buffer state in on_bytes for some unknown reason; +--- * to avoid textlock; +--- * to avoid infinite recursion: +--- get_folds_levels → parse → _do_callback → on_changedtree → get_folds_levels. ---@param bufnr integer ---@param fn function local function schedule_if_loaded(bufnr, fn) @@ -261,14 +281,12 @@ end ---@param foldinfo TS.FoldInfo ---@param tree_changes Range4[] local function on_changedtree(bufnr, foldinfo, tree_changes) - -- For some reason, queries seem to use the old buffer state in on_bytes. - -- Get around this by scheduling and manually updating folds. schedule_if_loaded(bufnr, function() for _, change in ipairs(tree_changes) do local srow, _, erow = Range.unpack4(change) get_folds_levels(bufnr, foldinfo, srow, erow) end - recompute_folds() + foldupdate(bufnr) end) end @@ -289,7 +307,7 @@ local function on_bytes(bufnr, foldinfo, start_row, old_row, new_row) end schedule_if_loaded(bufnr, function() get_folds_levels(bufnr, foldinfo, start_row, end_row_new) - recompute_folds() + foldupdate(bufnr) end) end end -- cgit From be74807eef13ff8c90d55cf8b22b01d6d33b1641 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Tue, 18 Jul 2023 15:42:30 +0100 Subject: docs(lua): more improvements (#24387) * docs(lua): teach lua2dox how to table * docs(lua): teach gen_vimdoc.py about local functions No more need to mark local functions with @private * docs(lua): mention @nodoc and @meta in dev-lua-doc * fixup! Co-authored-by: Justin M. Keyes --------- Co-authored-by: Justin M. Keyes --- runtime/lua/vim/treesitter/dev.lua | 2 -- runtime/lua/vim/treesitter/highlighter.lua | 2 +- runtime/lua/vim/treesitter/language.lua | 1 - runtime/lua/vim/treesitter/languagetree.lua | 5 ----- runtime/lua/vim/treesitter/query.lua | 9 --------- 5 files changed, 1 insertion(+), 18 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/dev.lua b/runtime/lua/vim/treesitter/dev.lua index 99cd147658..1bb5a08205 100644 --- a/runtime/lua/vim/treesitter/dev.lua +++ b/runtime/lua/vim/treesitter/dev.lua @@ -42,7 +42,6 @@ local TSTreeView = {} ---@param injections table Mapping of node ids to root nodes of injected language trees (see --- explanation above) ---@param tree TSP.Node[] Output table containing a list of tables each representing a node in the tree ----@private local function traverse(node, depth, lang, injections, tree) local injection = injections[node:id()] if injection then @@ -141,7 +140,6 @@ end local decor_ns = api.nvim_create_namespace('ts.dev') ----@private ---@param lnum integer ---@param col integer ---@param end_lnum integer diff --git a/runtime/lua/vim/treesitter/highlighter.lua b/runtime/lua/vim/treesitter/highlighter.lua index d4db6bc404..f8ec5b175d 100644 --- a/runtime/lua/vim/treesitter/highlighter.lua +++ b/runtime/lua/vim/treesitter/highlighter.lua @@ -17,6 +17,7 @@ local query = vim.treesitter.query local TSHighlighter = rawget(vim.treesitter, 'TSHighlighter') or {} TSHighlighter.__index = TSHighlighter +--- @nodoc TSHighlighter.active = TSHighlighter.active or {} ---@class TSHighlighterQuery @@ -205,7 +206,6 @@ function TSHighlighter:get_query(lang) return self._queries[lang] end ----@private ---@param self TSHighlighter ---@param buf integer ---@param line integer diff --git a/runtime/lua/vim/treesitter/language.lua b/runtime/lua/vim/treesitter/language.lua index 08c297c9ad..9695e2c41c 100644 --- a/runtime/lua/vim/treesitter/language.lua +++ b/runtime/lua/vim/treesitter/language.lua @@ -104,7 +104,6 @@ function M.add(lang, opts) vim._ts_add_language(path, lang, symbol_name) end ---- @private --- @param x string|string[] --- @return string[] local function ensure_list(x) diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index bf6333aaa4..0d4a1a54dd 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -164,7 +164,6 @@ function LanguageTree:_set_logger() self._parser:_set_logger(log_lex, log_parse, self._logger) end ----@private ---Measure execution time of a function ---@generic R1, R2, R3 ---@param f fun(): R1, R2, R2 @@ -444,7 +443,6 @@ function LanguageTree:destroy() end end ----@private ---@param region Range6[] local function region_tostr(region) if #region == 0 then @@ -560,7 +558,6 @@ function LanguageTree:included_regions() return regions end ----@private ---@param node TSNode ---@param source string|integer ---@param metadata TSMetadata @@ -600,7 +597,6 @@ end ---@alias TSInjection table> ----@private ---@param t table ---@param tree_index integer ---@param pattern integer @@ -963,7 +959,6 @@ function LanguageTree:register_cbs(cbs, recursive) end end ----@private ---@param tree TSTree ---@param range Range ---@return boolean diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index 7610ef7b7f..08186468a5 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -15,7 +15,6 @@ Query.__index = Query ---@class TSQueryModule local M = {} ----@private ---@param files string[] ---@return string[] local function dedupe_files(files) @@ -33,7 +32,6 @@ local function dedupe_files(files) return result end ----@private local function safe_read(filename, read_quantifier) local file, err = io.open(filename, 'r') if not file then @@ -44,7 +42,6 @@ local function safe_read(filename, read_quantifier) return content end ----@private --- Adds {ilang} to {base_langs}, only if {ilang} is different than {lang} --- ---@return boolean true If lang == ilang @@ -153,7 +150,6 @@ function M.get_files(lang, query_name, is_included) return query_files end ----@private ---@param filenames string[] ---@return string local function read_query_files(filenames) @@ -335,7 +331,6 @@ local predicate_handlers = { ['match?'] = (function() local magic_prefixes = { ['\\v'] = true, ['\\m'] = true, ['\\M'] = true, ['\\V'] = true } - ---@private local function check_magic(str) if string.len(str) < 2 or magic_prefixes[string.sub(str, 1, 2)] then return str @@ -624,12 +619,10 @@ function M.list_predicates() return vim.tbl_keys(predicate_handlers) end ----@private local function xor(x, y) return (x or y) and not (x and y) end ----@private local function is_directive(name) return string.sub(name, -1) == '!' end @@ -700,7 +693,6 @@ end --- Returns the start and stop value if set else the node's range. -- When the node's range is used, the stop is incremented by 1 -- to make the search inclusive. ----@private ---@param start integer ---@param stop integer ---@param node TSNode @@ -750,7 +742,6 @@ function Query:iter_captures(node, source, start, stop) start, stop = value_or_node_range(start, stop, node) local raw_iter = node:_rawquery(self.query, true, start, stop) - ---@private local function iter() local capture, captured_node, match = raw_iter() local metadata = {} -- cgit From 369f58797dbd3c0e18035d26e6f5d6634be7a2a9 Mon Sep 17 00:00:00 2001 From: ObserverOfTime Date: Mon, 7 Aug 2023 16:16:12 +0300 Subject: fix(treesitter): escape quotes in :InspectTree view #24582 Problem: Anonymous nodes containing double quotes break the highlighting. Solution: Escape double quotes in anonymous nodes. --- runtime/lua/vim/treesitter/dev.lua | 9 ++++++++- 1 file changed, 8 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 1bb5a08205..e94e8f08dc 100644 --- a/runtime/lua/vim/treesitter/dev.lua +++ b/runtime/lua/vim/treesitter/dev.lua @@ -152,6 +152,12 @@ local function get_range_str(lnum, col, end_lnum, end_col) return string.format('[%d:%d - %d:%d]', lnum + 1, col + 1, end_lnum + 1, end_col) end +---@param text string +---@return string +local function escape_quotes(text) + return string.format('"%s"', text:sub(2, #text - 1):gsub('"', '\\"')) +end + --- Write the contents of this View into {bufnr}. --- ---@param bufnr integer Buffer number to write into. @@ -164,8 +170,9 @@ function TSTreeView:draw(bufnr) for _, item in self:iter() do local range_str = get_range_str(item.lnum, item.col, item.end_lnum, item.end_col) local lang_str = self.opts.lang and string.format(' %s', item.lang) or '' + local text = item.named and item.text or escape_quotes(item.text) local line = - string.format('%s%s ; %s%s', string.rep(' ', item.depth), item.text, range_str, lang_str) + string.format('%s%s ; %s%s', string.rep(' ', item.depth), text, range_str, lang_str) if self.opts.lang then lang_hl_marks[#lang_hl_marks + 1] = { -- cgit From 0211f889b9538f7df5fbcb06273d273fb071efff Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Mon, 7 Aug 2023 18:22:36 +0100 Subject: fix(treesitter): make sure injections don't return empty ranges (#24595) When an injection has not set include children, make sure not to add the injection if no ranges are determined. This could happen when there is an injection with a child that has the same range as itself. e.g. consider this Makefile snippet ```make foo: $(VAR) ``` Line 2 has an injection for bash and a make variable reference. If include-children isn't set (default), then there is no range on line 2 to inject since the variable reference needs to be excluded. This caused the language tree to return an empty range, which the parser now interprets to mean the full buffer. This caused makefiles to have completely broken highlighting. --- runtime/lua/vim/treesitter/languagetree.lua | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index 0d4a1a54dd..4b2628609a 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -110,6 +110,10 @@ function LanguageTree.new(source, lang, opts) ---@type LanguageTreeOpts opts = opts or {} + if source == 0 then + source = vim.api.nvim_get_current_buf() + end + local injections = opts.injections or {} local self = setmetatable({ _source = source, @@ -561,11 +565,13 @@ end ---@param node TSNode ---@param source string|integer ---@param metadata TSMetadata +---@param include_children boolean ---@return Range6[] local function get_node_ranges(node, source, metadata, include_children) local range = vim.treesitter.get_range(node, source, metadata) + local child_count = node:named_child_count() - if include_children then + if include_children or child_count == 0 then return { range } end @@ -573,7 +579,8 @@ local function get_node_ranges(node, source, metadata, include_children) local srow, scol, sbyte, erow, ecol, ebyte = Range.unpack6(range) - for i = 0, node:named_child_count() - 1 do + -- We are excluding children so we need to mask out their ranges + for i = 0, child_count - 1 do local child = node:named_child(i) local c_srow, c_scol, c_sbyte, c_erow, c_ecol, c_ebyte = child:range(true) if c_srow > srow or c_scol > scol then @@ -604,7 +611,10 @@ end ---@param combined boolean ---@param ranges Range6[] local function add_injection(t, tree_index, pattern, lang, combined, ranges) - assert(type(lang) == 'string') + if #ranges == 0 then + -- Make sure not to add an empty range set as this is interpreted to mean the whole buffer. + return + end -- Each tree index should be isolated from the other nodes. if not t[tree_index] then -- cgit From c43c745a14dced87a23227d7be4f1c33d4455193 Mon Sep 17 00:00:00 2001 From: Christian Clason Date: Wed, 9 Aug 2023 11:06:13 +0200 Subject: fix(lua): improve annotations for stricter luals diagnostics (#24609) Problem: luals returns stricter diagnostics with bundled luarc.json Solution: Improve some function and type annotations: * use recognized uv.* types * disable diagnostic for global `vim` in shared.lua * docs: don't start comment lines with taglink (otherwise LuaLS will interpret it as a type) * add type alias for lpeg pattern * fix return annotation for `vim.secure.trust` * rename local Range object in vim.version (shadows `Range` in vim.treesitter) * fix some "missing fields" warnings * add missing required fields for test functions in eval.lua * rename lsp meta files for consistency --- 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 08186468a5..3b7e74c0cf 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -434,7 +434,7 @@ local predicate_handlers = { predicate_handlers['vim-match?'] = predicate_handlers['match?'] ---@class TSMetadata ----@field range Range +---@field range? Range ---@field [integer] TSMetadata ---@field [string] integer|string -- cgit From 68f12e7fcb1fb8b95ca0b1207683d929574c0555 Mon Sep 17 00:00:00 2001 From: Kyuuhachi <1547062+Kyuuhachi@users.noreply.github.com> Date: Wed, 9 Aug 2023 19:06:42 +0200 Subject: fix(lua): annotate that TSNode functions can return nil (#24621) --- runtime/lua/vim/treesitter/_meta.lua | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/_meta.lua b/runtime/lua/vim/treesitter/_meta.lua index 9ca4b560c6..36b1a9bbf8 100644 --- a/runtime/lua/vim/treesitter/_meta.lua +++ b/runtime/lua/vim/treesitter/_meta.lua @@ -14,15 +14,15 @@ ---@field extra fun(self: TSNode): boolean ---@field child_count fun(self: TSNode): integer ---@field named_child_count fun(self: TSNode): integer ----@field child fun(self: TSNode, integer): TSNode ----@field named_child fun(self: TSNode, integer): TSNode ----@field descendant_for_range fun(self: TSNode, integer, integer, integer, integer): TSNode ----@field named_descendant_for_range fun(self: TSNode, integer, integer, integer, integer): TSNode ----@field parent fun(self: TSNode): TSNode ----@field next_sibling fun(self: TSNode): TSNode ----@field prev_sibling fun(self: TSNode): TSNode ----@field next_named_sibling fun(self: TSNode): TSNode ----@field prev_named_sibling fun(self: TSNode): TSNode +---@field child fun(self: TSNode, integer): TSNode? +---@field named_child fun(self: TSNode, integer): TSNode? +---@field descendant_for_range fun(self: TSNode, integer, integer, integer, integer): TSNode? +---@field named_descendant_for_range fun(self: TSNode, integer, integer, integer, integer): TSNode? +---@field parent fun(self: TSNode): TSNode? +---@field next_sibling fun(self: TSNode): TSNode? +---@field prev_sibling fun(self: TSNode): TSNode? +---@field next_named_sibling fun(self: TSNode): TSNode? +---@field prev_named_sibling fun(self: TSNode): TSNode? ---@field named_children fun(self: TSNode): TSNode[] ---@field has_changes fun(self: TSNode): boolean ---@field equal fun(self: TSNode, other: TSNode): boolean -- cgit From 31c4ed26bc278282898123ad21bb6fead401fd6f Mon Sep 17 00:00:00 2001 From: Christian Clason Date: Fri, 11 Aug 2023 17:05:17 +0200 Subject: feat(treesitter): add injection language fallback (#24659) * feat(treesitter): add injection language fallback Problem: injection languages are often specified via aliases (e.g., filetype or in upper case), requiring custom directives. Solution: include lookup logic (try as parser name, then filetype, then lowercase) in LanguageTree itself and remove `#inject-language` directive. Co-authored-by: Lewis Russell --- runtime/lua/vim/treesitter/languagetree.lua | 27 +++++++++++++++++++++++++-- runtime/lua/vim/treesitter/query.lua | 27 --------------------------- 2 files changed, 25 insertions(+), 29 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index 4b2628609a..7f1d6d1111 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -635,6 +635,29 @@ local function add_injection(t, tree_index, pattern, lang, combined, ranges) table.insert(t[tree_index][lang][pattern].regions, ranges) end +-- TODO(clason): replace by refactored `ts.has_parser` API (without registering) +---@param lang string parser name +---@return boolean # true if parser for {lang} exists on rtp +local has_parser = function(lang) + return vim._ts_has_language(lang) + or #vim.api.nvim_get_runtime_file('parser/' .. lang .. '.*', false) > 0 +end + +--- Return parser name for language (if exists) or filetype (if registered and exists) +--- +---@param alias string language or filetype name +---@return string? # resolved parser name +local function resolve_lang(alias) + if has_parser(alias) then + return alias + end + + local lang = vim.treesitter.language.get_lang(alias) + if lang and has_parser(lang) then + return lang + end +end + ---@private --- Extract injections according to: --- https://tree-sitter.github.io/tree-sitter/syntax-highlighting#language-injection @@ -649,10 +672,10 @@ function LanguageTree:_get_injection(match, metadata) for id, node in pairs(match) do local name = self._injection_query.captures[id] - -- Lang should override any other language tag if name == 'injection.language' then - lang = vim.treesitter.get_node_text(node, self._source, { metadata = metadata[id] }) + local text = vim.treesitter.get_node_text(node, self._source, { metadata = metadata[id] }) + lang = resolve_lang(text) or resolve_lang(text:lower()) elseif name == 'injection.content' then ranges = get_node_ranges(node, self._source, metadata[id], include_children) end diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index 3b7e74c0cf..c3213e0192 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -541,33 +541,6 @@ local directive_handlers = { metadata.range = { start_row, start_col, end_row, end_col } end end, - -- Set injection language from node text, interpreted first as language and then as filetype - -- Example: (#inject-language! @_lang) - ['inject-language!'] = function(match, _, bufnr, pred, metadata) - local id = pred[2] - local node = match[id] - if not node then - return - end - - -- TODO(clason): replace by refactored `ts.has_parser` API - local has_parser = function(lang) - return vim._ts_has_language(lang) - or #vim.api.nvim_get_runtime_file('parser/' .. lang .. '.*', false) > 0 - end - - local alias = vim.treesitter.get_node_text(node, bufnr, { metadata = metadata[id] }) - if not alias then - return - elseif has_parser(alias) then - metadata['injection.language'] = alias - else - local lang = vim.treesitter.language.get_lang(alias) - if lang and has_parser(lang) then - metadata['injection.language'] = lang - end - end - end, } --- Adds a new predicate to be used in queries -- cgit From 2ca076e45fb3f1c08f6a1a374834df0701b8d778 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Thu, 10 Aug 2023 14:21:56 +0100 Subject: feat(treesitter)!: incremental injection parsing Problem: Treesitter highlighting is slow for large files with lots of injections. Solution: Only parse injections we are going to render during a redraw cycle. --- - `LanguageTree:parse()` will no longer parse injections by default and now requires an explicit range argument to be passed. - `TSHighlighter` now parses injections incrementally during on_win callbacks for the line range being rendered. - Plugins which require certain injections to be parsed must run `parser:parse({ start_row, end_row })` before using the tree. --- runtime/lua/vim/treesitter/_fold.lua | 7 +- runtime/lua/vim/treesitter/_meta.lua | 7 +- runtime/lua/vim/treesitter/_range.lua | 9 +- runtime/lua/vim/treesitter/dev.lua | 2 +- runtime/lua/vim/treesitter/highlighter.lua | 24 ++-- runtime/lua/vim/treesitter/languagetree.lua | 207 +++++++++++++++++++--------- runtime/lua/vim/treesitter/query.lua | 1 + 7 files changed, 173 insertions(+), 84 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/_fold.lua b/runtime/lua/vim/treesitter/_fold.lua index a02d0a584d..912a6e8a9f 100644 --- a/runtime/lua/vim/treesitter/_fold.lua +++ b/runtime/lua/vim/treesitter/_fold.lua @@ -147,11 +147,14 @@ local function normalise_erow(bufnr, erow) return math.min(erow or max_erow, max_erow) end +-- TODO(lewis6991): Setup a decor provider so injections folds can be parsed +-- as the window is redrawn ---@param bufnr integer ---@param info TS.FoldInfo ---@param srow integer? ---@param erow integer? -local function get_folds_levels(bufnr, info, srow, erow) +---@param parse_injections? boolean +local function get_folds_levels(bufnr, info, srow, erow, parse_injections) srow = srow or 0 erow = normalise_erow(bufnr, erow) @@ -162,7 +165,7 @@ local function get_folds_levels(bufnr, info, srow, erow) local parser = ts.get_parser(bufnr) - parser:parse() + parser:parse(parse_injections and { srow, erow } or nil) parser:for_each_tree(function(tree, ltree) local query = ts.query.get(ltree:lang(), 'folds') diff --git a/runtime/lua/vim/treesitter/_meta.lua b/runtime/lua/vim/treesitter/_meta.lua index 36b1a9bbf8..d8babc9402 100644 --- a/runtime/lua/vim/treesitter/_meta.lua +++ b/runtime/lua/vim/treesitter/_meta.lua @@ -1,6 +1,6 @@ ---@meta ----@class TSNode +---@class TSNode: userdata ---@field id fun(self: TSNode): integer ---@field tree fun(self: TSNode): TSTree ---@field range fun(self: TSNode, include_bytes: false?): integer, integer, integer, integer @@ -51,7 +51,7 @@ function TSNode:_rawquery(query, captures, start, end_, opts) end ---@field parse fun(self: TSParser, tree: TSTree?, source: integer|string, include_bytes: boolean?): TSTree, integer[] ---@field reset fun(self: TSParser) ---@field included_ranges fun(self: TSParser, include_bytes: boolean?): integer[] ----@field set_included_ranges fun(self: TSParser, ranges: Range6[]) +---@field set_included_ranges fun(self: TSParser, ranges: (Range6|TSNode)[]) ---@field set_timeout fun(self: TSParser, timeout: integer) ---@field timeout fun(self: TSParser): integer ---@field _set_logger fun(self: TSParser, lex: boolean, parse: boolean, cb: TSLoggerCallback) @@ -61,7 +61,8 @@ function TSNode:_rawquery(query, captures, start, end_, opts) end ---@field root fun(self: TSTree): TSNode ---@field edit fun(self: TSTree, _: integer, _: integer, _: integer, _: integer, _: integer, _: integer, _: integer, _: integer, _:integer) ---@field copy fun(self: TSTree): TSTree ----@field included_ranges fun(self: TSTree, include_bytes: boolean?): integer[] +---@field included_ranges fun(self: TSTree, include_bytes: true): Range6[] +---@field included_ranges fun(self: TSTree, include_bytes: false): Range4[] ---@return integer vim._ts_get_language_version = function() end diff --git a/runtime/lua/vim/treesitter/_range.lua b/runtime/lua/vim/treesitter/_range.lua index 35081c6400..8d727c3c52 100644 --- a/runtime/lua/vim/treesitter/_range.lua +++ b/runtime/lua/vim/treesitter/_range.lua @@ -2,6 +2,10 @@ local api = vim.api local M = {} +---@class Range2 +---@field [1] integer start row +---@field [2] integer end row + ---@class Range4 ---@field [1] integer start row ---@field [2] integer start column @@ -16,7 +20,7 @@ local M = {} ---@field [5] integer end column ---@field [6] integer end bytes ----@alias Range Range4|Range6 +---@alias Range Range2|Range4|Range6 ---@private ---@param a_row integer @@ -111,6 +115,9 @@ end ---@param r Range ---@return integer, integer, integer, integer function M.unpack4(r) + if #r == 2 then + return r[1], 0, r[2], 0 + end local off_1 = #r == 6 and 1 or 0 return r[1], r[2], r[3 + off_1], r[4 + off_1] end diff --git a/runtime/lua/vim/treesitter/dev.lua b/runtime/lua/vim/treesitter/dev.lua index e94e8f08dc..f7625eb94b 100644 --- a/runtime/lua/vim/treesitter/dev.lua +++ b/runtime/lua/vim/treesitter/dev.lua @@ -99,7 +99,7 @@ function TSTreeView:new(bufnr, lang) -- For each child tree (injected language), find the root of the tree and locate the node within -- the primary tree that contains that root. Add a mapping from the node in the primary tree to -- the root in the child tree to the {injections} table. - local root = parser:parse()[1]:root() + local root = parser:parse(true)[1]:root() local injections = {} ---@type table parser:for_each_child(function(child, lang_) child:for_each_tree(function(tree) diff --git a/runtime/lua/vim/treesitter/highlighter.lua b/runtime/lua/vim/treesitter/highlighter.lua index f8ec5b175d..56b075b723 100644 --- a/runtime/lua/vim/treesitter/highlighter.lua +++ b/runtime/lua/vim/treesitter/highlighter.lua @@ -1,5 +1,6 @@ local api = vim.api local query = vim.treesitter.query +local Range = require('vim.treesitter._range') ---@alias TSHlIter fun(): integer, TSNode, TSMetadata @@ -14,6 +15,7 @@ local query = vim.treesitter.query ---@field _highlight_states table ---@field _queries table ---@field tree LanguageTree +---@field redraw_count integer local TSHighlighter = rawget(vim.treesitter, 'TSHighlighter') or {} TSHighlighter.__index = TSHighlighter @@ -139,6 +141,7 @@ function TSHighlighter.new(tree, opts) return self end +--- @nodoc --- Removes all internal references to the highlighter function TSHighlighter:destroy() if TSHighlighter.active[self.bufnr] then @@ -186,7 +189,7 @@ function TSHighlighter:on_detach() end ---@package ----@param changes Range6[][] +---@param changes Range6[] function TSHighlighter:on_changedtree(changes) for _, ch in ipairs(changes) do api.nvim__buf_redraw_range(self.bufnr, ch[1], ch[4] + 1) @@ -245,7 +248,7 @@ local function on_line_impl(self, buf, line, is_spell_nav) end local range = vim.treesitter.get_range(node, buf, metadata[capture]) - local start_row, start_col, _, end_row, end_col, _ = unpack(range) + local start_row, start_col, end_row, end_col = Range.unpack4(range) local hl = highlighter_query.hl_cache[capture] local capture_name = highlighter_query:query().captures[capture] @@ -309,32 +312,23 @@ function TSHighlighter._on_spell_nav(_, _, buf, srow, _, erow, _) end end ----@private ----@param buf integer -function TSHighlighter._on_buf(_, buf) - local self = TSHighlighter.active[buf] - if self then - self.tree:parse() - end -end - ---@private ---@param _win integer ---@param buf integer ----@param _topline integer -function TSHighlighter._on_win(_, _win, buf, _topline) +---@param topline integer +---@param botline integer +function TSHighlighter._on_win(_, _win, buf, topline, botline) local self = TSHighlighter.active[buf] if not self then return false end - + self.tree:parse({ topline, botline }) self:reset_highlight_state() self.redraw_count = self.redraw_count + 1 return true end api.nvim_set_decoration_provider(ns, { - on_buf = TSHighlighter._on_buf, on_win = TSHighlighter._on_win, on_line = TSHighlighter._on_line, _on_spell_nav = TSHighlighter._on_spell_nav, diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index 7f1d6d1111..f274edf961 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -18,7 +18,7 @@ --- Whenever you need to access the current syntax tree, parse the buffer: --- ---
lua
----     local tree = parser:parse()
+---     local tree = parser:parse({ start_row, end_row })
 --- 
--- --- This returns a table of immutable |treesitter-tree| objects representing the current state of @@ -74,6 +74,7 @@ local TSCallbackNames = { ---@field package _callbacks_rec table Callback handlers (recursive) ---@field private _children table Injected languages ---@field private _injection_query Query Queries defining injected languages +---@field private _injections_processed boolean ---@field private _opts table Options ---@field private _parser TSParser Parser for language ---@field private _has_regions boolean @@ -115,7 +116,9 @@ function LanguageTree.new(source, lang, opts) end local injections = opts.injections or {} - local self = setmetatable({ + + --- @type LanguageTree + local self = { _source = source, _lang = lang, _children = {}, @@ -123,14 +126,19 @@ 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), _callbacks = {}, _callbacks_rec = {}, - }, LanguageTree) + } + + setmetatable(self, LanguageTree) if vim.g.__ts_debug and type(vim.g.__ts_debug) == 'number' then self:_set_logger() + self:_log('START') end for _, name in pairs(TSCallbackNames) do @@ -141,6 +149,7 @@ function LanguageTree.new(source, lang, opts) return self end +--- @private function LanguageTree:_set_logger() local source = self:source() source = type(source) == 'string' and 'text' or tostring(source) @@ -171,7 +180,7 @@ end ---Measure execution time of a function ---@generic R1, R2, R3 ---@param f fun(): R1, R2, R2 ----@return integer, R1, R2, R3 +---@return number, R1, R2, R3 local function tcall(f, ...) local start = vim.uv.hrtime() ---@diagnostic disable-next-line @@ -219,7 +228,7 @@ function LanguageTree:invalidate(reload) -- buffer was reloaded, reparse all trees if reload then - for _, t in ipairs(self._trees) do + for _, t in pairs(self._trees) do self:_do_callback('changedtree', t:included_ranges(true), t) end self._trees = {} @@ -250,14 +259,18 @@ function LanguageTree:is_valid(exclude_children) local valid = self._valid if type(valid) == 'table' then - for _, v in ipairs(valid) do - if not v then + for i = 1, #self:included_regions() do + if not valid[i] then return false end end end if not exclude_children then + if not self._injections_processed then + return false + end + for _, child in pairs(self._children) do if not child:is_valid(exclude_children) then return false @@ -265,9 +278,12 @@ function LanguageTree:is_valid(exclude_children) end end - assert(type(valid) == 'boolean') + if type(valid) == 'boolean' then + return valid + end - return valid + self._valid = true + return true end --- Returns a map of language to child tree. @@ -280,47 +296,72 @@ function LanguageTree:source() return self._source end ---- Parses all defined regions using a treesitter parser ---- for the language this tree represents. ---- This will run the injection query for this language to ---- determine if any child languages should be created. ---- ----@return TSTree[] -function LanguageTree:parse() - if self:is_valid() then - self:_log('valid') - return self._trees +--- @param region Range6[] +--- @param range? boolean|Range +--- @return boolean +local function intercepts_region(region, range) + if #region == 0 then + return true end - local changes = {} + if range == nil then + return false + end - -- Collect some stats - local regions_parsed = 0 + 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? +--- @return integer[] changes +--- @return integer no_regions_parsed +--- @return number total_parse_time +function LanguageTree:_parse_regions(range) + local changes = {} + local no_regions_parsed = 0 local total_parse_time = 0 - --- At least 1 region is invalid - if not self:is_valid(true) then - -- If there are no ranges, set to an empty list - -- so the included ranges in the parser are cleared. - for i, ranges in ipairs(self:included_regions()) do - if not self._valid or not self._valid[i] then - self._parser:set_included_ranges(ranges) - local parse_time, tree, tree_changes = - tcall(self._parser.parse, self._parser, self._trees[i], self._source, true) - - -- 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._trees[i] = tree - vim.list_extend(changes, tree_changes) - - total_parse_time = total_parse_time + parse_time - regions_parsed = regions_parsed + 1 - end + 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] and intercepts_region(ranges, range) then + self._parser:set_included_ranges(ranges) + local parse_time, tree, tree_changes = + tcall(self._parser.parse, self._parser, self._trees[i], self._source, true) + + -- 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._trees[i] = tree + vim.list_extend(changes, tree_changes) + + total_parse_time = total_parse_time + parse_time + no_regions_parsed = no_regions_parsed + 1 + self._valid[i] = true end end + return changes, no_regions_parsed, total_parse_time +end + +--- @private +--- @return number +function LanguageTree:_add_injections() local seen_langs = {} ---@type table local query_time, injections_by_lang = tcall(self._get_injections, self) @@ -348,19 +389,60 @@ function LanguageTree:parse() end end + return query_time +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. +--- +--- Any region with empty range (`{}`, typically only the root tree) is always parsed; +--- otherwise (typically injections) only if it intersects {range} (or if {range} is `true`). +--- +--- @param range boolean|Range|nil: Parse this range in the parser's source. +--- 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 TSTree[] +function LanguageTree:parse(range) + if self:is_valid() then + self:_log('valid') + return self._trees + end + + local changes --- @type Range6? + + -- Collect some stats + local no_regions_parsed = 0 + local query_time = 0 + local total_parse_time = 0 + + --- At least 1 region is invalid + if not self:is_valid(true) then + changes, no_regions_parsed, total_parse_time = self:_parse_regions(range) + -- Need to run injections when we parsed something + if no_regions_parsed > 0 then + self._injections_processed = false + end + end + + if not self._injections_processed and range ~= false and range ~= nil then + query_time = self:_add_injections() + self._injections_processed = true + end + self:_log({ - changes = changes, - regions_parsed = regions_parsed, + changes = changes and #changes > 0 and changes or nil, + regions_parsed = no_regions_parsed, parse_time = total_parse_time, query_time = query_time, + range = range, }) self:for_each_child(function(child) - child:parse() + child:parse(range) end) - self._valid = true - return self._trees end @@ -384,7 +466,7 @@ end --- ---@param fn fun(tree: TSTree, ltree: LanguageTree) function LanguageTree:for_each_tree(fn) - for _, tree in ipairs(self._trees) do + for _, tree in pairs(self._trees) do fn(tree, self) end @@ -466,18 +548,17 @@ function LanguageTree:_iter_regions(fn) return end - if type(self._valid) ~= 'table' then + local was_valid = type(self._valid) ~= 'table' + + if was_valid then + self:_log('was valid', self._valid) self._valid = {} end local all_valid = true for i, region in ipairs(self:included_regions()) do - if self._valid[i] == nil then - self._valid[i] = true - end - - if self._valid[i] then + if was_valid or self._valid[i] then self._valid[i] = fn(i, region) if not self._valid[i] then self:_log(function() @@ -521,6 +602,8 @@ function LanguageTree:set_included_regions(new_regions) for i, range in ipairs(region) do if type(range) == 'table' and #range == 4 then region[i] = Range.add_bytes(self._source, range) + elseif type(range) == 'userdata' then + region[i] = { range:range(true) } end end end @@ -542,7 +625,7 @@ function LanguageTree:set_included_regions(new_regions) end ---Gets the set of included regions ----@return integer[][] +---@return Range6[][] function LanguageTree:included_regions() if self._regions then return self._regions @@ -581,7 +664,7 @@ local function get_node_ranges(node, source, metadata, include_children) -- We are excluding children so we need to mask out their ranges for i = 0, child_count - 1 do - local child = node:named_child(i) + local child = assert(node:named_child(i)) local c_srow, c_scol, c_sbyte, c_erow, c_ecol, c_ebyte = child:range(true) if c_srow > srow or c_scol > scol then ranges[#ranges + 1] = { srow, scol, sbyte, c_srow, c_scol, c_sbyte } @@ -749,8 +832,8 @@ end --- --- TODO: Allow for an offset predicate to tailor the injection range --- instead of using the entire nodes range. ----@private ----@return table +--- @private +--- @return table function LanguageTree:_get_injections() if not self._injection_query then return {} @@ -759,7 +842,7 @@ function LanguageTree:_get_injections() ---@type table local injections = {} - for tree_index, tree in ipairs(self._trees) do + for index, tree in pairs(self._trees) do local root_node = tree:root() local start_line, _, end_line, _ = root_node:range() @@ -771,7 +854,7 @@ function LanguageTree:_get_injections() -- TODO(lewis6991): remove after 0.9 (#20434) lang, combined, ranges = self:_get_injection_deprecated(match, metadata) end - add_injection(injections, tree_index, pattern, lang, combined, ranges) + add_injection(injections, index, pattern, lang, combined, ranges) end end @@ -794,7 +877,7 @@ function LanguageTree:_get_injections() end, entry.regions) table.insert(result[lang], regions) else - for _, ranges in ipairs(entry.regions) do + for _, ranges in pairs(entry.regions) do table.insert(result[lang], ranges) end end @@ -828,7 +911,7 @@ function LanguageTree:_edit( end_row_new, end_col_new ) - for _, tree in ipairs(self._trees) do + for _, tree in pairs(self._trees) do tree:edit( start_byte, end_byte_old, diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index c3213e0192..3093657313 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -435,6 +435,7 @@ predicate_handlers['vim-match?'] = predicate_handlers['match?'] ---@class TSMetadata ---@field range? Range +---@field conceal? string ---@field [integer] TSMetadata ---@field [string] integer|string -- cgit From 8179d68dc1a90f47bfb307d73e71adc98883ae00 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Sun, 13 Aug 2023 08:03:56 +0100 Subject: fix(treesitter): logger memory leak --- runtime/lua/vim/treesitter/languagetree.lua | 4 +++- 1 file changed, 3 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 f274edf961..b4c9027794 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -156,6 +156,7 @@ function LanguageTree:_set_logger() local lang = self:lang() + vim.fn.mkdir(vim.fn.stdpath('log'), 'p') local logfilename = vim.fs.joinpath(vim.fn.stdpath('log'), 'treesitter.log') local logfile, openerr = io.open(logfilename, 'a+') @@ -208,7 +209,8 @@ function LanguageTree:_log(...) local info = debug.getinfo(2, 'nl') local nregions = #self:included_regions() - local prefix = string.format('%s:%d: (#regions=%d) ', info.name, info.currentline, nregions) + local prefix = + string.format('%s:%d: (#regions=%d) ', info.name or '???', info.currentline or 0, nregions) local msg = { prefix } for _, x in ipairs(args) do -- cgit From fc0ee871de2f56dbb80879c912203a6372c54e1c Mon Sep 17 00:00:00 2001 From: Christian Clason Date: Sat, 12 Aug 2023 17:54:04 +0200 Subject: fix(treesitter)!: remove deprecated legacy injection format --- runtime/lua/vim/treesitter/languagetree.lua | 67 ++--------------------------- 1 file changed, 4 insertions(+), 63 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index b4c9027794..4b419c4744 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -769,65 +769,6 @@ function LanguageTree:_get_injection(match, metadata) return lang, combined, ranges end ----@private ----@param match table ----@param metadata TSMetadata ----@return string, boolean, Range6[] -function LanguageTree:_get_injection_deprecated(match, metadata) - local lang = nil ---@type string - local ranges = {} ---@type Range6[] - local combined = metadata.combined ~= nil - - -- Directives can configure how injections are captured as well as actual node captures. - -- This allows more advanced processing for determining ranges and language resolution. - if metadata.content then - local content = metadata.content ---@type any - - -- Allow for captured nodes to be used - if type(content) == 'number' then - content = { match[content]:range() } - end - - if type(content) == 'table' and #content >= 4 then - vim.list_extend(ranges, content) - end - end - - local mlang = metadata.language - if mlang ~= nil then - assert(type(mlang) == 'string') - lang = mlang - end - - -- You can specify the content and language together - -- using a tag with the language, for example - -- @javascript - for id, node in pairs(match) do - local name = self._injection_query.captures[id] - - -- Lang should override any other language tag - if name == 'language' and not lang then - lang = vim.treesitter.get_node_text(node, self._source, { metadata = metadata[id] }) - elseif name == 'combined' then - combined = true - elseif name == 'content' and #ranges == 0 then - ranges[#ranges + 1] = vim.treesitter.get_range(node, self._source, metadata[id]) - -- Ignore any tags that start with "_" - -- Allows for other tags to be used in matches - elseif string.sub(name, 1, 1) ~= '_' then - if not lang then - lang = name - end - - if #ranges == 0 then - ranges[#ranges + 1] = vim.treesitter.get_range(node, self._source, metadata[id]) - end - end - end - - return lang, combined, ranges -end - --- Gets language injection points by language. --- --- This is where most of the injection processing occurs. @@ -852,11 +793,11 @@ function LanguageTree:_get_injections() self._injection_query:iter_matches(root_node, self._source, start_line, end_line + 1) do local lang, combined, ranges = self:_get_injection(match, metadata) - if not lang then - -- TODO(lewis6991): remove after 0.9 (#20434) - lang, combined, ranges = self:_get_injection_deprecated(match, metadata) + if lang then + add_injection(injections, index, pattern, lang, combined, ranges) + else + self:_log('match from injection query failed for pattern', pattern) end - add_injection(injections, index, pattern, lang, combined, ranges) end end -- cgit From 54be7d6b45a9ae51c218c0e5c1c20dd608b97297 Mon Sep 17 00:00:00 2001 From: Pham Huy Hoang Date: Wed, 16 Aug 2023 15:02:15 +0900 Subject: docs(lua): add missing fields to treesitter/_meta According to `:h TSNode` docs, there's also `TSNode:sexpr()` and `TSNode:has_error()` that is part of `TSNode` class, but this wasn't documented in `treesitter/_meta.lua`. Adding missing fields in so the types is similar to `:h TSNode` --- runtime/lua/vim/treesitter/_meta.lua | 2 ++ 1 file changed, 2 insertions(+) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/_meta.lua b/runtime/lua/vim/treesitter/_meta.lua index d8babc9402..9a94f12c16 100644 --- a/runtime/lua/vim/treesitter/_meta.lua +++ b/runtime/lua/vim/treesitter/_meta.lua @@ -25,6 +25,8 @@ ---@field prev_named_sibling fun(self: TSNode): TSNode? ---@field named_children fun(self: TSNode): TSNode[] ---@field has_changes fun(self: TSNode): boolean +---@field has_error fun(self: TSNode): boolean +---@field sexpr fun(self: TSNode): string ---@field equal fun(self: TSNode, other: TSNode): boolean ---@field iter_children fun(self: TSNode): fun(): TSNode, string local TSNode = {} -- cgit From c6ec7fa8d741d6301701067ecd095bf02e7a741a Mon Sep 17 00:00:00 2001 From: Amaan Qureshi Date: Tue, 22 Aug 2023 00:51:38 -0400 Subject: feat(treesitter): add 'injection.self' and 'injection.parent' Co-authored-by: ObserverOfTime --- runtime/lua/vim/treesitter/languagetree.lua | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index 4b419c4744..e81778b269 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -82,6 +82,7 @@ local TSCallbackNames = { ---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 +---@field private _parent_lang? string Parent language name ---@field private _source (integer|string) Buffer or string to parse ---@field private _trees TSTree[] Reference to parsed tree (one for each language) ---@field private _valid boolean|table If the parsed tree is valid @@ -105,8 +106,9 @@ LanguageTree.__index = LanguageTree ---@param opts (table|nil) Optional arguments: --- - injections table Map of language to injection query strings. Overrides the --- built-in runtime file searching for language injections. +---@param parent_lang? string Parent language name of this tree ---@return LanguageTree parser object -function LanguageTree.new(source, lang, opts) +function LanguageTree.new(source, lang, opts, parent_lang) language.add(lang) ---@type LanguageTreeOpts opts = opts or {} @@ -121,6 +123,7 @@ function LanguageTree.new(source, lang, opts) local self = { _source = source, _lang = lang, + _parent_lang = parent_lang, _children = {}, _trees = {}, _opts = opts, @@ -489,7 +492,7 @@ function LanguageTree:add_child(lang) self:remove_child(lang) end - local child = LanguageTree.new(self._source, lang, self._opts) + local child = LanguageTree.new(self._source, lang, self._opts, self:lang()) -- Inherit recursive callbacks for nm, cb in pairs(self._callbacks_rec) do @@ -752,7 +755,9 @@ end function LanguageTree:_get_injection(match, metadata) local ranges = {} ---@type Range6[] local combined = metadata['injection.combined'] ~= nil - local lang = metadata['injection.language'] --[[@as string?]] + local lang = metadata['injection.self'] ~= nil and self:lang() + or metadata['injection.parent'] ~= nil and self._parent_lang + or metadata['injection.language'] --[[@as string?]] local include_children = metadata['injection.include-children'] ~= nil for id, node in pairs(match) do -- cgit From 5d8ab32f3871b0232972cac1116ac7cba98389e5 Mon Sep 17 00:00:00 2001 From: Maria José Solano Date: Fri, 25 Aug 2023 11:17:36 -0700 Subject: feat(treesitter): add a query editor (#24703) --- runtime/lua/vim/treesitter/dev.lua | 198 +++++++++++++++++++++++++++++++++---- 1 file changed, 179 insertions(+), 19 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/dev.lua b/runtime/lua/vim/treesitter/dev.lua index f7625eb94b..b7f2c0e473 100644 --- a/runtime/lua/vim/treesitter/dev.lua +++ b/runtime/lua/vim/treesitter/dev.lua @@ -124,7 +124,7 @@ function TSTreeView:new(bufnr, lang) end local t = { - ns = api.nvim_create_namespace(''), + ns = api.nvim_create_namespace('treesitter/dev-inspect'), nodes = nodes, named = named, opts = { @@ -158,6 +158,29 @@ local function escape_quotes(text) return string.format('"%s"', text:sub(2, #text - 1):gsub('"', '\\"')) end +---@param w integer +---@return boolean closed Whether the window was closed. +local function close_win(w) + if api.nvim_win_is_valid(w) then + api.nvim_win_close(w, true) + return true + end + + return false +end + +---@param w integer +---@param b integer +local function set_dev_properties(w, b) + vim.wo[w].scrolloff = 5 + vim.wo[w].wrap = false + vim.wo[w].foldmethod = 'manual' -- disable folding + vim.bo[b].buflisted = false + vim.bo[b].buftype = 'nofile' + vim.bo[b].bufhidden = 'wipe' + vim.bo[b].filetype = 'query' +end + --- Write the contents of this View into {bufnr}. --- ---@param bufnr integer Buffer number to write into. @@ -247,12 +270,9 @@ function M.inspect_tree(opts) local win = api.nvim_get_current_win() local pg = assert(TSTreeView:new(buf, opts.lang)) - -- Close any existing dev window - if vim.b[buf].dev then - local w = vim.b[buf].dev - if api.nvim_win_is_valid(w) then - api.nvim_win_close(w, true) - end + -- Close any existing inspector window + if vim.b[buf].dev_inspect then + close_win(vim.b[buf].dev_inspect) end local w = opts.winid @@ -268,16 +288,10 @@ function M.inspect_tree(opts) b = api.nvim_win_get_buf(w) end - vim.b[buf].dev = w - - vim.wo[w].scrolloff = 5 - vim.wo[w].wrap = false - vim.wo[w].foldmethod = 'manual' -- disable folding - vim.bo[b].buflisted = false - vim.bo[b].buftype = 'nofile' - vim.bo[b].bufhidden = 'wipe' + vim.b[buf].dev_inspect = w + vim.b[b].dev_base = win -- base window handle vim.b[b].disable_query_linter = true - vim.bo[b].filetype = 'query' + set_dev_properties(w, b) local title --- @type string? local opts_title = opts.title @@ -306,7 +320,7 @@ function M.inspect_tree(opts) api.nvim_buf_set_keymap(b, 'n', 'a', '', { desc = 'Toggle anonymous nodes', callback = function() - local row, col = unpack(api.nvim_win_get_cursor(w)) + local row, col = unpack(api.nvim_win_get_cursor(w)) ---@type integer, integer local curnode = pg:get(row) while curnode and not curnode.named do row = row - 1 @@ -336,6 +350,15 @@ function M.inspect_tree(opts) pg:draw(b) end, }) + api.nvim_buf_set_keymap(b, 'n', 'o', '', { + desc = 'Toggle query previewer', + callback = function() + local preview_w = vim.b[buf].dev_preview + if not preview_w or not close_win(preview_w) then + M.preview_query() + end + end, + }) local group = api.nvim_create_augroup('treesitter/dev', {}) @@ -436,11 +459,148 @@ function M.inspect_tree(opts) buffer = buf, once = true, callback = function() - if api.nvim_win_is_valid(w) then - api.nvim_win_close(w, true) + close_win(w) + end, + }) +end + +local preview_ns = api.nvim_create_namespace('treesitter/dev-preview') + +---@param query_win integer +---@param base_win integer +local function update_preview_highlights(query_win, base_win) + local base_buf = api.nvim_win_get_buf(base_win) + local query_buf = api.nvim_win_get_buf(query_win) + local parser = vim.treesitter.get_parser(base_buf) + local lang = parser:lang() + api.nvim_buf_clear_namespace(base_buf, preview_ns, 0, -1) + local query_content = table.concat(api.nvim_buf_get_lines(query_buf, 0, -1, false), '\n') + + local ok_query, query = pcall(vim.treesitter.query.parse, lang, query_content) + if not ok_query then + return + end + + local cursor_word = vim.fn.expand('') --[[@as string]] + -- Only highlight captures if the cursor is on a capture name + if cursor_word:find('^@') == nil then + return + end + -- Remove the '@' from the cursor word + cursor_word = cursor_word:sub(2) + local topline, botline = vim.fn.line('w0', base_win), vim.fn.line('w$', base_win) + for id, node in query:iter_captures(parser:trees()[1]:root(), base_buf, topline - 1, botline) do + local capture_name = query.captures[id] + if capture_name == cursor_word then + local lnum, col, end_lnum, end_col = node:range() + api.nvim_buf_set_extmark(base_buf, preview_ns, lnum, col, { + end_row = end_lnum, + end_col = end_col, + hl_group = 'Visual', + virt_text = { + { capture_name, 'Title' }, + }, + }) + end + end +end + +--- @private +function M.preview_query() + local buf = api.nvim_get_current_buf() + local win = api.nvim_get_current_win() + + -- Close any existing previewer window + if vim.b[buf].dev_preview then + close_win(vim.b[buf].dev_preview) + end + + local cmd = '60vnew' + -- If the inspector is open, place the previewer above it. + local base_win = vim.b[buf].dev_base ---@type integer? + local base_buf = base_win and api.nvim_win_get_buf(base_win) + local inspect_win = base_buf and vim.b[base_buf].dev_inspect + if base_win and base_buf and api.nvim_win_is_valid(inspect_win) then + vim.api.nvim_set_current_win(inspect_win) + buf = base_buf + win = base_win + cmd = 'new' + end + vim.cmd(cmd) + + local ok, parser = pcall(vim.treesitter.get_parser, buf) + if not ok then + return nil, 'No parser available for the given buffer' + end + local lang = parser:lang() + + local query_win = api.nvim_get_current_win() + local query_buf = api.nvim_win_get_buf(query_win) + + vim.b[buf].dev_preview = query_win + vim.bo[query_buf].omnifunc = 'v:lua.vim.treesitter.query.omnifunc' + set_dev_properties(query_win, query_buf) + + -- Note that omnifunc guesses the language based on the containing folder, + -- so we add the parser's language to the buffer's name so that omnifunc + -- can infer the language later. + api.nvim_buf_set_name(query_buf, string.format('%s/query_previewer.scm', lang)) + + local group = api.nvim_create_augroup('treesitter/dev-preview', {}) + api.nvim_create_autocmd({ 'TextChanged', 'InsertLeave' }, { + group = group, + buffer = query_buf, + desc = 'Update query previewer diagnostics when the query changes', + callback = function() + vim.treesitter.query.lint(query_buf, { langs = lang, clear = false }) + end, + }) + api.nvim_create_autocmd({ 'TextChanged', 'InsertLeave', 'CursorMoved', 'BufEnter' }, { + group = group, + buffer = query_buf, + desc = 'Update query previewer highlights when the cursor moves', + callback = function() + update_preview_highlights(query_win, win) + end, + }) + api.nvim_create_autocmd('BufLeave', { + group = group, + buffer = query_buf, + desc = 'Clear the query previewer highlights when leaving the previewer', + callback = function() + api.nvim_buf_clear_namespace(buf, preview_ns, 0, -1) + end, + }) + api.nvim_create_autocmd('BufLeave', { + group = group, + buffer = buf, + desc = 'Clear the query previewer highlights when leaving the source buffer', + callback = function() + if not api.nvim_buf_is_loaded(query_buf) then + return true end + + api.nvim_buf_clear_namespace(query_buf, preview_ns, 0, -1) + end, + }) + api.nvim_create_autocmd('BufHidden', { + group = group, + buffer = buf, + desc = 'Close the previewer window when the source buffer is hidden', + once = true, + callback = function() + close_win(query_win) end, }) + + api.nvim_buf_set_lines(query_buf, 0, -1, false, { + ';; Write your query here. Use @captures to highlight matches in the source buffer.', + ';; Completion for grammar nodes is available (see :h compl-omni)', + '', + '', + }) + vim.cmd('normal! G') + vim.cmd.startinsert() end return M -- cgit From 2bf3e82676d5caf674cf5ed1eb9677376d9cfa35 Mon Sep 17 00:00:00 2001 From: Maria José Solano Date: Sat, 26 Aug 2023 13:24:29 -0700 Subject: fix(treesitter): validate window before updating preview highlights --- runtime/lua/vim/treesitter/dev.lua | 4 +++- 1 file changed, 3 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 b7f2c0e473..72b6e3db4a 100644 --- a/runtime/lua/vim/treesitter/dev.lua +++ b/runtime/lua/vim/treesitter/dev.lua @@ -560,7 +560,9 @@ function M.preview_query() buffer = query_buf, desc = 'Update query previewer highlights when the cursor moves', callback = function() - update_preview_highlights(query_win, win) + if api.nvim_win_is_valid(win) then + update_preview_highlights(query_win, win) + end end, }) api.nvim_create_autocmd('BufLeave', { -- cgit From ffb340bf63af42ac347e23e0488898adc4391328 Mon Sep 17 00:00:00 2001 From: Jaehwang Jung Date: Thu, 24 Aug 2023 17:32:43 +0900 Subject: fix(treesitter): update folds only once on InsertLeave Problem: With treesitter fold, InsertLeave can be slow, because a single session of insert mode may schedule multiple fold updates in on_bytes and on_changedtree. Solution: Don't create duplicate autocmds. --- runtime/lua/vim/treesitter/_fold.lua | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/_fold.lua b/runtime/lua/vim/treesitter/_fold.lua index 912a6e8a9f..d82e04a5a8 100644 --- a/runtime/lua/vim/treesitter/_fold.lua +++ b/runtime/lua/vim/treesitter/_fold.lua @@ -235,6 +235,8 @@ local M = {} ---@type table local foldinfos = {} +local group = api.nvim_create_augroup('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). --- @@ -253,7 +255,15 @@ local function foldupdate(bufnr) if api.nvim_get_mode().mode == 'i' then -- foldUpdate() is guarded in insert mode. So update folds on InsertLeave + if #(api.nvim_get_autocmds({ + group = group, + buffer = bufnr, + })) > 0 then + return + end api.nvim_create_autocmd('InsertLeave', { + group = group, + buffer = bufnr, once = true, callback = do_update, }) -- cgit From 65738202f8be3ca63b75197d48f2c7a9324c035b Mon Sep 17 00:00:00 2001 From: Jaehwang Jung Date: Tue, 12 Sep 2023 04:29:39 +0900 Subject: fix(decorations): better approximation of botline #24794 Problem: * The guessed botline might be smaller than the actual botline e.g. when there are folds and the user is typing in insert mode. This may result in incorrect treesitter highlights for injections. * botline can be larger than the last line number of the buffer, which results in errors when placing extmarks. Solution: * Take a more conservative approximation. I am not sure if it is sufficient to guarantee correctness, but it seems to be good enough for the case mentioned above. * Clamp it to the last line number. Co-authored-by: Lewis Russell --- runtime/lua/vim/treesitter/highlighter.lua | 2 +- 1 file changed, 1 insertion(+), 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 56b075b723..8d4d6a9337 100644 --- a/runtime/lua/vim/treesitter/highlighter.lua +++ b/runtime/lua/vim/treesitter/highlighter.lua @@ -322,7 +322,7 @@ function TSHighlighter._on_win(_, _win, buf, topline, botline) if not self then return false end - self.tree:parse({ topline, botline }) + self.tree:parse({ topline, botline + 1 }) self:reset_highlight_state() self.redraw_count = self.redraw_count + 1 return true -- cgit From 6b5f44817e93c2985f3ea32122f1dc0047054018 Mon Sep 17 00:00:00 2001 From: L Lllvvuu Date: Mon, 11 Sep 2023 23:15:24 -0700 Subject: fix(languagetree): remove double recursion in LanguageTree:parse `LanguageTree:parse` is recursive, and calls `LanguageTree:for_each_child`, which is also recursive. That means that, starting from the third level (child of child of root), nodes will be parsed twice. Which then means that if the tree is N layers deep, there will be ~2^N parses even if the branching factor is 1. Now, why was the tree deepening with each character inserted? And why did this only regress in #24647? These are mysteries for another time. Fixes: #25104 --- 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 e81778b269..3c60da7643 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -444,9 +444,9 @@ function LanguageTree:parse(range) range = range, }) - self:for_each_child(function(child) + for _, child in pairs(self._children) do child:parse(range) - end) + end return self._trees end -- cgit From 7a76fb8547548304450b59624f9c75a554396504 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Tue, 12 Sep 2023 11:38:31 +0100 Subject: fix(treesitter): remove more double recursion Do not call `for_each_child` in functions that are already recursive. --- runtime/lua/vim/treesitter/dev.lua | 20 ++++++-------- runtime/lua/vim/treesitter/languagetree.lua | 43 ++++++++++++++++++----------- 2 files changed, 36 insertions(+), 27 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/dev.lua b/runtime/lua/vim/treesitter/dev.lua index 72b6e3db4a..67aa8670ba 100644 --- a/runtime/lua/vim/treesitter/dev.lua +++ b/runtime/lua/vim/treesitter/dev.lua @@ -101,17 +101,15 @@ function TSTreeView:new(bufnr, lang) -- the root in the child tree to the {injections} table. local root = parser:parse(true)[1]:root() local injections = {} ---@type table - parser:for_each_child(function(child, lang_) - child:for_each_tree(function(tree) - local r = tree:root() - local node = root:named_descendant_for_range(r:range()) - if node then - injections[node:id()] = { - lang = lang_, - root = r, - } - end - end) + parser:for_each_tree(function(tree, ltree) + local r = tree:root() + local node = root:named_descendant_for_range(r:range()) + if node then + injections[node:id()] = { + lang = ltree:lang(), + root = r, + } + end end) local nodes = traverse(root, 0, parser:lang(), injections, {}) diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index 3c60da7643..6037b17b20 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -897,6 +897,20 @@ function LanguageTree:_edit( end return true end) + + for _, child in pairs(self._children) do + child:_edit( + start_byte, + end_byte_old, + end_byte_new, + start_row, + start_col, + end_row_old, + end_col_old, + end_row_new, + end_col_new + ) + end end ---@package @@ -943,20 +957,17 @@ function LanguageTree:_on_bytes( ) -- Edit trees together BEFORE emitting a bytes callback. - ---@private - self:for_each_child(function(child) - child:_edit( - start_byte, - start_byte + old_byte, - start_byte + new_byte, - start_row, - start_col, - start_row + old_row, - old_end_col, - start_row + new_row, - new_end_col - ) - end, true) + self:_edit( + start_byte, + start_byte + old_byte, + start_byte + new_byte, + start_row, + start_col, + start_row + old_row, + old_end_col, + start_row + new_row, + new_end_col + ) self:_do_callback( 'bytes', @@ -1017,9 +1028,9 @@ function LanguageTree:register_cbs(cbs, recursive) end if recursive then - self:for_each_child(function(child) + for _, child in pairs(self._children) do child:register_cbs(cbs, true) - end) + end end end -- cgit From 1f551e068f728ff38bd7fdcfa3a6daf362bab9da Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Tue, 12 Sep 2023 15:26:57 +0100 Subject: fix(treesitter): fixup for InspectTree Fixes #25120 --- runtime/lua/vim/treesitter/dev.lua | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/dev.lua b/runtime/lua/vim/treesitter/dev.lua index 67aa8670ba..e7af259d28 100644 --- a/runtime/lua/vim/treesitter/dev.lua +++ b/runtime/lua/vim/treesitter/dev.lua @@ -101,16 +101,18 @@ function TSTreeView:new(bufnr, lang) -- the root in the child tree to the {injections} table. local root = parser:parse(true)[1]:root() local injections = {} ---@type table - parser:for_each_tree(function(tree, ltree) - local r = tree:root() - local node = root:named_descendant_for_range(r:range()) - if node then - injections[node:id()] = { - lang = ltree:lang(), - root = r, - } - end - end) + for _, child in pairs(parser:children()) do + child:for_each_tree(function(tree, ltree) + local r = tree:root() + local node = root:named_descendant_for_range(r:range()) + if node then + injections[node:id()] = { + lang = ltree:lang(), + root = r, + } + end + end) + end local nodes = traverse(root, 0, parser:lang(), injections, {}) -- cgit From 4607807f9fcb83d4e183f6f67e705ffd7f451077 Mon Sep 17 00:00:00 2001 From: Jaehwang Jung Date: Sun, 20 Aug 2023 14:17:45 +0900 Subject: fix(treesitter): don't update fold if tree is unchanged Problem: Folds are opened when the visible range changes even if there are no modifications to the buffer, e.g, when using zM for the first time. If the parsed tree was invalid, on_win re-parses and gets empty tree changes, which triggers fold updates. Solution: Don't update folds in on_changedtree if there are no changes. --- 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 d82e04a5a8..8bc08c9c2e 100644 --- a/runtime/lua/vim/treesitter/_fold.lua +++ b/runtime/lua/vim/treesitter/_fold.lua @@ -299,7 +299,9 @@ local function on_changedtree(bufnr, foldinfo, tree_changes) local srow, _, erow = Range.unpack4(change) get_folds_levels(bufnr, foldinfo, srow, erow) end - foldupdate(bufnr) + if #tree_changes > 0 then + foldupdate(bufnr) + end end) end -- cgit From a4743487b71b54f05063465d5f8cde8014bcb73c Mon Sep 17 00:00:00 2001 From: L Lllvvuu Date: Thu, 14 Sep 2023 00:08:43 -0700 Subject: fix(treesitter): `language.add` - only register parser if it exists Fixes: #24531 --- runtime/lua/vim/treesitter/language.lua | 4 ++-- 1 file changed, 2 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 9695e2c41c..15bf666a1e 100644 --- a/runtime/lua/vim/treesitter/language.lua +++ b/runtime/lua/vim/treesitter/language.lua @@ -82,9 +82,8 @@ function M.add(lang, opts) filetype = { filetype, { 'string', 'table' }, true }, }) - M.register(lang, filetype) - if vim._ts_has_language(lang) then + M.register(lang, filetype) return end @@ -102,6 +101,7 @@ function M.add(lang, opts) end vim._ts_add_language(path, lang, symbol_name) + M.register(lang, filetype) end --- @param x string|string[] -- cgit From 9fc321c9768d1a18893e14f46b0ebacef1be1db4 Mon Sep 17 00:00:00 2001 From: LW Date: Thu, 14 Sep 2023 03:36:16 -0700 Subject: refactor(treesitter): deprecate for_each_child #25118 The name for_each_child is misleading and caused bugs. After #25111, #25115, there are no more usages of `for_each_child` in Nvim. In the future if we want to restore this functionality we can consider a generalized vim.traverse(node, key, visitor) function. --- runtime/lua/vim/treesitter/languagetree.lua | 3 +++ 1 file changed, 3 insertions(+) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index 6037b17b20..1ea0f6b2b9 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -451,11 +451,14 @@ function LanguageTree:parse(range) return self._trees end +---@deprecated Misleading name. Use `LanguageTree:children()` (non-recursive) instead, +--- add recursion yourself if needed. --- Invokes the callback for each |LanguageTree| and its children recursively --- ---@param fn fun(tree: LanguageTree, lang: string) ---@param include_self boolean|nil Whether to include the invoking tree in the results function LanguageTree:for_each_child(fn, include_self) + vim.deprecate('LanguageTree:for_each_child()', 'LanguageTree:children()', '0.11') if include_self then fn(self, self._lang) end -- cgit From 2e92065686f62851318150a315591c30b8306a4b Mon Sep 17 00:00:00 2001 From: Gregory Anders <8965202+gpanders@users.noreply.github.com> Date: Thu, 14 Sep 2023 08:23:01 -0500 Subject: docs: replace
 with ``` (#25136)

---
 runtime/lua/vim/treesitter/languagetree.lua | 12 ++++++------
 runtime/lua/vim/treesitter/query.lua        | 18 +++++++++++-------
 2 files changed, 17 insertions(+), 13 deletions(-)

(limited to 'runtime/lua/vim/treesitter')

diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua
index 1ea0f6b2b9..79f36a27fd 100644
--- a/runtime/lua/vim/treesitter/languagetree.lua
+++ b/runtime/lua/vim/treesitter/languagetree.lua
@@ -7,9 +7,9 @@
 ---
 --- To create a LanguageTree (parser object) for a given buffer and language, use:
 ---
---- 
lua
----     local parser = vim.treesitter.get_parser(bufnr, lang)
---- 
+--- ```lua +--- local parser = vim.treesitter.get_parser(bufnr, lang) +--- ``` --- --- (where `bufnr=0` means current buffer). `lang` defaults to 'filetype'. --- Note: currently the parser is retained for the lifetime of a buffer but this may change; @@ -17,9 +17,9 @@ --- --- Whenever you need to access the current syntax tree, parse the buffer: --- ----
lua
----     local tree = parser:parse({ start_row, end_row })
---- 
+--- ```lua +--- local tree = parser:parse({ start_row, end_row }) +--- ``` --- --- This returns a table of immutable |treesitter-tree| objects representing the current state of --- the buffer. When the plugin wants to access the state after a (possible) edit it must call diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index 3093657313..350ccba7e4 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -692,7 +692,8 @@ end --- The iterator returns three values: a numeric id identifying the capture, --- the captured node, and metadata from any directives processing the match. --- The following example shows how to get captures by name: ----
lua
+---
+--- ```lua
 --- for id, node, metadata in query:iter_captures(tree:root(), bufnr, first, last) do
 ---   local name = query.captures[id] -- name of the capture in the query
 ---   -- typically useful info about the node:
@@ -700,7 +701,7 @@ end
 ---   local row1, col1, row2, col2 = node:range() -- range of the capture
 ---   -- ... use the info here ...
 --- end
---- 
+--- ``` --- ---@param node TSNode under which the search will occur ---@param source (integer|string) Source buffer or string to extract text from @@ -743,7 +744,8 @@ end --- If the query has more than one pattern, the capture table might be sparse --- and e.g. `pairs()` method should be used over `ipairs`. --- Here is an example iterating over all captures in every match: ----
lua
+---
+--- ```lua
 --- for pattern, match, metadata in cquery:iter_matches(tree:root(), bufnr, first, last) do
 ---   for id, node in pairs(match) do
 ---     local name = query.captures[id]
@@ -754,7 +756,7 @@ end
 ---     -- ... use the info here ...
 ---   end
 --- end
---- 
+--- ``` --- ---@param node TSNode under which the search will occur ---@param source (integer|string) Source buffer or string to search @@ -824,9 +826,11 @@ end --- Omnifunc for completing node names and predicates in treesitter queries. --- --- Use via ----
lua
----   vim.bo.omnifunc = 'v:lua.vim.treesitter.query.omnifunc'
---- 
+--- +--- ```lua +--- vim.bo.omnifunc = 'v:lua.vim.treesitter.query.omnifunc' +--- ``` +--- function M.omnifunc(findstart, base) return require('vim.treesitter._query_linter').omnifunc(findstart, base) end -- cgit From 28233bcb49067aaa70fa6e5fec14e2cc4bcaa315 Mon Sep 17 00:00:00 2001 From: Maria José Solano Date: Fri, 15 Sep 2023 03:10:55 -0700 Subject: refactor(treesitter): rename "preview" => "edit" #25161 "Edit" more closely describes the generic application than "Preview", though the buffer contents don't (yet) map to an actual file on disk. https://github.com/neovim/neovim/pull/24703#discussion_r1321719133 --- runtime/lua/vim/treesitter/dev.lua | 48 ++++++++++++++++++------------------ runtime/lua/vim/treesitter/query.lua | 9 +++++++ 2 files changed, 33 insertions(+), 24 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/dev.lua b/runtime/lua/vim/treesitter/dev.lua index e7af259d28..bc54853103 100644 --- a/runtime/lua/vim/treesitter/dev.lua +++ b/runtime/lua/vim/treesitter/dev.lua @@ -351,11 +351,11 @@ function M.inspect_tree(opts) end, }) api.nvim_buf_set_keymap(b, 'n', 'o', '', { - desc = 'Toggle query previewer', + desc = 'Toggle query editor', callback = function() - local preview_w = vim.b[buf].dev_preview - if not preview_w or not close_win(preview_w) then - M.preview_query() + local edit_w = vim.b[buf].dev_edit + if not edit_w or not close_win(edit_w) then + M.edit_query() end end, }) @@ -464,16 +464,16 @@ function M.inspect_tree(opts) }) end -local preview_ns = api.nvim_create_namespace('treesitter/dev-preview') +local edit_ns = api.nvim_create_namespace('treesitter/dev-edit') ---@param query_win integer ---@param base_win integer -local function update_preview_highlights(query_win, base_win) +local function update_editor_highlights(query_win, base_win) local base_buf = api.nvim_win_get_buf(base_win) local query_buf = api.nvim_win_get_buf(query_win) local parser = vim.treesitter.get_parser(base_buf) local lang = parser:lang() - api.nvim_buf_clear_namespace(base_buf, preview_ns, 0, -1) + api.nvim_buf_clear_namespace(base_buf, edit_ns, 0, -1) local query_content = table.concat(api.nvim_buf_get_lines(query_buf, 0, -1, false), '\n') local ok_query, query = pcall(vim.treesitter.query.parse, lang, query_content) @@ -493,7 +493,7 @@ local function update_preview_highlights(query_win, base_win) local capture_name = query.captures[id] if capture_name == cursor_word then local lnum, col, end_lnum, end_col = node:range() - api.nvim_buf_set_extmark(base_buf, preview_ns, lnum, col, { + api.nvim_buf_set_extmark(base_buf, edit_ns, lnum, col, { end_row = end_lnum, end_col = end_col, hl_group = 'Visual', @@ -506,17 +506,17 @@ local function update_preview_highlights(query_win, base_win) end --- @private -function M.preview_query() +function M.edit_query() local buf = api.nvim_get_current_buf() local win = api.nvim_get_current_win() - -- Close any existing previewer window - if vim.b[buf].dev_preview then - close_win(vim.b[buf].dev_preview) + -- Close any existing editor window + if vim.b[buf].dev_edit then + close_win(vim.b[buf].dev_edit) end local cmd = '60vnew' - -- If the inspector is open, place the previewer above it. + -- If the inspector is open, place the editor above it. local base_win = vim.b[buf].dev_base ---@type integer? local base_buf = base_win and api.nvim_win_get_buf(base_win) local inspect_win = base_buf and vim.b[base_buf].dev_inspect @@ -537,20 +537,20 @@ function M.preview_query() local query_win = api.nvim_get_current_win() local query_buf = api.nvim_win_get_buf(query_win) - vim.b[buf].dev_preview = query_win + vim.b[buf].dev_edit = query_win vim.bo[query_buf].omnifunc = 'v:lua.vim.treesitter.query.omnifunc' set_dev_properties(query_win, query_buf) -- Note that omnifunc guesses the language based on the containing folder, -- so we add the parser's language to the buffer's name so that omnifunc -- can infer the language later. - api.nvim_buf_set_name(query_buf, string.format('%s/query_previewer.scm', lang)) + api.nvim_buf_set_name(query_buf, string.format('%s/query_editor.scm', lang)) - local group = api.nvim_create_augroup('treesitter/dev-preview', {}) + local group = api.nvim_create_augroup('treesitter/dev-edit', {}) api.nvim_create_autocmd({ 'TextChanged', 'InsertLeave' }, { group = group, buffer = query_buf, - desc = 'Update query previewer diagnostics when the query changes', + desc = 'Update query editor diagnostics when the query changes', callback = function() vim.treesitter.query.lint(query_buf, { langs = lang, clear = false }) end, @@ -558,37 +558,37 @@ function M.preview_query() api.nvim_create_autocmd({ 'TextChanged', 'InsertLeave', 'CursorMoved', 'BufEnter' }, { group = group, buffer = query_buf, - desc = 'Update query previewer highlights when the cursor moves', + desc = 'Update query editor highlights when the cursor moves', callback = function() if api.nvim_win_is_valid(win) then - update_preview_highlights(query_win, win) + update_editor_highlights(query_win, win) end end, }) api.nvim_create_autocmd('BufLeave', { group = group, buffer = query_buf, - desc = 'Clear the query previewer highlights when leaving the previewer', + desc = 'Clear highlights when leaving the query editor', callback = function() - api.nvim_buf_clear_namespace(buf, preview_ns, 0, -1) + api.nvim_buf_clear_namespace(buf, edit_ns, 0, -1) end, }) api.nvim_create_autocmd('BufLeave', { group = group, buffer = buf, - desc = 'Clear the query previewer highlights when leaving the source buffer', + desc = 'Clear the query editor highlights when leaving the source buffer', callback = function() if not api.nvim_buf_is_loaded(query_buf) then return true end - api.nvim_buf_clear_namespace(query_buf, preview_ns, 0, -1) + api.nvim_buf_clear_namespace(query_buf, edit_ns, 0, -1) end, }) api.nvim_create_autocmd('BufHidden', { group = group, buffer = buf, - desc = 'Close the previewer window when the source buffer is hidden', + desc = 'Close the editor window when the source buffer is hidden', once = true, callback = function() close_win(query_win) diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index 350ccba7e4..d7973cc48f 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -835,4 +835,13 @@ function M.omnifunc(findstart, base) return require('vim.treesitter._query_linter').omnifunc(findstart, base) end +--- Open a window for live editing of a treesitter query. +--- +--- Can also be shown with `:EditQuery`. *:EditQuery* +--- +--- Note that the editor opens a scratch buffer, and so queries aren't persisted on disk. +function M.edit() + require('vim.treesitter.dev').edit_query() +end + return M -- cgit From 6debb1852355e0112ce75a5b4aed714ba1469ddb Mon Sep 17 00:00:00 2001 From: Maria José Solano Date: Fri, 15 Sep 2023 12:45:40 -0700 Subject: refactor(treesitter): remove duplicated diagnostic code (#24976) * refactor(treesitter): remove duplicated diagnostic code * fixup!: fix type errors * fixup!: add type namespace --- runtime/lua/vim/treesitter/_query_linter.lua | 177 ++++++++------------------- 1 file changed, 48 insertions(+), 129 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 3dd0177a81..abf0bf345d 100644 --- a/runtime/lua/vim/treesitter/_query_linter.lua +++ b/runtime/lua/vim/treesitter/_query_linter.lua @@ -1,8 +1,6 @@ local api = vim.api local namespace = api.nvim_create_namespace('vim.treesitter.query_linter') --- those node names exist for every language -local BUILT_IN_NODE_NAMES = { '_', 'ERROR' } local M = {} @@ -10,11 +8,13 @@ local M = {} --- @field langs string[] --- @field clear boolean +--- @alias vim.treesitter.ParseError {msg: string, range: Range4} + --- @private --- Caches parse results for queries for each language. --- Entries of parse_cache[lang][query_text] will either be true for successful parse or contain the ---- error message of the parse ---- @type table> +--- message and range of the parse error. +--- @type table> local parse_cache = {} --- Contains language dependent context for the query linter @@ -26,20 +26,16 @@ local parse_cache = {} --- @private --- Adds a diagnostic for node in the query buffer --- @param diagnostics Diagnostic[] ---- @param node TSNode ---- @param buf integer +--- @param range Range4 --- @param lint string --- @param lang string? -local function add_lint_for_node(diagnostics, node, buf, lint, lang) - local node_text = vim.treesitter.get_node_text(node, buf):gsub('\n', ' ') - --- @type string - local message = lint .. ': ' .. node_text - local error_range = { node:range() } +local function add_lint_for_node(diagnostics, range, lint, lang) + local message = lint:gsub('\n', ' ') diagnostics[#diagnostics + 1] = { - lnum = error_range[1], - end_lnum = error_range[3], - col = error_range[2], - end_col = error_range[4], + lnum = range[1], + end_lnum = range[3], + col = range[2], + end_col = range[4], severity = vim.diagnostic.ERROR, message = message, source = lang, @@ -91,6 +87,31 @@ local lint_query = [[;; query (ERROR) @error ]] +--- @private +--- @param err string +--- @param node TSNode +--- @return vim.treesitter.ParseError +local function get_error_entry(err, node) + local start_line, start_col = node:range() + local line_offset, col_offset, msg = err:gmatch('.-:%d+: Query error at (%d+):(%d+)%. ([^:]+)')() ---@type string, string, string + start_line, start_col = + start_line + tonumber(line_offset) - 1, start_col + tonumber(col_offset) - 1 + local end_line, end_col = start_line, start_col + if msg:match('^Invalid syntax') or msg:match('^Impossible') then + -- Use the length of the underlined node + local underlined = vim.split(err, '\n')[2] + end_col = end_col + #underlined + elseif msg:match('^Invalid') then + -- Use the length of the problematic type/capture/field + end_col = end_col + #msg:match('"([^"]+)"') + end + + return { + msg = msg, + range = { start_line, start_col, end_line, end_col }, + } +end + --- @private --- @param node TSNode --- @param buf integer @@ -106,104 +127,19 @@ local function check_toplevel(node, buf, lang, diagnostics) local lang_cache = parse_cache[lang] if lang_cache[query_text] == nil then - local ok, err = pcall(vim.treesitter.query.parse, lang, query_text) + local cache_val, err = pcall(vim.treesitter.query.parse, lang, query_text) ---@type boolean|vim.treesitter.ParseError, string|Query - if not ok and type(err) == 'string' then - err = err:match('.-:%d+: (.+)') + if not cache_val and type(err) == 'string' then + cache_val = get_error_entry(err, node) end - lang_cache[query_text] = ok or err + lang_cache[query_text] = cache_val end local cache_entry = lang_cache[query_text] - if type(cache_entry) == 'string' then - add_lint_for_node(diagnostics, node, buf, cache_entry, lang) - end -end - ---- @private ---- @param node TSNode ---- @param buf integer ---- @param lang string ---- @param parser_info table ---- @param diagnostics Diagnostic[] -local function check_field(node, buf, lang, parser_info, diagnostics) - local field_name = vim.treesitter.get_node_text(node, buf) - if not vim.tbl_contains(parser_info.fields, field_name) then - add_lint_for_node(diagnostics, node, buf, 'Invalid field', lang) - end -end - ---- @private ---- @param node TSNode ---- @param buf integer ---- @param lang string ---- @param parser_info (table) ---- @param diagnostics Diagnostic[] -local function check_node(node, buf, lang, parser_info, diagnostics) - local node_type = vim.treesitter.get_node_text(node, buf) - local is_named = node_type:sub(1, 1) ~= '"' - - if not is_named then - node_type = node_type:gsub('"(.*)".*$', '%1'):gsub('\\(.)', '%1') - end - - local found = vim.tbl_contains(BUILT_IN_NODE_NAMES, node_type) - or vim.tbl_contains(parser_info.symbols, function(s) - return vim.deep_equal(s, { node_type, is_named }) - end, { predicate = true }) - - if not found then - add_lint_for_node(diagnostics, node, buf, 'Invalid node type', lang) - end -end - ---- @private ---- @param node TSNode ---- @param buf integer ---- @param is_predicate boolean ---- @return string -local function get_predicate_name(node, buf, is_predicate) - local name = vim.treesitter.get_node_text(node, buf) - if is_predicate then - if vim.startswith(name, 'not-') then - --- @type string - name = name:sub(string.len('not-') + 1) - end - return name .. '?' - end - return name .. '!' -end - ---- @private ---- @param predicate_node TSNode ---- @param predicate_type_node TSNode ---- @param buf integer ---- @param lang string? ---- @param diagnostics Diagnostic[] -local function check_predicate(predicate_node, predicate_type_node, buf, lang, diagnostics) - local type_string = vim.treesitter.get_node_text(predicate_type_node, buf) - - -- Quirk of the query grammar that directives are also predicates! - if type_string == '?' then - if - not vim.tbl_contains( - vim.treesitter.query.list_predicates(), - get_predicate_name(predicate_node, buf, true) - ) - then - add_lint_for_node(diagnostics, predicate_node, buf, 'Unknown predicate', lang) - end - elseif type_string == '!' then - if - not vim.tbl_contains( - vim.treesitter.query.list_directives(), - get_predicate_name(predicate_node, buf, false) - ) - then - add_lint_for_node(diagnostics, predicate_node, buf, 'Unknown directive', lang) - end + if type(cache_entry) ~= 'boolean' then + add_lint_for_node(diagnostics, cache_entry.range, cache_entry.msg, lang) end end @@ -214,8 +150,6 @@ end --- @param lang_context QueryLinterLanguageContext --- @param diagnostics Diagnostic[] local function lint_match(buf, match, query, lang_context, diagnostics) - local predicate --- @type TSNode - local predicate_type --- @type TSNode local lang = lang_context.lang local parser_info = lang_context.parser_info @@ -223,31 +157,16 @@ local function lint_match(buf, match, query, lang_context, diagnostics) local cap_id = query.captures[id] -- perform language-independent checks only for first lang - if lang_context.is_first_lang then - if cap_id == 'error' then - add_lint_for_node(diagnostics, node, buf, 'Syntax error') - elseif cap_id == 'predicate.name' then - predicate = node - elseif cap_id == 'predicate.type' then - predicate_type = node - end + 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) end -- other checks rely on Neovim parser introspection - if lang and parser_info then - if cap_id == 'toplevel' then - check_toplevel(node, buf, lang, diagnostics) - elseif cap_id == 'field' then - check_field(node, buf, lang, parser_info, diagnostics) - elseif cap_id == 'node.named' or cap_id == 'node.anonymous' then - check_node(node, buf, lang, parser_info, diagnostics) - end + if lang and parser_info and cap_id == 'toplevel' then + check_toplevel(node, buf, lang, diagnostics) end end - - if predicate and predicate_type then - check_predicate(predicate, predicate_type, buf, lang, diagnostics) - end end --- @private @@ -339,7 +258,7 @@ function M.omnifunc(findstart, base) end end for _, s in pairs(parser_info.symbols) do - local text = s[2] and s[1] or '"' .. s[1]:gsub([[\]], [[\\]]) .. '"' + local text = s[2] and s[1] or '"' .. s[1]:gsub([[\]], [[\\]]) .. '"' ---@type string if text:find(base, 1, true) then table.insert(items, text) end -- cgit From 908843df61fc80f160392edc0af0d1672a8f9a68 Mon Sep 17 00:00:00 2001 From: L Lllvvuu Date: Wed, 13 Sep 2023 16:51:54 -0700 Subject: fix(languagetree): apply `resolve_lang` to `metadata['injection.language']` `resolve_lang` is applied to `@injection.language` when it's supplied as a capture: https://github.com/neovim/neovim/blob/f5953edbac14febce9d4f8a3c35bdec1eae26fbe/runtime/lua/vim/treesitter/languagetree.lua#L766-L768 If we want to support `metadata['injection.language']` (as per #22518 and [tree-sitter upstream](https://tree-sitter.github.io/tree-sitter/syntax-highlighting#language-injection)) then the behavior should be consistent. Fixes: nvim-treesitter/nvim-treesitter#4918 --- runtime/lua/vim/treesitter/languagetree.lua | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index 79f36a27fd..4dd5a18396 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -734,7 +734,8 @@ local has_parser = function(lang) or #vim.api.nvim_get_runtime_file('parser/' .. lang .. '.*', false) > 0 end ---- Return parser name for language (if exists) or filetype (if registered and exists) +--- Return parser name for language (if exists) or filetype (if registered and exists). +--- Also attempts with the input lower-cased. --- ---@param alias string language or filetype name ---@return string? # resolved parser name @@ -743,10 +744,19 @@ local function resolve_lang(alias) return alias end + if has_parser(alias:lower()) then + return alias:lower() + end + local lang = vim.treesitter.language.get_lang(alias) if lang and has_parser(lang) then return lang end + + lang = vim.treesitter.language.get_lang(alias:lower()) + if lang and has_parser(lang) then + return lang + end end ---@private @@ -758,9 +768,10 @@ end function LanguageTree:_get_injection(match, metadata) local ranges = {} ---@type Range6[] local combined = metadata['injection.combined'] ~= nil + local injection_lang = metadata['injection.language'] --[[@as string?]] local lang = metadata['injection.self'] ~= nil and self:lang() or metadata['injection.parent'] ~= nil and self._parent_lang - or metadata['injection.language'] --[[@as string?]] + or (injection_lang and resolve_lang(injection_lang)) local include_children = metadata['injection.include-children'] ~= nil for id, node in pairs(match) do @@ -768,7 +779,7 @@ function LanguageTree:_get_injection(match, metadata) -- Lang should override any other language tag if name == 'injection.language' then local text = vim.treesitter.get_node_text(node, self._source, { metadata = metadata[id] }) - lang = resolve_lang(text) or resolve_lang(text:lower()) + lang = resolve_lang(text) elseif name == 'injection.content' then ranges = get_node_ranges(node, self._source, metadata[id], include_children) end -- cgit From 07080f67fe7e526576d5d50777fb122a99b3e183 Mon Sep 17 00:00:00 2001 From: L Lllvvuu Date: Sat, 16 Sep 2023 02:48:49 -0700 Subject: perf(treesitter): do not scan past given line for predicate match Problem --- If a highlighter query returns a significant number of predicate non-matches, the highlighter will scan well past the end of the window. Solution --- In the iterator returned from `iter_captures`, accept an optional parameter `end_line`. If no parameter provided, the behavior is unchanged, hence this is a non-invasive tweak. Fixes: #25113 nvim-treesitter/nvim-treesitter#5057 --- runtime/lua/vim/treesitter/highlighter.lua | 63 ++++++++++++++++-------------- runtime/lua/vim/treesitter/query.lua | 10 +++-- 2 files changed, 40 insertions(+), 33 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/highlighter.lua b/runtime/lua/vim/treesitter/highlighter.lua index 8d4d6a9337..496193c6ed 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') ----@alias TSHlIter fun(): integer, TSNode, TSMetadata +---@alias TSHlIter fun(end_line: integer|nil): integer, TSNode, TSMetadata ---@class TSHighlightState ---@field next_row integer @@ -241,40 +241,43 @@ local function on_line_impl(self, buf, line, is_spell_nav) end while line >= state.next_row do - local capture, node, metadata = state.iter() + local capture, node, metadata = state.iter(line) - if capture == nil then - break + local range = { root_end_row + 1, 0, root_end_row + 1, 0 } + if node then + range = vim.treesitter.get_range(node, buf, metadata and metadata[capture]) end - - local range = vim.treesitter.get_range(node, buf, metadata[capture]) local start_row, start_col, end_row, end_col = Range.unpack4(range) - local hl = highlighter_query.hl_cache[capture] - - local capture_name = highlighter_query:query().captures[capture] - local spell = nil ---@type boolean? - if capture_name == 'spell' then - spell = true - elseif capture_name == 'nospell' then - spell = false - end - -- Give nospell a higher priority so it always overrides spell captures. - local spell_pri_offset = capture_name == 'nospell' and 1 or 0 - - if hl and end_row >= line and (not is_spell_nav or spell ~= nil) then - local priority = (tonumber(metadata.priority) or vim.highlight.priorities.treesitter) - + spell_pri_offset - api.nvim_buf_set_extmark(buf, ns, start_row, start_col, { - end_line = end_row, - end_col = end_col, - hl_group = hl, - ephemeral = true, - priority = priority, - conceal = metadata.conceal, - spell = spell, - }) + if capture then + local hl = highlighter_query.hl_cache[capture] + + local capture_name = highlighter_query:query().captures[capture] + local spell = nil ---@type boolean? + if capture_name == 'spell' then + spell = true + elseif capture_name == 'nospell' then + spell = false + end + + -- Give nospell a higher priority so it always overrides spell captures. + local spell_pri_offset = capture_name == 'nospell' and 1 or 0 + + if hl and end_row >= line and (not is_spell_nav or spell ~= nil) then + local priority = (tonumber(metadata.priority) or vim.highlight.priorities.treesitter) + + spell_pri_offset + api.nvim_buf_set_extmark(buf, ns, start_row, start_col, { + end_line = end_row, + end_col = end_col, + hl_group = hl, + ephemeral = true, + priority = priority, + conceal = metadata.conceal, + spell = spell, + }) + end end + if start_row > line then state.next_row = start_row end diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index d7973cc48f..6d9b214d4a 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -708,7 +708,8 @@ end ---@param start integer Starting line for the search ---@param stop integer Stopping line for the search (end-exclusive) --- ----@return (fun(): integer, TSNode, TSMetadata): capture id, capture node, metadata +---@return (fun(end_line: integer|nil): integer, TSNode, TSMetadata): +--- capture id, capture node, metadata function Query:iter_captures(node, source, start, stop) if type(source) == 'number' and source == 0 then source = api.nvim_get_current_buf() @@ -717,7 +718,7 @@ function Query:iter_captures(node, source, start, stop) start, stop = value_or_node_range(start, stop, node) local raw_iter = node:_rawquery(self.query, true, start, stop) - local function iter() + local function iter(end_line) local capture, captured_node, match = raw_iter() local metadata = {} @@ -725,7 +726,10 @@ function Query:iter_captures(node, source, start, stop) local active = self:match_preds(match, match.pattern, source) match.active = active if not active then - return iter() -- tail call: try next match + if end_line and captured_node:range() > end_line then + return nil, captured_node, nil + end + return iter(end_line) -- tail call: try next match end self:apply_directives(match, match.pattern, source, metadata) -- cgit From f40a109716d7f748dd9e9f70b57e4d0bb285518b Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Wed, 13 Sep 2023 10:39:34 +0100 Subject: fix(treesitter): fix trim predicate --- runtime/lua/vim/treesitter/query.lua | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index 6d9b214d4a..e83ad00eeb 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -514,7 +514,10 @@ local directive_handlers = { -- Example: (#trim! @fold) -- TODO(clason): generalize to arbitrary whitespace removal ['trim!'] = function(match, _, bufnr, pred, metadata) - local node = match[pred[2]] + local capture_id = pred[2] + assert(type(capture_id) == 'number') + + local node = match[capture_id] if not node then return end @@ -526,9 +529,9 @@ local directive_handlers = { return end - while true do + while end_row >= start_row do -- As we only care when end_col == 0, always inspect one line above end_row. - local end_line = vim.api.nvim_buf_get_lines(bufnr, end_row - 1, end_row, true)[1] + local end_line = api.nvim_buf_get_lines(bufnr, end_row - 1, end_row, true)[1] if end_line ~= '' then break @@ -539,7 +542,8 @@ local directive_handlers = { -- If this produces an invalid range, we just skip it. if start_row < end_row or (start_row == end_row and start_col <= end_col) then - metadata.range = { start_row, start_col, end_row, end_col } + metadata[capture_id] = metadata[capture_id] or {} + metadata[capture_id].range = { start_row, start_col, end_row, end_col } end end, } -- cgit From 7e5ce42977d7b2f223ca57e0237db778b8e424ea Mon Sep 17 00:00:00 2001 From: Jaehwang Jung Date: Sat, 16 Sep 2023 23:43:22 +0900 Subject: fix(treesitter): properly combine injection.combined regions Problem: It doesn't make much sense to flatten each region (= list of ranges). This coincidentally worked for region with a single range. Solution: Custom function for combining regions. --- runtime/lua/vim/treesitter/languagetree.lua | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index 4dd5a18396..b555ee231b 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -788,6 +788,19 @@ function LanguageTree:_get_injection(match, metadata) return lang, combined, ranges end +--- Can't use vim.tbl_flatten since a range is just a table. +---@param regions Range6[][] +---@return Range6[] +local function combine_regions(regions) + local result = {} ---@type Range6[] + for _, region in ipairs(regions) do + for _, range in ipairs(region) do + result[#result + 1] = range + end + end + return result +end + --- Gets language injection points by language. --- --- This is where most of the injection processing occurs. @@ -833,11 +846,7 @@ function LanguageTree:_get_injections() for _, entry in pairs(patterns) do if entry.combined then - ---@diagnostic disable-next-line:no-unknown - local regions = vim.tbl_map(function(e) - return vim.tbl_flatten(e) - end, entry.regions) - table.insert(result[lang], regions) + table.insert(result[lang], combine_regions(entry.regions)) else for _, ranges in pairs(entry.regions) do table.insert(result[lang], ranges) -- cgit From 28f54a78782318cb9c356a372b9e52a3a6b1f8dd Mon Sep 17 00:00:00 2001 From: Maria José Solano Date: Sat, 16 Sep 2023 10:05:59 -0700 Subject: feat(treesitter): add lang parameter to the query editor (#25181) --- runtime/lua/vim/treesitter/dev.lua | 15 ++++++++------- runtime/lua/vim/treesitter/query.lua | 6 ++++-- 2 files changed, 12 insertions(+), 9 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/dev.lua b/runtime/lua/vim/treesitter/dev.lua index bc54853103..7f24ba8590 100644 --- a/runtime/lua/vim/treesitter/dev.lua +++ b/runtime/lua/vim/treesitter/dev.lua @@ -468,11 +468,11 @@ local edit_ns = api.nvim_create_namespace('treesitter/dev-edit') ---@param query_win integer ---@param base_win integer -local function update_editor_highlights(query_win, base_win) +---@param lang string +local function update_editor_highlights(query_win, base_win, lang) local base_buf = api.nvim_win_get_buf(base_win) local query_buf = api.nvim_win_get_buf(query_win) - local parser = vim.treesitter.get_parser(base_buf) - local lang = parser:lang() + local parser = vim.treesitter.get_parser(base_buf, lang) api.nvim_buf_clear_namespace(base_buf, edit_ns, 0, -1) local query_content = table.concat(api.nvim_buf_get_lines(query_buf, 0, -1, false), '\n') @@ -506,7 +506,8 @@ local function update_editor_highlights(query_win, base_win) end --- @private -function M.edit_query() +--- @param lang? string language to open the query editor for. +function M.edit_query(lang) local buf = api.nvim_get_current_buf() local win = api.nvim_get_current_win() @@ -528,11 +529,11 @@ function M.edit_query() end vim.cmd(cmd) - local ok, parser = pcall(vim.treesitter.get_parser, buf) + local ok, parser = pcall(vim.treesitter.get_parser, buf, lang) if not ok then return nil, 'No parser available for the given buffer' end - local lang = parser:lang() + lang = parser:lang() local query_win = api.nvim_get_current_win() local query_buf = api.nvim_win_get_buf(query_win) @@ -561,7 +562,7 @@ function M.edit_query() desc = 'Update query editor highlights when the cursor moves', callback = function() if api.nvim_win_is_valid(win) then - update_editor_highlights(query_win, win) + update_editor_highlights(query_win, win, lang) end end, }) diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index e83ad00eeb..44ed37d64e 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -848,8 +848,10 @@ end --- Can also be shown with `:EditQuery`. *:EditQuery* --- --- Note that the editor opens a scratch buffer, and so queries aren't persisted on disk. -function M.edit() - require('vim.treesitter.dev').edit_query() +--- +--- @param lang? string language to open the query editor for. If omitted, inferred from the current buffer's filetype. +function M.edit(lang) + require('vim.treesitter.dev').edit_query(lang) end return M -- cgit From 71d9b7d15c9b2e0df7db69a172aea0723a40bb71 Mon Sep 17 00:00:00 2001 From: Jaehwang Jung Date: Sun, 17 Sep 2023 17:54:30 +0900 Subject: fix(treesitter): _trees may not be list-like Problem: With incremental injection parsing, injected languages' parsers parse only the relevant regions and stores the result in _trees with the index of the corresponding region. Therefore, there can be holes in _trees. Solution: * Use generic table functions where appropriate. * Fix type annotations and docs. --- runtime/lua/vim/treesitter/_meta.lua | 3 +- runtime/lua/vim/treesitter/languagetree.lua | 57 +++++++++++++++++------------ 2 files changed, 36 insertions(+), 24 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/_meta.lua b/runtime/lua/vim/treesitter/_meta.lua index 9a94f12c16..d01b7be3b0 100644 --- a/runtime/lua/vim/treesitter/_meta.lua +++ b/runtime/lua/vim/treesitter/_meta.lua @@ -50,7 +50,8 @@ function TSNode:_rawquery(query, captures, start, end_, opts) end ---@alias TSLoggerCallback fun(logtype: 'parse'|'lex', msg: string) ---@class TSParser ----@field parse fun(self: TSParser, tree: TSTree?, source: integer|string, include_bytes: boolean?): TSTree, integer[] +---@field parse fun(self: TSParser, tree: TSTree?, source: integer|string, include_bytes: true): TSTree, Range6[] +---@field parse fun(self: TSParser, tree: TSTree?, source: integer|string, include_bytes: false|nil): TSTree, Range4[] ---@field reset fun(self: TSParser) ---@field included_ranges fun(self: TSParser, include_bytes: boolean?): integer[] ---@field set_included_ranges fun(self: TSParser, ranges: (Range6|TSNode)[]) diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index b555ee231b..f931291ed7 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -78,13 +78,14 @@ local TSCallbackNames = { ---@field private _opts table Options ---@field private _parser TSParser Parser for language ---@field private _has_regions boolean ----@field private _regions Range6[][]? +---@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() ---@field private _lang string Language name ---@field private _parent_lang? string Parent language name ---@field private _source (integer|string) Buffer or string to parse ----@field private _trees TSTree[] Reference to parsed tree (one for each language) +---@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 _logger? fun(logtype: string, msg: string) ---@field private _logfile? file* @@ -211,7 +212,7 @@ function LanguageTree:_log(...) end local info = debug.getinfo(2, 'nl') - local nregions = #self:included_regions() + local nregions = vim.tbl_count(self:included_regions()) local prefix = string.format('%s:%d: (#regions=%d) ', info.name or '???', info.currentline or 0, nregions) @@ -244,8 +245,13 @@ function LanguageTree:invalidate(reload) end end ---- Returns all trees this language tree contains. +--- Returns all trees of the regions parsed by this parser. --- Does not include child languages. +--- The result is list-like if +--- * this LanguageTree is the root, in which case the result is empty or a singleton list; or +--- * the root LanguageTree is fully parsed. +--- +---@return table function LanguageTree:trees() return self._trees end @@ -255,16 +261,15 @@ function LanguageTree:lang() return self._lang end ---- Determines whether this tree is valid. ---- If the tree is invalid, call `parse()`. ---- This will return the updated tree. ----@param exclude_children boolean|nil +--- 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`) ---@return boolean function LanguageTree:is_valid(exclude_children) local valid = self._valid if type(valid) == 'table' then - for i = 1, #self:included_regions() do + for i, _ in pairs(self:included_regions()) do if not valid[i] then return false end @@ -328,7 +333,7 @@ end --- @private --- @param range boolean|Range? ---- @return integer[] changes +--- @return Range6[] changes --- @return integer no_regions_parsed --- @return number total_parse_time function LanguageTree:_parse_regions(range) @@ -370,7 +375,7 @@ function LanguageTree:_add_injections() local seen_langs = {} ---@type table local query_time, injections_by_lang = tcall(self._get_injections, self) - for lang, injection_ranges in pairs(injections_by_lang) do + for lang, injection_regions in pairs(injections_by_lang) do local has_lang = pcall(language.add, lang) -- Child language trees should just be ignored if not found, since @@ -383,7 +388,7 @@ function LanguageTree:_add_injections() child = self:add_child(lang) end - child:set_included_regions(injection_ranges) + child:set_included_regions(injection_regions) seen_langs[lang] = true end end @@ -408,14 +413,14 @@ 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 TSTree[] +--- @return table function LanguageTree:parse(range) if self:is_valid() then self:_log('valid') return self._trees end - local changes --- @type Range6? + local changes --- @type Range6[]? -- Collect some stats local no_regions_parsed = 0 @@ -565,7 +570,7 @@ function LanguageTree:_iter_regions(fn) local all_valid = true - for i, region in ipairs(self:included_regions()) do + 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 @@ -601,7 +606,7 @@ end --- nodes, which is useful for templating languages like ERB and EJS. --- ---@private ----@param new_regions Range6[][] List of regions this tree should manage and parse. +---@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 @@ -609,16 +614,20 @@ function LanguageTree:set_included_regions(new_regions) for _, region in ipairs(new_regions) do for i, range in ipairs(region) do if type(range) == 'table' and #range == 4 then - region[i] = Range.add_bytes(self._source, range) + region[i] = Range.add_bytes(self._source, range --[[@as Range4]]) elseif type(range) == 'userdata' then region[i] = { range:range(true) } end end end + -- included_regions is not guaranteed to be list-like, but this is still sound, i.e. if + -- new_regions is different from included_regions, then outdated regions in included_regions are + -- invalidated. For example, if included_regions = new_regions ++ hole ++ outdated_regions, then + -- outdated_regions is invalidated by _iter_regions in else branch. if #self:included_regions() ~= #new_regions then -- TODO(lewis6991): inefficient; invalidate trees incrementally - for _, t in ipairs(self._trees) do + for _, t in pairs(self._trees) do self:_do_callback('changedtree', t:included_ranges(true), t) end self._trees = {} @@ -632,20 +641,22 @@ function LanguageTree:set_included_regions(new_regions) self._regions = new_regions end ----Gets the set of included regions ----@return Range6[][] +---Gets the set of included regions managed by this LanguageTree. This can be different from the +---regions set by injection query, because a partial |LanguageTree:parse()| drops the regions +---outside the requested range. +---@return table function LanguageTree:included_regions() if self._regions then return self._regions end - if not self._has_regions or #self._trees == 0 then + if not self._has_regions or next(self._trees) == nil then -- treesitter.c will default empty ranges to { -1, -1, -1, -1, -1, -1} return { {} } end local regions = {} ---@type Range6[][] - for i, _ in ipairs(self._trees) do + for i, _ in pairs(self._trees) do regions[i] = self._trees[i]:included_ranges(true) end @@ -801,7 +812,7 @@ local function combine_regions(regions) return result end ---- Gets language injection points by language. +--- Gets language injection regions by language. --- --- This is where most of the injection processing occurs. --- -- cgit From 1b55f51d0d8468ca357514a868ac8e188b0c8722 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Wed, 20 Sep 2023 04:15:23 -0700 Subject: docs: misc #24561 fix #24699 fix #25253 --- runtime/lua/vim/treesitter/dev.lua | 5 +++-- runtime/lua/vim/treesitter/query.lua | 8 +++++--- 2 files changed, 8 insertions(+), 5 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/dev.lua b/runtime/lua/vim/treesitter/dev.lua index 7f24ba8590..61d84017d4 100644 --- a/runtime/lua/vim/treesitter/dev.lua +++ b/runtime/lua/vim/treesitter/dev.lua @@ -597,8 +597,9 @@ function M.edit_query(lang) }) api.nvim_buf_set_lines(query_buf, 0, -1, false, { - ';; Write your query here. Use @captures to highlight matches in the source buffer.', - ';; Completion for grammar nodes is available (see :h compl-omni)', + ';; Write queries here (see $VIMRUNTIME/queries/ for examples).', + ';; Move cursor to a capture ("@foo") to highlight matches in the source buffer.', + ';; Completion for grammar nodes is available (:help compl-omni)', '', '', }) diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index 44ed37d64e..313d837d5c 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -843,11 +843,13 @@ function M.omnifunc(findstart, base) return require('vim.treesitter._query_linter').omnifunc(findstart, base) end ---- Open a window for live editing of a treesitter query. +--- Opens a live editor to query the buffer you started from. --- ---- Can also be shown with `:EditQuery`. *:EditQuery* +--- Can also be shown with *:EditQuery*. --- ---- Note that the editor opens a scratch buffer, and so queries aren't persisted on disk. +--- If you move the cursor to a capture name ("@foo"), text matching the capture is highlighted in +--- the source buffer. The query editor is a scratch buffer, use `:write` to save it. You can find +--- example queries at `$VIMRUNTIME/queries/`. --- --- @param lang? string language to open the query editor for. If omitted, inferred from the current buffer's filetype. function M.edit(lang) -- cgit From 877d04d0fb83b5fc602dbab22b58f26a793ec236 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Sat, 16 Sep 2023 23:10:30 +0100 Subject: feat(lua): add vim.func._memoize Memoizes a function, using a custom function to hash the arguments. Private for now until: - There are other places in the codebase that could benefit from this (e.g. LSP), but might require other changes to accommodate. - Invalidation of the cache needs to be controllable. Using weak tables is an acceptable invalidation policy, but it shouldn't be the only one. - I don't think the story around `hash_fn` is completely thought out. We may be able to have a good default hash_fn by hashing each argument, so basically a better 'concat'. --- runtime/lua/vim/treesitter/_query_linter.lua | 54 +++++++++------------------- runtime/lua/vim/treesitter/languagetree.lua | 10 +++--- runtime/lua/vim/treesitter/query.lua | 34 +++--------------- 3 files changed, 27 insertions(+), 71 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 abf0bf345d..87d74789a3 100644 --- a/runtime/lua/vim/treesitter/_query_linter.lua +++ b/runtime/lua/vim/treesitter/_query_linter.lua @@ -10,20 +10,12 @@ local M = {} --- @alias vim.treesitter.ParseError {msg: string, range: Range4} ---- @private ---- Caches parse results for queries for each language. ---- Entries of parse_cache[lang][query_text] will either be true for successful parse or contain the ---- message and range of the parse error. ---- @type table> -local parse_cache = {} - --- Contains language dependent context for the query linter --- @class QueryLinterLanguageContext --- @field lang string? Current `lang` of the targeted parser --- @field parser_info table? Parser info returned by vim.treesitter.language.inspect --- @field is_first_lang boolean Whether this is the first language of a linter run checking queries for multiple `langs` ---- @private --- Adds a diagnostic for node in the query buffer --- @param diagnostics Diagnostic[] --- @param range Range4 @@ -42,7 +34,6 @@ local function add_lint_for_node(diagnostics, range, lint, lang) } end ---- @private --- Determines the target language of a query file by its path: /.scm --- @param buf integer --- @return string? @@ -53,7 +44,6 @@ local function guess_query_lang(buf) end end ---- @private --- @param buf integer --- @param opts QueryLinterOpts|QueryLinterNormalizedOpts|nil --- @return QueryLinterNormalizedOpts @@ -87,7 +77,6 @@ local lint_query = [[;; query (ERROR) @error ]] ---- @private --- @param err string --- @param node TSNode --- @return vim.treesitter.ParseError @@ -112,38 +101,26 @@ local function get_error_entry(err, node) } end ---- @private --- @param node TSNode --- @param buf integer --- @param lang string ---- @param diagnostics Diagnostic[] -local function check_toplevel(node, buf, lang, diagnostics) - local query_text = vim.treesitter.get_node_text(node, buf) - - if not parse_cache[lang] then - parse_cache[lang] = {} - end - - local lang_cache = parse_cache[lang] - - if lang_cache[query_text] == nil then - local cache_val, err = pcall(vim.treesitter.query.parse, lang, query_text) ---@type boolean|vim.treesitter.ParseError, string|Query - - if not cache_val and type(err) == 'string' then - cache_val = get_error_entry(err, node) - end - - lang_cache[query_text] = cache_val - end +local function hash_parse(node, buf, lang) + return tostring(node:id()) .. tostring(buf) .. tostring(vim.b[buf].changedtick) .. lang +end - local cache_entry = lang_cache[query_text] +--- @param node TSNode +--- @param buf integer +--- @param lang string +--- @return vim.treesitter.ParseError? +local parse = vim.func._memoize(hash_parse, function(node, buf, lang) + local query_text = vim.treesitter.get_node_text(node, buf) + local ok, err = pcall(vim.treesitter.query.parse, lang, query_text) ---@type boolean|vim.treesitter.ParseError, string|Query - if type(cache_entry) ~= 'boolean' then - add_lint_for_node(diagnostics, cache_entry.range, cache_entry.msg, lang) + if not ok and type(err) == 'string' then + return get_error_entry(err, node) end -end +end) ---- @private --- @param buf integer --- @param match table --- @param query Query @@ -164,7 +141,10 @@ local function lint_match(buf, match, query, lang_context, diagnostics) -- other checks rely on Neovim parser introspection if lang and parser_info and cap_id == 'toplevel' then - check_toplevel(node, buf, lang, diagnostics) + local err = parse(node, buf, lang) + if err then + add_lint_for_node(diagnostics, err.range, err.msg, lang) + end end end end diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index f931291ed7..b2c4e9167d 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -738,12 +738,14 @@ local function add_injection(t, tree_index, pattern, lang, combined, ranges) end -- TODO(clason): replace by refactored `ts.has_parser` API (without registering) ----@param lang string parser name ----@return boolean # true if parser for {lang} exists on rtp -local has_parser = function(lang) +--- The result of this function is cached to prevent nvim_get_runtime_file from being +--- called too often +--- @param lang string parser name +--- @return boolean # true if parser for {lang} exists on rtp +local has_parser = vim.func._memoize(1, function(lang) return vim._ts_has_language(lang) or #vim.api.nvim_get_runtime_file('parser/' .. lang .. '.*', false) > 0 -end +end) --- Return parser name for language (if exists) or filetype (if registered and exists). --- Also attempts with the input lower-cased. diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index 313d837d5c..8cbbffcd60 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -191,12 +191,6 @@ function M.set(lang, query_name, text) explicit_queries[lang][query_name] = M.parse(lang, text) end ---- `false` if query files didn't exist or were empty ----@type table> -local query_get_cache = vim.defaulttable(function() - return setmetatable({}, { __mode = 'v' }) -end) - ---@deprecated function M.get_query(...) vim.deprecate('vim.treesitter.query.get_query()', 'vim.treesitter.query.get()', '0.10') @@ -209,34 +203,19 @@ end ---@param query_name string Name of the query (e.g. "highlights") --- ---@return Query|nil Parsed query -function M.get(lang, query_name) +M.get = vim.func._memoize('concat-2', function(lang, query_name) if explicit_queries[lang][query_name] then return explicit_queries[lang][query_name] end - local cached = query_get_cache[lang][query_name] - if cached then - return cached - elseif cached == false then - return nil - end - local query_files = M.get_files(lang, query_name) local query_string = read_query_files(query_files) if #query_string == 0 then - query_get_cache[lang][query_name] = false return nil end - local query = M.parse(lang, query_string) - query_get_cache[lang][query_name] = query - return query -end - ----@type table> -local query_parse_cache = vim.defaulttable(function() - return setmetatable({}, { __mode = 'v' }) + return M.parse(lang, query_string) end) ---@deprecated @@ -262,20 +241,15 @@ end ---@param query string Query in s-expr syntax --- ---@return Query Parsed query -function M.parse(lang, query) +M.parse = vim.func._memoize('concat-2', function(lang, query) language.add(lang) - local cached = query_parse_cache[lang][query] - if cached then - return cached - end local self = setmetatable({}, Query) self.query = vim._ts_parse_query(lang, query) self.info = self.query:inspect() self.captures = self.info.captures - query_parse_cache[lang][query] = self return self -end +end) ---@deprecated function M.get_range(...) -- cgit From e353c869cea4541d00d627ec82724d3f247225a3 Mon Sep 17 00:00:00 2001 From: L Lllvvuu Date: Tue, 19 Sep 2023 21:41:07 -0700 Subject: fix(languagetree): don't treat unparsed nodes as occupying full range This is incorrect in the following scenario: 1. The language tree is Lua > Vim > Lua. 2. An edit simultaneously wipes out the `_regions` of all nodes, while taking the Vim injection off-screen. 3. The Vim injection is not re-parsed, so the child Lua `_regions` is still `nil`. 4. The child Lua is assumed, incorrectly, to occupy the whole document. 5. This causes the injections to be parsed again, resulting in Lua > Vim > Lua > Vim. 6. Now, by the same process, Vim ends up with its range assumed over the whole document. Now the parse is broken and results in broken highlighting and poor performance. It should be fine to instead treat an unparsed node as occupying nothing (i.e. effectively non-existent). Since, either: - The parent was just parsed, hence defining `_regions` - The parent was not just parsed, in which case this node doesn't need to be parsed either. Also, the name `has_regions` is confusing; it seems to simply mean the opposite of "root" or "full_document". However, this PR does not touch it. --- 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 b2c4e9167d..670f2797b7 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -650,8 +650,8 @@ function LanguageTree:included_regions() return self._regions end - if not self._has_regions or next(self._trees) == nil then - -- treesitter.c will default empty ranges to { -1, -1, -1, -1, -1, -1} + if not self._has_regions then + -- treesitter.c will default empty ranges to { -1, -1, -1, -1, -1, -1} (the full range) return { {} } end -- cgit From bc0bf9d030bbcb01db69c44cf88b95ca41dd3065 Mon Sep 17 00:00:00 2001 From: Maria José Solano Date: Wed, 20 Sep 2023 19:03:40 -0700 Subject: docs: fix type warnings --- runtime/lua/vim/treesitter/dev.lua | 2 +- 1 file changed, 1 insertion(+), 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 61d84017d4..db30d638af 100644 --- a/runtime/lua/vim/treesitter/dev.lua +++ b/runtime/lua/vim/treesitter/dev.lua @@ -258,7 +258,7 @@ end --- @private --- ---- @param opts InspectTreeOpts +--- @param opts InspectTreeOpts? function M.inspect_tree(opts) vim.validate({ opts = { opts, 't', true }, -- cgit From 9ce1623837a817c3f4f5deff9c8ba862578b6009 Mon Sep 17 00:00:00 2001 From: Till Bungert Date: Sun, 1 Oct 2023 21:10:51 +0200 Subject: feat(treesitter): add foldtext with treesitter highlighting (#25391) --- runtime/lua/vim/treesitter/_fold.lua | 92 ++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/_fold.lua b/runtime/lua/vim/treesitter/_fold.lua index 8bc08c9c2e..c6a4b48d4f 100644 --- a/runtime/lua/vim/treesitter/_fold.lua +++ b/runtime/lua/vim/treesitter/_fold.lua @@ -361,4 +361,96 @@ function M.foldexpr(lnum) return foldinfos[bufnr].levels[lnum] or '0' end +---@package +---@return { [1]: string, [2]: string[] }[]|string +function M.foldtext() + local foldstart = vim.v.foldstart + local bufnr = api.nvim_get_current_buf() + + ---@type boolean, LanguageTree + local ok, parser = pcall(ts.get_parser, bufnr) + if not ok then + return vim.fn.foldtext() + end + + local query = ts.query.get(parser:lang(), 'highlights') + if not query then + return vim.fn.foldtext() + end + + local tree = parser:parse({ foldstart - 1, foldstart })[1] + + local line = api.nvim_buf_get_lines(bufnr, foldstart - 1, foldstart, false)[1] + if not line then + return vim.fn.foldtext() + end + + ---@type { [1]: string, [2]: string[], range: { [1]: integer, [2]: integer } }[] | { [1]: string, [2]: string[] }[] + local result = {} + + local line_pos = 0 + + for id, node, metadata in query:iter_captures(tree:root(), 0, foldstart - 1, foldstart) do + local name = query.captures[id] + local start_row, start_col, end_row, end_col = node:range() + + local priority = tonumber(metadata.priority or vim.highlight.priorities.treesitter) + + if start_row == foldstart - 1 and end_row == foldstart - 1 then + -- check for characters ignored by treesitter + if start_col > line_pos then + table.insert(result, { + line:sub(line_pos + 1, start_col), + { { 'Folded', priority } }, + range = { line_pos, start_col }, + }) + end + line_pos = end_col + + local text = line:sub(start_col + 1, end_col) + table.insert(result, { text, { { '@' .. name, priority } }, range = { start_col, end_col } }) + end + end + + local i = 1 + while i <= #result do + -- find first capture that is not in current range and apply highlights on the way + local j = i + 1 + while + j <= #result + and result[j].range[1] >= result[i].range[1] + and result[j].range[2] <= result[i].range[2] + do + for k, v in ipairs(result[i][2]) do + if not vim.tbl_contains(result[j][2], v) then + table.insert(result[j][2], k, v) + end + end + j = j + 1 + end + + -- remove the parent capture if it is split into children + if j > i + 1 then + table.remove(result, i) + else + -- highlights need to be sorted by priority, on equal prio, the deeper nested capture (earlier + -- in list) should be considered higher prio + if #result[i][2] > 1 then + table.sort(result[i][2], function(a, b) + return a[2] < b[2] + end) + end + + result[i][2] = vim.tbl_map(function(tbl) + return tbl[1] + end, result[i][2]) + result[i] = { result[i][1], result[i][2] } + + i = i + 1 + end + end + + return result +end + return M -- cgit From 3af59a415c98afc42755308e56912b302ad5eb3d Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Tue, 3 Oct 2023 13:07:03 +0800 Subject: fix(treesitter): make Visual hl work consistently with foldtext (#25484) Problem: Visual highlight is inconsistent on a folded line with treesitter foldtext. Solution: Don't added Folded highlight as it is already in background. --- 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 c6a4b48d4f..5c1cc06908 100644 --- a/runtime/lua/vim/treesitter/_fold.lua +++ b/runtime/lua/vim/treesitter/_fold.lua @@ -401,7 +401,7 @@ function M.foldtext() if start_col > line_pos then table.insert(result, { line:sub(line_pos + 1, start_col), - { { 'Folded', priority } }, + {}, range = { line_pos, start_col }, }) end -- cgit From c3d21ad1bccd9a2975be73b1115213fd884eada3 Mon Sep 17 00:00:00 2001 From: dundargoc Date: Fri, 15 Sep 2023 09:43:48 +0200 Subject: docs: small fixes Co-authored-by: Wansmer Co-authored-by: Andrew Voynov Co-authored-by: David Moberg --- runtime/lua/vim/treesitter/_meta.lua | 1 + 1 file changed, 1 insertion(+) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/_meta.lua b/runtime/lua/vim/treesitter/_meta.lua index d01b7be3b0..cd8462bcb7 100644 --- a/runtime/lua/vim/treesitter/_meta.lua +++ b/runtime/lua/vim/treesitter/_meta.lua @@ -29,6 +29,7 @@ ---@field sexpr fun(self: TSNode): string ---@field equal fun(self: TSNode, other: TSNode): boolean ---@field iter_children fun(self: TSNode): fun(): TSNode, string +---@field field fun(self: TSNode, name: string): TSNode[] local TSNode = {} ---@param query userdata -- cgit From 3fd7449d5abe9a75fed8fb6b68c5958bd1a9ee12 Mon Sep 17 00:00:00 2001 From: Maria José Solano Date: Tue, 17 Oct 2023 13:34:39 -0700 Subject: fix(treesitter): check that buf is loaded in autocommands (#25679) --- runtime/lua/vim/treesitter/dev.lua | 8 +++++++- 1 file changed, 7 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 db30d638af..d9f0a21106 100644 --- a/runtime/lua/vim/treesitter/dev.lua +++ b/runtime/lua/vim/treesitter/dev.lua @@ -366,6 +366,10 @@ function M.inspect_tree(opts) group = group, buffer = b, callback = function() + if not api.nvim_buf_is_loaded(buf) then + return true + end + api.nvim_buf_clear_namespace(buf, pg.ns, 0, -1) local row = api.nvim_win_get_cursor(w)[1] local pos = pg:get(row) @@ -438,6 +442,9 @@ function M.inspect_tree(opts) group = group, buffer = b, callback = function() + if not api.nvim_buf_is_loaded(buf) then + return true + end api.nvim_buf_clear_namespace(buf, pg.ns, 0, -1) end, }) @@ -449,7 +456,6 @@ function M.inspect_tree(opts) if not api.nvim_buf_is_loaded(b) then return true end - api.nvim_buf_clear_namespace(b, pg.ns, 0, -1) end, }) -- cgit From ae7020c667b5314fe832d2a8897fb16d57194bdf Mon Sep 17 00:00:00 2001 From: Pham Huy Hoang Date: Fri, 6 Oct 2023 16:27:10 +0900 Subject: fix(docs): fix TSNode incorrect signatures --- runtime/lua/vim/treesitter/_meta.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/_meta.lua b/runtime/lua/vim/treesitter/_meta.lua index cd8462bcb7..e6dcbc7a62 100644 --- a/runtime/lua/vim/treesitter/_meta.lua +++ b/runtime/lua/vim/treesitter/_meta.lua @@ -14,10 +14,10 @@ ---@field extra fun(self: TSNode): boolean ---@field child_count fun(self: TSNode): integer ---@field named_child_count fun(self: TSNode): integer ----@field child fun(self: TSNode, integer): TSNode? ----@field named_child fun(self: TSNode, integer): TSNode? ----@field descendant_for_range fun(self: TSNode, integer, integer, integer, integer): TSNode? ----@field named_descendant_for_range fun(self: TSNode, integer, integer, integer, integer): TSNode? +---@field child fun(self: TSNode, index: integer): TSNode? +---@field named_child fun(self: TSNode, index: integer): TSNode? +---@field descendant_for_range fun(self: TSNode, start_row: integer, start_col: integer, end_row: integer, end_col: integer): TSNode? +---@field named_descendant_for_range fun(self: TSNode, start_row: integer, start_col: integer, end_row: integer, end_col: integer): TSNode? ---@field parent fun(self: TSNode): TSNode? ---@field next_sibling fun(self: TSNode): TSNode? ---@field prev_sibling fun(self: TSNode): TSNode? -- cgit From 315c711700a87fe3fa546906ab39557ebba19baf Mon Sep 17 00:00:00 2001 From: Maria José Solano Date: Wed, 18 Oct 2023 21:18:24 -0700 Subject: fix(treesitter): set cursor position when opening inspector --- runtime/lua/vim/treesitter/dev.lua | 64 ++++++++++++++++++++++++-------------- 1 file changed, 40 insertions(+), 24 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/dev.lua b/runtime/lua/vim/treesitter/dev.lua index d9f0a21106..39c03cd1a5 100644 --- a/runtime/lua/vim/treesitter/dev.lua +++ b/runtime/lua/vim/treesitter/dev.lua @@ -181,6 +181,42 @@ local function set_dev_properties(w, b) vim.bo[b].filetype = 'query' end +--- Updates the cursor position in the inspector to match the node under the cursor. +--- +--- @param pg TSTreeView +--- @param lang string +--- @param source_buf integer +--- @param inspect_buf integer +--- @param inspect_win integer +--- @param pos? { [1]: integer, [2]: integer } +local function set_inspector_cursor(pg, lang, source_buf, inspect_buf, inspect_win, pos) + api.nvim_buf_clear_namespace(inspect_buf, pg.ns, 0, -1) + + local cursor_node = vim.treesitter.get_node({ + bufnr = source_buf, + lang = lang, + pos = pos, + ignore_injections = false, + }) + if not cursor_node then + return + end + + local cursor_node_id = cursor_node:id() + for i, v in pg:iter() do + if v.id == cursor_node_id then + local start = v.depth + local end_col = start + #v.text + api.nvim_buf_set_extmark(inspect_buf, pg.ns, i - 1, start, { + end_col = end_col, + hl_group = 'Visual', + }) + api.nvim_win_set_cursor(inspect_win, { i, 0 }) + break + end + end +end + --- Write the contents of this View into {bufnr}. --- ---@param bufnr integer Buffer number to write into. @@ -307,6 +343,9 @@ function M.inspect_tree(opts) pg:draw(b) + local cursor = api.nvim_win_get_cursor(win) + set_inspector_cursor(pg, opts.lang, buf, b, w, { cursor[1] - 1, cursor[2] }) + api.nvim_buf_clear_namespace(buf, pg.ns, 0, -1) api.nvim_buf_set_keymap(b, 'n', '', '', { desc = 'Jump to the node under the cursor in the source buffer', @@ -398,30 +437,7 @@ function M.inspect_tree(opts) return true end - api.nvim_buf_clear_namespace(b, pg.ns, 0, -1) - - local cursor_node = vim.treesitter.get_node({ - bufnr = buf, - lang = opts.lang, - ignore_injections = false, - }) - if not cursor_node then - return - end - - local cursor_node_id = cursor_node:id() - for i, v in pg:iter() do - if v.id == cursor_node_id then - local start = v.depth - local end_col = start + #v.text - api.nvim_buf_set_extmark(b, pg.ns, i - 1, start, { - end_col = end_col, - hl_group = 'Visual', - }) - api.nvim_win_set_cursor(w, { i, 0 }) - break - end - end + set_inspector_cursor(pg, opts.lang, buf, b, w) end, }) -- cgit From 72ed99319dd662f0e35b58e888b57f98ac3b3eec Mon Sep 17 00:00:00 2001 From: Dmytro Soltys Date: Mon, 27 Nov 2023 13:34:32 +0100 Subject: fix(treesitter): don't invalidate parser when discovering injections When parsing with a range, languagetree looks up injections and adds them if needed. This explicitly invalidates parser, making `is_valid` report `false` both when including and excluding children. This is an attempt to describe desired behaviour of `is_valid` in tests, with what ended up being a single line change to satisfy them. --- runtime/lua/vim/treesitter/languagetree.lua | 2 -- 1 file changed, 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 670f2797b7..0171b416cd 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -508,7 +508,6 @@ function LanguageTree:add_child(lang) end self._children[lang] = child - self:invalidate() self:_do_callback('child_added', self._children[lang]) return self._children[lang] @@ -524,7 +523,6 @@ function LanguageTree:remove_child(lang) if child then self._children[lang] = nil child:destroy() - self:invalidate() self:_do_callback('child_removed', child) end end -- cgit From 7bc5ee7f9327e8210c78bd21935130840aaf63f2 Mon Sep 17 00:00:00 2001 From: Gregory Anders <8965202+gpanders@users.noreply.github.com> Date: Wed, 29 Nov 2023 08:10:02 -0600 Subject: fix(treesitter): use proper query syntax for inspector (#26274) --- runtime/lua/vim/treesitter/dev.lua | 69 ++++++++++++++++++-------------------- 1 file changed, 33 insertions(+), 36 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/dev.lua b/runtime/lua/vim/treesitter/dev.lua index 39c03cd1a5..1c581e3f29 100644 --- a/runtime/lua/vim/treesitter/dev.lua +++ b/runtime/lua/vim/treesitter/dev.lua @@ -55,12 +55,12 @@ local function traverse(node, depth, lang, injections, tree) local text ---@type string if named then if field then - text = string.format('%s: (%s)', field, type) + text = string.format('%s: (%s', field, type) else - text = string.format('(%s)', type) + text = string.format('(%s', type) end else - text = string.format('"%s"', type:gsub('\n', '\\n')) + text = string.format('"%s"', type:gsub('\n', '\\n'):gsub('"', '\\"')) end table.insert(tree, { @@ -76,6 +76,10 @@ local function traverse(node, depth, lang, injections, tree) }) traverse(child, depth + 1, lang, injections, tree) + + if named then + tree[#tree].text = string.format('%s)', tree[#tree].text) + end end return tree @@ -152,12 +156,6 @@ local function get_range_str(lnum, col, end_lnum, end_col) return string.format('[%d:%d - %d:%d]', lnum + 1, col + 1, end_lnum + 1, end_col) end ----@param text string ----@return string -local function escape_quotes(text) - return string.format('"%s"', text:sub(2, #text - 1):gsub('"', '\\"')) -end - ---@param w integer ---@return boolean closed Whether the window was closed. local function close_win(w) @@ -183,14 +181,14 @@ end --- Updates the cursor position in the inspector to match the node under the cursor. --- ---- @param pg TSTreeView +--- @param treeview TSTreeView --- @param lang string --- @param source_buf integer --- @param inspect_buf integer --- @param inspect_win integer --- @param pos? { [1]: integer, [2]: integer } -local function set_inspector_cursor(pg, lang, source_buf, inspect_buf, inspect_win, pos) - api.nvim_buf_clear_namespace(inspect_buf, pg.ns, 0, -1) +local function set_inspector_cursor(treeview, lang, source_buf, inspect_buf, inspect_win, pos) + api.nvim_buf_clear_namespace(inspect_buf, treeview.ns, 0, -1) local cursor_node = vim.treesitter.get_node({ bufnr = source_buf, @@ -203,11 +201,11 @@ local function set_inspector_cursor(pg, lang, source_buf, inspect_buf, inspect_w end local cursor_node_id = cursor_node:id() - for i, v in pg:iter() do + for i, v in treeview:iter() do if v.id == cursor_node_id then local start = v.depth local end_col = start + #v.text - api.nvim_buf_set_extmark(inspect_buf, pg.ns, i - 1, start, { + api.nvim_buf_set_extmark(inspect_buf, treeview.ns, i - 1, start, { end_col = end_col, hl_group = 'Visual', }) @@ -229,9 +227,8 @@ function TSTreeView:draw(bufnr) for _, item in self:iter() do local range_str = get_range_str(item.lnum, item.col, item.end_lnum, item.end_col) local lang_str = self.opts.lang and string.format(' %s', item.lang) or '' - local text = item.named and item.text or escape_quotes(item.text) local line = - string.format('%s%s ; %s%s', string.rep(' ', item.depth), text, range_str, lang_str) + string.format('%s%s ; %s%s', string.rep(' ', item.depth), item.text, range_str, lang_str) if self.opts.lang then lang_hl_marks[#lang_hl_marks + 1] = { @@ -304,7 +301,7 @@ function M.inspect_tree(opts) local buf = api.nvim_get_current_buf() local win = api.nvim_get_current_win() - local pg = assert(TSTreeView:new(buf, opts.lang)) + local treeview = assert(TSTreeView:new(buf, opts.lang)) -- Close any existing inspector window if vim.b[buf].dev_inspect then @@ -341,17 +338,17 @@ function M.inspect_tree(opts) assert(type(title) == 'string', 'Window title must be a string') api.nvim_buf_set_name(b, title) - pg:draw(b) + treeview:draw(b) local cursor = api.nvim_win_get_cursor(win) - set_inspector_cursor(pg, opts.lang, buf, b, w, { cursor[1] - 1, cursor[2] }) + set_inspector_cursor(treeview, opts.lang, buf, b, w, { cursor[1] - 1, cursor[2] }) - api.nvim_buf_clear_namespace(buf, pg.ns, 0, -1) + api.nvim_buf_clear_namespace(buf, treeview.ns, 0, -1) api.nvim_buf_set_keymap(b, 'n', '', '', { desc = 'Jump to the node under the cursor in the source buffer', callback = function() local row = api.nvim_win_get_cursor(w)[1] - local pos = pg:get(row) + local pos = treeview:get(row) api.nvim_set_current_win(win) api.nvim_win_set_cursor(win, { pos.lnum + 1, pos.col }) end, @@ -360,21 +357,21 @@ function M.inspect_tree(opts) desc = 'Toggle anonymous nodes', callback = function() local row, col = unpack(api.nvim_win_get_cursor(w)) ---@type integer, integer - local curnode = pg:get(row) + local curnode = treeview:get(row) while curnode and not curnode.named do row = row - 1 - curnode = pg:get(row) + curnode = treeview:get(row) end - pg.opts.anon = not pg.opts.anon - pg:draw(b) + treeview.opts.anon = not treeview.opts.anon + treeview:draw(b) if not curnode then return end local id = curnode.id - for i, node in pg:iter() do + for i, node in treeview:iter() do if node.id == id then api.nvim_win_set_cursor(w, { i, col }) break @@ -385,8 +382,8 @@ function M.inspect_tree(opts) api.nvim_buf_set_keymap(b, 'n', 'I', '', { desc = 'Toggle language display', callback = function() - pg.opts.lang = not pg.opts.lang - pg:draw(b) + treeview.opts.lang = not treeview.opts.lang + treeview:draw(b) end, }) api.nvim_buf_set_keymap(b, 'n', 'o', '', { @@ -409,10 +406,10 @@ function M.inspect_tree(opts) return true end - api.nvim_buf_clear_namespace(buf, pg.ns, 0, -1) + api.nvim_buf_clear_namespace(buf, treeview.ns, 0, -1) local row = api.nvim_win_get_cursor(w)[1] - local pos = pg:get(row) - api.nvim_buf_set_extmark(buf, pg.ns, pos.lnum, pos.col, { + local pos = treeview:get(row) + api.nvim_buf_set_extmark(buf, treeview.ns, pos.lnum, pos.col, { end_row = pos.end_lnum, end_col = math.max(0, pos.end_col), hl_group = 'Visual', @@ -437,7 +434,7 @@ function M.inspect_tree(opts) return true end - set_inspector_cursor(pg, opts.lang, buf, b, w) + set_inspector_cursor(treeview, opts.lang, buf, b, w) end, }) @@ -449,8 +446,8 @@ function M.inspect_tree(opts) return true end - pg = assert(TSTreeView:new(buf, opts.lang)) - pg:draw(b) + treeview = assert(TSTreeView:new(buf, opts.lang)) + treeview:draw(b) end, }) @@ -461,7 +458,7 @@ function M.inspect_tree(opts) if not api.nvim_buf_is_loaded(buf) then return true end - api.nvim_buf_clear_namespace(buf, pg.ns, 0, -1) + api.nvim_buf_clear_namespace(buf, treeview.ns, 0, -1) end, }) @@ -472,7 +469,7 @@ function M.inspect_tree(opts) if not api.nvim_buf_is_loaded(b) then return true end - api.nvim_buf_clear_namespace(b, pg.ns, 0, -1) + api.nvim_buf_clear_namespace(b, treeview.ns, 0, -1) end, }) -- cgit From b6e339eb90e1a04f407f381739e46ad3c84f69c5 Mon Sep 17 00:00:00 2001 From: Pham Huy Hoang Date: Wed, 29 Nov 2023 23:16:52 +0900 Subject: fix(treesitter): make InspectTree correctly handle nested injections (#26085) Problem: Only injections under the top level tree are found. Solution: Iterate through all trees to find injections. When two injections are contained within the same node in the parent tree, prefer the injection with the larger byte length. --- runtime/lua/vim/treesitter/dev.lua | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/dev.lua b/runtime/lua/vim/treesitter/dev.lua index 1c581e3f29..aa4331946a 100644 --- a/runtime/lua/vim/treesitter/dev.lua +++ b/runtime/lua/vim/treesitter/dev.lua @@ -105,18 +105,23 @@ function TSTreeView:new(bufnr, lang) -- the root in the child tree to the {injections} table. local root = parser:parse(true)[1]:root() local injections = {} ---@type table - for _, child in pairs(parser:children()) do - child:for_each_tree(function(tree, ltree) - local r = tree:root() - local node = root:named_descendant_for_range(r:range()) - if node then - injections[node:id()] = { - lang = ltree:lang(), - root = r, - } - end - end) - end + + parser:for_each_tree(function(parent_tree, parent_ltree) + local parent = parent_tree:root() + for _, child in pairs(parent_ltree:children()) do + child:for_each_tree(function(tree, ltree) + local r = tree:root() + local node = assert(parent:named_descendant_for_range(r:range())) + local id = node:id() + if not injections[id] or r:byte_length() > injections[id].root:byte_length() then + injections[id] = { + lang = ltree:lang(), + root = r, + } + end + end) + end + end) local nodes = traverse(root, 0, parser:lang(), injections, {}) -- cgit From 18c1fd8e9d759da6806747910320dce6bea2ab42 Mon Sep 17 00:00:00 2001 From: Gregory Anders <8965202+gpanders@users.noreply.github.com> Date: Wed, 29 Nov 2023 08:59:36 -0600 Subject: docs: document TSNode:byte_length() (#26287) Also update the type annotation of TSNode:id(), which returns a string, not an integer. --- runtime/lua/vim/treesitter/_meta.lua | 3 ++- runtime/lua/vim/treesitter/dev.lua | 10 +++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/_meta.lua b/runtime/lua/vim/treesitter/_meta.lua index e6dcbc7a62..80c998b555 100644 --- a/runtime/lua/vim/treesitter/_meta.lua +++ b/runtime/lua/vim/treesitter/_meta.lua @@ -1,7 +1,7 @@ ---@meta ---@class TSNode: userdata ----@field id fun(self: TSNode): integer +---@field id fun(self: TSNode): string ---@field tree fun(self: TSNode): TSTree ---@field range fun(self: TSNode, include_bytes: false?): integer, integer, integer, integer ---@field range fun(self: TSNode, include_bytes: true): integer, integer, integer, integer, integer, integer @@ -30,6 +30,7 @@ ---@field equal fun(self: TSNode, other: TSNode): boolean ---@field iter_children fun(self: TSNode): fun(): TSNode, string ---@field field fun(self: TSNode, name: string): TSNode[] +---@field byte_length fun(self: TSNode): integer local TSNode = {} ---@param query userdata diff --git a/runtime/lua/vim/treesitter/dev.lua b/runtime/lua/vim/treesitter/dev.lua index aa4331946a..d6825eb024 100644 --- a/runtime/lua/vim/treesitter/dev.lua +++ b/runtime/lua/vim/treesitter/dev.lua @@ -24,6 +24,10 @@ local TSTreeView = {} ---@field lang string Source language of this node ---@field root TSNode +---@class TSP.Injection +---@field lang string Source language of this injection +---@field root TSNode Root node of the injection + --- Traverse all child nodes starting at {node}. --- --- This is a recursive function. The {depth} parameter indicates the current recursion level. @@ -39,8 +43,8 @@ local TSTreeView = {} ---@param node TSNode Starting node to begin traversal |tsnode| ---@param depth integer Current recursion depth ---@param lang string Language of the tree currently being traversed ----@param injections table Mapping of node ids to root nodes of injected language trees (see ---- explanation above) +---@param injections table Mapping of node ids to root nodes +--- of injected language trees (see explanation above) ---@param tree TSP.Node[] Output table containing a list of tables each representing a node in the tree local function traverse(node, depth, lang, injections, tree) local injection = injections[node:id()] @@ -104,7 +108,7 @@ function TSTreeView:new(bufnr, lang) -- the primary tree that contains that root. Add a mapping from the node in the primary tree to -- the root in the child tree to the {injections} table. local root = parser:parse(true)[1]:root() - local injections = {} ---@type table + local injections = {} ---@type table parser:for_each_tree(function(parent_tree, parent_ltree) local parent = parent_tree:root() -- cgit From 4a8bf24ac690004aedf5540fa440e788459e5e34 Mon Sep 17 00:00:00 2001 From: Gregory Anders <8965202+gpanders@users.noreply.github.com> Date: Wed, 29 Nov 2023 10:17:53 -0600 Subject: fix(treesitter): adjust indentation in inspector highlights (#26302) --- runtime/lua/vim/treesitter/dev.lua | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) (limited to 'runtime/lua/vim/treesitter') diff --git a/runtime/lua/vim/treesitter/dev.lua b/runtime/lua/vim/treesitter/dev.lua index d6825eb024..69ddc9b558 100644 --- a/runtime/lua/vim/treesitter/dev.lua +++ b/runtime/lua/vim/treesitter/dev.lua @@ -8,6 +8,7 @@ local M = {} ---@field opts table Options table with the following keys: --- - anon (boolean): If true, display anonymous nodes --- - lang (boolean): If true, display the language alongside each node +--- - indent (number): Number of spaces to indent nested lines. Default is 2. ---@field nodes TSP.Node[] ---@field named TSP.Node[] local TSTreeView = {} @@ -143,6 +144,7 @@ function TSTreeView:new(bufnr, lang) opts = { anon = false, lang = false, + indent = 2, }, } @@ -212,7 +214,7 @@ local function set_inspector_cursor(treeview, lang, source_buf, inspect_buf, ins local cursor_node_id = cursor_node:id() for i, v in treeview:iter() do if v.id == cursor_node_id then - local start = v.depth + local start = v.depth * treeview.opts.indent ---@type integer local end_col = start + #v.text api.nvim_buf_set_extmark(inspect_buf, treeview.ns, i - 1, start, { end_col = end_col, @@ -236,8 +238,13 @@ function TSTreeView:draw(bufnr) for _, item in self:iter() do local range_str = get_range_str(item.lnum, item.col, item.end_lnum, item.end_col) local lang_str = self.opts.lang and string.format(' %s', item.lang) or '' - local line = - string.format('%s%s ; %s%s', string.rep(' ', item.depth), item.text, range_str, lang_str) + local line = string.format( + '%s%s ; %s%s', + string.rep(' ', item.depth * self.opts.indent), + item.text, + range_str, + lang_str + ) if self.opts.lang then lang_hl_marks[#lang_hl_marks + 1] = { -- cgit