mirror of
				https://github.com/neovim/neovim.git
				synced 2025-10-26 12:27:24 +00:00 
			
		
		
		
	 a664246171
			
		
	
	a664246171
	
	
	
		
			
			Remove following functions: - vim.lsp.util.extract_completion_items - vim.lsp.util.get_progress_messages - vim.lsp.util.parse_snippet() - vim.lsp.util.text_document_completion_list_to_complete_items - LanguageTree:for_each_child - health#report_error - health#report_info - health#report_ok - health#report_start - health#report_warn - vim.health.report_error - vim.health.report_info - vim.health.report_ok - vim.health.report_start - vim.health.report_warn
		
			
				
	
	
		
			1163 lines
		
	
	
		
			33 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
			
		
		
	
	
			1163 lines
		
	
	
		
			33 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
| --- @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')
 | |
| 
 | |
| ---@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
 | |
| ---@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
 | |
| 
 | |
| --- @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.
 | |
| ---
 | |
| ---@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)
 | |
|   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),
 | |
|     _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
 | |
| 
 | |
|   -- 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?
 | |
| --- @return Range6[] 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
 | |
| 
 | |
|   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)
 | |
|       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<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
 | |
| 
 | |
| --- 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 table<integer, 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 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
 | |
|     child:parse(range)
 | |
|   end
 | |
| 
 | |
|   return self._trees
 | |
| 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.
 | |
| ---@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 registering)
 | |
| --- 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,
 | |
|         { all = true }
 | |
|       )
 | |
|     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._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
 | |
| 
 | |
| ---@package
 | |
| ---@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
 | |
| 
 | |
| ---@package
 | |
| function LanguageTree:_on_reload()
 | |
|   self:invalidate(true)
 | |
| end
 | |
| 
 | |
| ---@package
 | |
| 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()], 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 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 `{ start_line, start_col, end_line, end_col }`
 | |
| ---@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 `{ start_line, start_col, end_line, end_col }`
 | |
| ---@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 named node that contains {range}.
 | |
| ---
 | |
| ---@param range Range4 `{ start_line, start_col, end_line, end_col }`
 | |
| ---@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 `{ start_line, start_col, end_line, end_col }`
 | |
| ---@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
 |