12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298 |
- --- @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({ 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
- --- `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.
- ---
- -- 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 query = require('vim.treesitter.query')
- local language = require('vim.treesitter.language')
- local Range = require('vim.treesitter._range')
- local default_parse_timeout_ms = 3
- ---@alias TSCallbackName
- ---| 'changedtree'
- ---| 'bytes'
- ---| 'detach'
- ---| 'child_added'
- ---| 'child_removed'
- ---@alias TSCallbackNameOn
- ---| 'on_changedtree'
- ---| 'on_bytes'
- ---| 'on_detach'
- ---| 'on_child_added'
- ---| 'on_child_removed'
- --- @type table<TSCallbackNameOn,TSCallbackName>
- local TSCallbackNames = {
- on_changedtree = 'changedtree',
- on_bytes = 'bytes',
- on_detach = 'detach',
- on_child_added = 'child_added',
- on_child_removed = 'child_removed',
- }
- ---@nodoc
- ---@class vim.treesitter.LanguageTree
- ---@field private _callbacks table<TSCallbackName,function[]> Callback handlers
- ---@field package _callbacks_rec table<TSCallbackName,function[]> Callback handlers (recursive)
- ---@field private _children table<string,vim.treesitter.LanguageTree> Injected languages
- ---@field private _injection_query vim.treesitter.Query Queries defining injected languages
- ---@field private _injections_processed boolean
- ---@field private _opts table Options
- ---@field private _parser TSParser Parser for language
- ---Table of regions for which the tree is currently running an async parse
- ---@field private _ranges_being_parsed table<string, boolean>
- ---Table of callback queues, keyed by each region for which the callbacks should be run
- ---@field private _cb_queues table<string, fun(err?: string, trees?: table<integer, TSTree>)[]>
- ---@field private _has_regions boolean
- ---@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? vim.treesitter.LanguageTree Parent LanguageTree
- ---@field private _source (integer|string) Buffer or string to parse
- ---@field private _trees table<integer, TSTree> Reference to parsed tree (one for each language).
- ---Each key is the index of region, which is synced with _regions and _valid.
- ---@field private _valid boolean|table<integer,boolean> If the parsed tree is valid
- ---@field private _logger? fun(logtype: string, msg: string)
- ---@field private _logfile? file*
- local LanguageTree = {}
- ---Optional arguments:
- ---@class vim.treesitter.LanguageTree.new.Opts
- ---@inlinedoc
- ---@field queries? table<string,string> -- Deprecated
- ---@field injections? table<string,string>
- LanguageTree.__index = LanguageTree
- --- @nodoc
- ---
- --- 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 vim.treesitter.LanguageTree.new.Opts?
- ---@return vim.treesitter.LanguageTree parser object
- function LanguageTree.new(source, lang, opts)
- assert(language.add(lang))
- opts = opts or {}
- if source == 0 then
- source = vim.api.nvim_get_current_buf()
- end
- local injections = opts.injections or {}
- --- @type vim.treesitter.LanguageTree
- local self = {
- _source = source,
- _lang = lang,
- _children = {},
- _trees = {},
- _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),
- _ranges_being_parsed = {},
- _cb_queues = {},
- _callbacks = {},
- _callbacks_rec = {},
- }
- 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
- self._callbacks[name] = {}
- self._callbacks_rec[name] = {}
- end
- return self
- end
- --- @private
- function LanguageTree:_set_logger()
- local source = self:source()
- source = type(source) == 'string' and 'text' or tostring(source)
- local lang = self:lang()
- local logdir = vim.fn.stdpath('log') --[[@as string]]
- vim.fn.mkdir(logdir, 'p')
- local logfilename = vim.fs.joinpath(logdir, '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
- ---Measure execution time of a function
- ---@generic R1, R2, R3
- ---@param f fun(): R1, R2, R2
- ---@return number, R1, R2, R3
- local function tcall(f, ...)
- local start = vim.uv.hrtime()
- ---@diagnostic disable-next-line
- local r = { f(...) }
- --- @type number
- local duration = (vim.uv.hrtime() - start) / 1000000
- return duration, unpack(r)
- end
- ---@private
- ---@param ... any
- function LanguageTree:_log(...)
- if not self._logger then
- return
- end
- if not vim.g.__ts_debug or vim.g.__ts_debug < 1 then
- return
- end
- local args = { ... }
- if type(args[1]) == 'function' then
- args = { args[1]() }
- end
- local info = debug.getinfo(2, 'nl')
- local nregions = vim.tbl_count(self:included_regions())
- 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
- if type(x) == 'string' then
- msg[#msg + 1] = x
- else
- msg[#msg + 1] = vim.inspect(x, { newline = ' ', indent = '' })
- end
- end
- self._logger('nvim', table.concat(msg, ' '))
- end
- --- Invalidates this parser and its children.
- ---
- --- Should only be called when the tracked state of the LanguageTree is not valid against the parse
- --- tree in treesitter. Doesn't clear filesystem cache. Called often, so needs to be fast.
- ---@param reload boolean|nil
- function LanguageTree:invalidate(reload)
- self._valid = false
- self._parser:reset()
- -- buffer was reloaded, reparse all trees
- if reload then
- for _, t in pairs(self._trees) do
- self:_do_callback('changedtree', t:included_ranges(true), t)
- end
- self._trees = {}
- end
- for _, child in pairs(self._children) do
- child:invalidate(reload)
- end
- end
- --- 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
- --- Gets the language of this tree node.
- function LanguageTree:lang()
- return self._lang
- end
- --- Returns whether this LanguageTree is valid, i.e., |LanguageTree:trees()| reflects the latest
- --- state of the source. If invalid, user should call |LanguageTree:parse()|.
- ---@param exclude_children boolean|nil whether to ignore the validity of children (default `false`)
- ---@return boolean
- function LanguageTree:is_valid(exclude_children)
- local valid = self._valid
- if type(valid) == 'table' then
- for i, _ in pairs(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
- end
- end
- end
- if type(valid) == 'boolean' then
- return valid
- end
- self._valid = true
- return true
- end
- --- Returns a map of language to child tree.
- function LanguageTree:children()
- return self._children
- end
- --- Returns the source content of the language tree (bufnr or string).
- function LanguageTree:source()
- return self._source
- end
- --- @param region Range6[]
- --- @param range? boolean|Range
- --- @return boolean
- local function intercepts_region(region, range)
- if #region == 0 then
- return true
- end
- if range == nil then
- return false
- end
- if type(range) == 'boolean' then
- return range
- end
- for _, r in ipairs(region) do
- if Range.intercepts(r, range) then
- return true
- end
- end
- return false
- end
- --- @private
- --- @param range boolean|Range?
- --- @param timeout integer?
- --- @return Range6[] changes
- --- @return integer no_regions_parsed
- --- @return number total_parse_time
- --- @return boolean finished whether async parsing still needs time
- function LanguageTree:_parse_regions(range, timeout)
- local changes = {}
- local no_regions_parsed = 0
- local total_parse_time = 0
- if type(self._valid) ~= 'table' then
- self._valid = {}
- end
- -- If there are no ranges, set to an empty list
- -- so the included ranges in the parser are cleared.
- for i, ranges in pairs(self:included_regions()) do
- if
- not self._valid[i]
- and (
- intercepts_region(ranges, range)
- or (self._trees[i] and intercepts_region(self._trees[i]:included_ranges(false), range))
- )
- then
- self._parser:set_included_ranges(ranges)
- self._parser:set_timeout(timeout and timeout * 1000 or 0) -- ms -> micros
- local parse_time, tree, tree_changes =
- tcall(self._parser.parse, self._parser, self._trees[i], self._source, true)
- if not tree then
- return changes, no_regions_parsed, total_parse_time, false
- end
- -- Pass ranges if this is an initial parse
- local cb_changes = self._trees[i] and tree_changes or tree:included_ranges(true)
- 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, true
- end
- --- @private
- --- @return number
- 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_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
- -- they can depend on the text of a node. Intermediate strings
- -- would cause errors for unknown parsers.
- if has_lang then
- local child = self._children[lang]
- if not child then
- child = self:add_child(lang)
- end
- child:set_included_regions(injection_regions)
- seen_langs[lang] = true
- end
- end
- for lang, _ in pairs(self._children) do
- if not seen_langs[lang] then
- self:remove_child(lang)
- end
- end
- return query_time
- end
- --- @param range boolean|Range?
- --- @return string
- local function range_to_string(range)
- return type(range) == 'table' and table.concat(range, ',') or tostring(range)
- end
- --- @private
- --- @param range boolean|Range?
- --- @param callback fun(err?: string, trees?: table<integer, TSTree>)
- function LanguageTree:_push_async_callback(range, callback)
- local key = range_to_string(range)
- self._cb_queues[key] = self._cb_queues[key] or {}
- local queue = self._cb_queues[key]
- queue[#queue + 1] = callback
- end
- --- @private
- --- @param range boolean|Range?
- --- @param err? string
- --- @param trees? table<integer, TSTree>
- function LanguageTree:_run_async_callbacks(range, err, trees)
- local key = range_to_string(range)
- for _, cb in ipairs(self._cb_queues[key]) do
- cb(err, trees)
- end
- self._ranges_being_parsed[key] = false
- self._cb_queues[key] = {}
- end
- --- Run an asynchronous parse, calling {on_parse} when complete.
- ---
- --- @private
- --- @param range boolean|Range?
- --- @param on_parse fun(err?: string, trees?: table<integer, TSTree>)
- --- @return table<integer, TSTree>? trees the list of parsed trees, if parsing completed synchronously
- function LanguageTree:_async_parse(range, on_parse)
- self:_push_async_callback(range, on_parse)
- -- If we are already running an async parse, just queue the callback.
- local range_string = range_to_string(range)
- if not self._ranges_being_parsed[range_string] then
- self._ranges_being_parsed[range_string] = true
- else
- return
- end
- local buf = vim.b[self._source]
- local ct = buf.changedtick
- local total_parse_time = 0
- local redrawtime = vim.o.redrawtime
- local timeout = not vim.g._ts_force_sync_parsing and default_parse_timeout_ms or nil
- local function step()
- -- If buffer was changed in the middle of parsing, reset parse state
- if buf.changedtick ~= ct then
- ct = buf.changedtick
- total_parse_time = 0
- end
- local parse_time, trees, finished = tcall(self._parse, self, range, timeout)
- total_parse_time = total_parse_time + parse_time
- if finished then
- self:_run_async_callbacks(range, nil, trees)
- return trees
- elseif total_parse_time > redrawtime then
- self:_run_async_callbacks(range, 'TIMEOUT', nil)
- return nil
- else
- vim.schedule(step)
- end
- end
- return step()
- end
- --- Recursively parse all regions in the language tree using |treesitter-parsers|
- --- for the corresponding languages and run injection queries on the parsed trees
- --- to determine whether child trees should be created and parsed.
- ---
- --- 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).
- --- @param on_parse fun(err?: string, trees?: table<integer, TSTree>)? Function invoked when parsing completes.
- --- When provided and `vim.g._ts_force_sync_parsing` is not set, parsing will run
- --- asynchronously. The first argument to the function is a string respresenting the error type,
- --- in case of a failure (currently only possible for timeouts). The second argument is the list
- --- of trees returned by the parse (upon success), or `nil` if the parse timed out (determined
- --- by 'redrawtime').
- ---
- --- If parsing was still able to finish synchronously (within 3ms), `parse()` returns the list
- --- of trees. Otherwise, it returns `nil`.
- --- @return table<integer, TSTree>?
- function LanguageTree:parse(range, on_parse)
- if on_parse then
- return self:_async_parse(range, on_parse)
- end
- local trees, _ = self:_parse(range)
- return trees
- end
- --- @private
- --- @param range boolean|Range|nil
- --- @param timeout integer?
- --- @return table<integer, TSTree> trees
- --- @return boolean finished
- function LanguageTree:_parse(range, timeout)
- if self:is_valid() then
- self:_log('valid')
- return self._trees, true
- end
- local changes --- @type Range6[]?
- -- Collect some stats
- local no_regions_parsed = 0
- local query_time = 0
- local total_parse_time = 0
- local is_finished --- @type boolean
- -- At least 1 region is invalid
- if not self:is_valid(true) then
- changes, no_regions_parsed, total_parse_time, is_finished = self:_parse_regions(range, timeout)
- timeout = timeout and math.max(timeout - total_parse_time, 0)
- if not is_finished then
- return self._trees, is_finished
- end
- -- Need to run injections when we parsed something
- if no_regions_parsed > 0 then
- self._injections_processed = false
- end
- end
- if not self._injections_processed and range then
- query_time = self:_add_injections()
- self._injections_processed = true
- end
- self:_log({
- 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,
- })
- for _, child in pairs(self._children) do
- if timeout == 0 then
- return self._trees, false
- end
- local ctime, _, child_finished = tcall(child._parse, child, range, timeout)
- timeout = timeout and math.max(timeout - ctime, 0)
- if not child_finished then
- return self._trees, child_finished
- end
- end
- return self._trees, true
- end
- --- Invokes the callback for each |LanguageTree| recursively.
- ---
- --- Note: This includes the invoking tree's child trees as well.
- ---
- ---@param fn fun(tree: TSTree, ltree: vim.treesitter.LanguageTree)
- function LanguageTree:for_each_tree(fn)
- for _, tree in pairs(self._trees) do
- fn(tree, self)
- end
- for _, child in pairs(self._children) do
- child:for_each_tree(fn)
- end
- end
- --- Adds a child language to this |LanguageTree|.
- ---
- --- If the language already exists as a child, it will first be removed.
- ---
- ---@private
- ---@param lang string Language to add.
- ---@return vim.treesitter.LanguageTree injected
- function LanguageTree:add_child(lang)
- if self._children[lang] then
- self:remove_child(lang)
- end
- 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
- child._parent = self
- self._children[lang] = child
- self:_do_callback('child_added', self._children[lang])
- return self._children[lang]
- end
- --- @package
- function LanguageTree:parent()
- return self._parent
- end
- --- Removes a child language from this |LanguageTree|.
- ---
- ---@private
- ---@param lang string Language to remove.
- function LanguageTree:remove_child(lang)
- local child = self._children[lang]
- if child then
- self._children[lang] = nil
- child:destroy()
- self:_do_callback('child_removed', child)
- end
- end
- --- Destroys this |LanguageTree| and all its children.
- ---
- --- Any cleanup logic should be performed here.
- ---
- --- Note: This DOES NOT remove this tree from a parent. Instead,
- --- `remove_child` must be called on the parent to remove it.
- function LanguageTree:destroy()
- -- Cleanup here
- for _, child in pairs(self._children) do
- child:destroy()
- end
- end
- ---@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)
- 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
- 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 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
- 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.
- ---
- --- For example, `{ { node1 }, { node2} }` contains two separate regions.
- --- They will be parsed by the parser in two different contexts, thus resulting
- --- in two separate trees.
- ---
- --- On the other hand, `{ { node1, node2 } }` is a single region consisting of
- --- two nodes. This will be parsed by the parser in a single context, thus resulting
- --- in a single tree.
- ---
- --- This allows for embedded languages to be parsed together across different
- --- nodes, which is useful for templating languages like ERB and EJS.
- ---
- ---@private
- ---@param new_regions (Range4|Range6|TSNode)[][] List of regions this tree should manage and parse.
- function LanguageTree:set_included_regions(new_regions)
- self._has_regions = true
- -- Transform the tables from 4 element long to 6 element long (with byte offset)
- for _, region in ipairs(new_regions) do
- for i, range in ipairs(region) do
- if type(range) == 'table' and #range == 4 then
- 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 pairs(self._trees) do
- self:_do_callback('changedtree', t:included_ranges(true), t)
- end
- self._trees = {}
- self:invalidate()
- else
- self:_iter_regions(function(i, region)
- return vim.deep_equal(new_regions[i], region)
- end)
- end
- self._regions = new_regions
- end
- ---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.
- ---Each list represents a range in the form of
- ---{ {start_row}, {start_col}, {start_bytes}, {end_row}, {end_col}, {end_bytes} }.
- ---@return table<integer, Range6[]>
- function LanguageTree:included_regions()
- if self._regions then
- return self._regions
- end
- if not self._has_regions then
- -- treesitter.c will default empty ranges to { -1, -1, -1, -1, -1, -1} (the full range)
- return { {} }
- end
- local regions = {} ---@type Range6[][]
- for i, _ in pairs(self._trees) do
- regions[i] = self._trees[i]:included_ranges(true)
- end
- self._regions = regions
- return regions
- end
- ---@param node TSNode
- ---@param source string|integer
- ---@param metadata vim.treesitter.query.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 or child_count == 0 then
- return { range }
- end
- local ranges = {} ---@type Range6[]
- local srow, scol, sbyte, erow, ecol, ebyte = Range.unpack6(range)
- -- We are excluding children so we need to mask out their ranges
- for i = 0, child_count - 1 do
- 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 }
- end
- srow = c_erow
- scol = c_ecol
- sbyte = c_ebyte
- end
- if erow > srow or ecol > scol then
- ranges[#ranges + 1] = Range.add_bytes(source, { srow, scol, sbyte, erow, ecol, ebyte })
- end
- return ranges
- end
- ---@nodoc
- ---@class vim.treesitter.languagetree.InjectionElem
- ---@field combined boolean
- ---@field regions Range6[][]
- ---@alias vim.treesitter.languagetree.Injection table<string,table<integer,vim.treesitter.languagetree.InjectionElem>>
- ---@param t table<integer,vim.treesitter.languagetree.Injection>
- ---@param tree_index integer
- ---@param pattern integer
- ---@param lang string
- ---@param combined boolean
- ---@param ranges Range6[]
- local function add_injection(t, tree_index, pattern, lang, combined, ranges)
- 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
- 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
- -- TODO(clason): replace by refactored `ts.has_parser` API (without side effects)
- --- 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)
- --- 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)
- -- validate that `alias` is a legal language
- if not (alias and alias:match('[%w_]+') == alias) then
- return
- end
- 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
- ---@param match table<integer,TSNode[]>
- ---@param metadata vim.treesitter.query.TSMetadata
- ---@return string?, boolean, Range6[]
- 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 (injection_lang and resolve_lang(injection_lang))
- local include_children = metadata['injection.include-children'] ~= nil
- for id, nodes in pairs(match) do
- for _, node in ipairs(nodes) do
- local name = self._injection_query.captures[id]
- -- 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:lower()) -- language names are always lower case
- elseif name == 'injection.filename' then
- local text = vim.treesitter.get_node_text(node, self._source, { metadata = metadata[id] })
- local ft = vim.filetype.match({ filename = text })
- lang = ft and resolve_lang(ft)
- elseif name == 'injection.content' then
- ranges = get_node_ranges(node, self._source, metadata[id], include_children)
- end
- end
- end
- 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 regions by language.
- ---
- --- This is where most of the injection processing occurs.
- ---
- --- TODO: Allow for an offset predicate to tailor the injection range
- --- instead of using the entire nodes range.
- --- @private
- --- @return table<string, Range6[][]>
- function LanguageTree:_get_injections()
- if not self._injection_query then
- return {}
- end
- ---@type table<integer,vim.treesitter.languagetree.Injection>
- local injections = {}
- for index, tree in pairs(self._trees) do
- local root_node = tree:root()
- local start_line, _, end_line, _ = root_node:range()
- for pattern, match, metadata in
- 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 lang then
- add_injection(injections, index, pattern, lang, combined, ranges)
- else
- self:_log('match from injection query failed for pattern', pattern)
- end
- end
- end
- ---@type table<string,Range6[][]>
- local result = {}
- -- Generate a map by lang of node lists.
- -- Each list is a set of ranges that should be parsed together.
- for _, lang_map in pairs(injections) do
- for lang, patterns in pairs(lang_map) do
- if not result[lang] then
- result[lang] = {}
- end
- for _, entry in pairs(patterns) do
- if entry.combined then
- table.insert(result[lang], combine_regions(entry.regions))
- else
- for _, ranges in pairs(entry.regions) do
- table.insert(result[lang], ranges)
- end
- end
- end
- end
- end
- return result
- end
- ---@private
- ---@param cb_name TSCallbackName
- 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
- 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 pairs(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._parser:reset()
- self._regions = nil
- local changed_range = {
- start_row,
- start_col,
- start_byte,
- end_row_old,
- end_col_old,
- end_byte_old,
- }
- -- 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
- end
- 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
- ---@nodoc
- ---@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,
- start_row,
- start_col,
- start_byte,
- old_row,
- old_col,
- old_byte,
- new_row,
- new_col,
- new_byte
- )
- 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,
- start_row,
- start_col,
- start_byte,
- old_row,
- old_col,
- old_byte,
- new_row,
- new_col,
- new_byte
- )
- -- Edit trees together BEFORE emitting a bytes callback.
- 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',
- bufnr,
- changed_tick,
- start_row,
- start_col,
- start_byte,
- old_row,
- old_col,
- old_byte,
- new_row,
- new_col,
- new_byte
- )
- end
- ---@nodoc
- function LanguageTree:_on_reload()
- self:invalidate(true)
- end
- ---@nodoc
- 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].
- ---@param cbs table<TSCallbackNameOn,function> An [nvim_buf_attach()]-like table argument with the following handlers:
- --- - `on_bytes` : see [nvim_buf_attach()].
- --- - `on_changedtree` : a callback that will be called every time the tree has syntactical changes.
- --- 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)
- if not cbs then
- return
- end
- local callbacks = recursive and self._callbacks_rec or self._callbacks
- for name, cbname in pairs(TSCallbackNames) do
- if cbs[name] then
- table.insert(callbacks[cbname], cbs[name])
- end
- end
- if recursive then
- for _, child in pairs(self._children) do
- child:register_cbs(cbs, true)
- end
- end
- end
- ---@param tree TSTree
- ---@param range Range
- ---@return boolean
- local function tree_contains(tree, range)
- local tree_ranges = tree:included_ranges(false)
- return Range.contains({
- tree_ranges[1][1],
- tree_ranges[1][2],
- tree_ranges[#tree_ranges][3],
- tree_ranges[#tree_ranges][4],
- }, range)
- end
- --- Determines whether {range} is contained in the |LanguageTree|.
- ---
- ---@param range Range4
- ---@return boolean
- function LanguageTree:contains(range)
- for _, tree in pairs(self._trees) do
- if tree_contains(tree, range) then
- return true
- end
- end
- return false
- end
- --- @class vim.treesitter.LanguageTree.tree_for_range.Opts
- --- @inlinedoc
- ---
- --- Ignore injected languages
- --- (default: `true`)
- --- @field ignore_injections? boolean
- --- Gets the tree that contains {range}.
- ---
- ---@param range Range4
- ---@param opts? vim.treesitter.LanguageTree.tree_for_range.Opts
- ---@return TSTree?
- function LanguageTree:tree_for_range(range, opts)
- opts = opts or {}
- local ignore = vim.F.if_nil(opts.ignore_injections, true)
- if not ignore then
- for _, child in pairs(self._children) do
- local tree = child:tree_for_range(range, opts)
- if tree then
- return tree
- end
- end
- end
- for _, tree in pairs(self._trees) do
- if tree_contains(tree, range) then
- return tree
- end
- end
- return nil
- end
- --- Gets the smallest node that contains {range}.
- ---
- ---@param range Range4
- ---@param opts? vim.treesitter.LanguageTree.tree_for_range.Opts
- ---@return TSNode?
- function LanguageTree:node_for_range(range, opts)
- local tree = self:tree_for_range(range, opts)
- if tree then
- return tree:root():descendant_for_range(unpack(range))
- end
- end
- --- Gets the smallest named node that contains {range}.
- ---
- ---@param range Range4
- ---@param opts? vim.treesitter.LanguageTree.tree_for_range.Opts
- ---@return TSNode?
- function LanguageTree:named_node_for_range(range, opts)
- local tree = self:tree_for_range(range, opts)
- if tree then
- return tree:root():named_descendant_for_range(unpack(range))
- end
- end
- --- Gets the appropriate language that contains {range}.
- ---
- ---@param range Range4
- ---@return vim.treesitter.LanguageTree tree Managing {range}
- function LanguageTree:language_for_range(range)
- for _, child in pairs(self._children) do
- if child:contains(range) then
- return child:language_for_range(range)
- end
- end
- return self
- end
- return LanguageTree
|