aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim/treesitter
diff options
context:
space:
mode:
Diffstat (limited to 'runtime/lua/vim/treesitter')
-rw-r--r--runtime/lua/vim/treesitter/_meta.lua3
-rw-r--r--runtime/lua/vim/treesitter/_query_linter.lua54
-rw-r--r--runtime/lua/vim/treesitter/dev.lua7
-rw-r--r--runtime/lua/vim/treesitter/languagetree.lua69
-rw-r--r--runtime/lua/vim/treesitter/query.lua42
5 files changed, 73 insertions, 102 deletions
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/_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<string,table<string,vim.treesitter.ParseError|true>>
-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: <lang>/<query_type>.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<integer,TSNode>
--- @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/dev.lua b/runtime/lua/vim/treesitter/dev.lua
index 7f24ba8590..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 },
@@ -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/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua
index b555ee231b..670f2797b7 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<integer, 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 _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<integer, TSTree> Reference to parsed tree (one for each language).
+---Each key is the index of region, which is synced with _regions and _valid.
---@field private _valid boolean|table<integer,boolean> If the parsed tree is valid
---@field private _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<integer, TSTree>
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<string,boolean>
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<integer, TSTree>
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<integer, Range6[]>
function LanguageTree:included_regions()
if self._regions then
return self._regions
end
- if not self._has_regions or #self._trees == 0 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
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
@@ -727,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.
@@ -801,7 +814,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.
---
diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua
index 44ed37d64e..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<string, table<string, Query|false>>
-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<string, table<string, Query>>
-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(...)
@@ -843,11 +817,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)