123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412 |
- local api = vim.api
- local query = vim.treesitter.query
- local Range = require('vim.treesitter._range')
- local ns = api.nvim_create_namespace('nvim.treesitter.highlighter')
- ---@alias vim.treesitter.highlighter.Iter fun(end_line: integer|nil): integer, TSNode, vim.treesitter.query.TSMetadata, TSQueryMatch
- ---@class (private) vim.treesitter.highlighter.Query
- ---@field private _query vim.treesitter.Query?
- ---@field private lang string
- ---@field private hl_cache table<integer,integer>
- local TSHighlighterQuery = {}
- TSHighlighterQuery.__index = TSHighlighterQuery
- ---@private
- ---@param lang string
- ---@param query_string string?
- ---@return vim.treesitter.highlighter.Query
- function TSHighlighterQuery.new(lang, query_string)
- local self = setmetatable({}, TSHighlighterQuery)
- self.lang = lang
- self.hl_cache = {}
- if query_string then
- self._query = query.parse(lang, query_string)
- else
- self._query = query.get(lang, 'highlights')
- end
- return self
- end
- ---@package
- ---@param capture integer
- ---@return integer?
- function TSHighlighterQuery:get_hl_from_capture(capture)
- if not self.hl_cache[capture] then
- local name = self._query.captures[capture]
- local id = 0
- if not vim.startswith(name, '_') then
- id = api.nvim_get_hl_id_by_name('@' .. name .. '.' .. self.lang)
- end
- self.hl_cache[capture] = id
- end
- return self.hl_cache[capture]
- end
- ---@nodoc
- function TSHighlighterQuery:query()
- return self._query
- end
- ---@class (private) vim.treesitter.highlighter.State
- ---@field tstree TSTree
- ---@field next_row integer
- ---@field iter vim.treesitter.highlighter.Iter?
- ---@field highlighter_query vim.treesitter.highlighter.Query
- ---@nodoc
- ---@class vim.treesitter.highlighter
- ---@field active table<integer,vim.treesitter.highlighter>
- ---@field bufnr integer
- ---@field private orig_spelloptions string
- --- A map of highlight states.
- --- This state is kept during rendering across each line update.
- ---@field private _highlight_states vim.treesitter.highlighter.State[]
- ---@field private _queries table<string,vim.treesitter.highlighter.Query>
- ---@field tree vim.treesitter.LanguageTree
- ---@field private redraw_count integer
- ---@field parsing boolean true if we are parsing asynchronously
- local TSHighlighter = {
- active = {},
- }
- TSHighlighter.__index = TSHighlighter
- ---@nodoc
- ---
- --- Creates a highlighter for `tree`.
- ---
- ---@param tree vim.treesitter.LanguageTree parser object to use for highlighting
- ---@param opts (table|nil) Configuration of the highlighter:
- --- - queries table overwrite queries used by the highlighter
- ---@return vim.treesitter.highlighter Created highlighter object
- function TSHighlighter.new(tree, opts)
- local self = setmetatable({}, TSHighlighter)
- if type(tree:source()) ~= 'number' then
- error('TSHighlighter can not be used with a string parser source.')
- end
- opts = opts or {} ---@type { queries: table<string,string> }
- self.tree = tree
- tree:register_cbs({
- on_detach = function()
- self:on_detach()
- end,
- })
- tree:register_cbs({
- on_changedtree = function(...)
- self:on_changedtree(...)
- end,
- on_child_removed = function(child)
- child:for_each_tree(function(t)
- self:on_changedtree(t:included_ranges(true))
- end)
- end,
- }, true)
- local source = tree:source()
- assert(type(source) == 'number')
- self.bufnr = source
- self.redraw_count = 0
- self._highlight_states = {}
- self._queries = {}
- -- Queries for a specific language can be overridden by a custom
- -- string query... if one is not provided it will be looked up by file.
- if opts.queries then
- for lang, query_string in pairs(opts.queries) do
- self._queries[lang] = TSHighlighterQuery.new(lang, query_string)
- end
- end
- self.orig_spelloptions = vim.bo[self.bufnr].spelloptions
- vim.bo[self.bufnr].syntax = ''
- vim.b[self.bufnr].ts_highlight = true
- TSHighlighter.active[self.bufnr] = self
- -- Tricky: if syntax hasn't been enabled, we need to reload color scheme
- -- but use synload.vim rather than syntax.vim to not enable
- -- syntax FileType autocmds. Later on we should integrate with the
- -- `:syntax` and `set syntax=...` machinery properly.
- -- Still need to ensure that syntaxset augroup exists, so that calling :destroy()
- -- immediately afterwards will not error.
- if vim.g.syntax_on ~= 1 then
- vim.cmd.runtime({ 'syntax/synload.vim', bang = true })
- vim.api.nvim_create_augroup('syntaxset', { clear = false })
- end
- vim._with({ buf = self.bufnr }, function()
- vim.opt_local.spelloptions:append('noplainbuffer')
- end)
- return self
- end
- --- @nodoc
- --- Removes all internal references to the highlighter
- function TSHighlighter:destroy()
- TSHighlighter.active[self.bufnr] = nil
- if api.nvim_buf_is_loaded(self.bufnr) then
- vim.bo[self.bufnr].spelloptions = self.orig_spelloptions
- vim.b[self.bufnr].ts_highlight = nil
- if vim.g.syntax_on == 1 then
- api.nvim_exec_autocmds('FileType', { group = 'syntaxset', buffer = self.bufnr })
- end
- end
- end
- ---@param srow integer
- ---@param erow integer exclusive
- ---@private
- function TSHighlighter:prepare_highlight_states(srow, erow)
- self._highlight_states = {}
- self.tree:for_each_tree(function(tstree, tree)
- if not tstree then
- return
- end
- local root_node = tstree:root()
- local root_start_row, _, root_end_row, _ = root_node:range()
- -- Only consider trees within the visible range
- if root_start_row > erow or root_end_row < srow then
- return
- end
- local highlighter_query = self:get_query(tree:lang())
- -- Some injected languages may not have highlight queries.
- if not highlighter_query:query() then
- return
- end
- -- _highlight_states should be a list so that the highlights are added in the same order as
- -- for_each_tree traversal. This ensures that parents' highlight don't override children's.
- table.insert(self._highlight_states, {
- tstree = tstree,
- next_row = 0,
- iter = nil,
- highlighter_query = highlighter_query,
- })
- end)
- end
- ---@param fn fun(state: vim.treesitter.highlighter.State)
- ---@package
- function TSHighlighter:for_each_highlight_state(fn)
- for _, state in ipairs(self._highlight_states) do
- fn(state)
- end
- end
- ---@package
- function TSHighlighter:on_detach()
- self:destroy()
- end
- ---@package
- ---@param changes Range6[]
- function TSHighlighter:on_changedtree(changes)
- for _, ch in ipairs(changes) do
- api.nvim__redraw({ buf = self.bufnr, range = { ch[1], ch[4] + 1 }, flush = false })
- end
- end
- --- Gets the query used for @param lang
- ---@nodoc
- ---@param lang string Language used by the highlighter.
- ---@return vim.treesitter.highlighter.Query
- function TSHighlighter:get_query(lang)
- if not self._queries[lang] then
- self._queries[lang] = TSHighlighterQuery.new(lang)
- end
- return self._queries[lang]
- end
- --- @param match TSQueryMatch
- --- @param bufnr integer
- --- @param capture integer
- --- @param metadata vim.treesitter.query.TSMetadata
- --- @return string?
- local function get_url(match, bufnr, capture, metadata)
- ---@type string|number|nil
- local url = metadata[capture] and metadata[capture].url
- if not url or type(url) == 'string' then
- return url
- end
- local captures = match:captures()
- if not captures[url] then
- return
- end
- -- Assume there is only one matching node. If there is more than one, take the URL
- -- from the first.
- local other_node = captures[url][1]
- return vim.treesitter.get_node_text(other_node, bufnr, {
- metadata = metadata[url],
- })
- end
- --- @param capture_name string
- --- @return boolean?, integer
- local function get_spell(capture_name)
- if capture_name == 'spell' then
- return true, 0
- elseif capture_name == 'nospell' then
- -- Give nospell a higher priority so it always overrides spell captures.
- return false, 1
- end
- return nil, 0
- end
- ---@param self vim.treesitter.highlighter
- ---@param buf integer
- ---@param line integer
- ---@param is_spell_nav boolean
- local function on_line_impl(self, buf, line, is_spell_nav)
- self:for_each_highlight_state(function(state)
- local root_node = state.tstree:root()
- local root_start_row, _, root_end_row, _ = root_node:range()
- -- Only consider trees that contain this line
- if root_start_row > line or root_end_row < line then
- return
- end
- if state.iter == nil or state.next_row < line then
- -- Mainly used to skip over folds
- -- TODO(lewis6991): Creating a new iterator loses the cached predicate results for query
- -- matches. Move this logic inside iter_captures() so we can maintain the cache.
- state.iter =
- state.highlighter_query:query():iter_captures(root_node, self.bufnr, line, root_end_row + 1)
- end
- local captures = state.highlighter_query:query().captures
- while line >= state.next_row do
- local capture, node, metadata, match = state.iter(line)
- local range = { root_end_row + 1, 0, root_end_row + 1, 0 }
- if node then
- range = vim.treesitter.get_range(node, buf, metadata and metadata[capture])
- end
- local start_row, start_col, end_row, end_col = Range.unpack4(range)
- if capture then
- local hl = state.highlighter_query:get_hl_from_capture(capture)
- local capture_name = captures[capture]
- local spell, spell_pri_offset = get_spell(capture_name)
- -- The "priority" attribute can be set at the pattern level or on a particular capture
- local priority = (
- tonumber(metadata.priority or metadata[capture] and metadata[capture].priority)
- or vim.hl.priorities.treesitter
- ) + spell_pri_offset
- -- The "conceal" attribute can be set at the pattern level or on a particular capture
- local conceal = metadata.conceal or metadata[capture] and metadata[capture].conceal
- local url = get_url(match, buf, capture, metadata)
- if hl and end_row >= line and (not is_spell_nav or spell ~= nil) then
- api.nvim_buf_set_extmark(buf, ns, start_row, start_col, {
- end_line = end_row,
- end_col = end_col,
- hl_group = hl,
- ephemeral = true,
- priority = priority,
- conceal = conceal,
- spell = spell,
- url = url,
- })
- end
- end
- if start_row > line then
- state.next_row = start_row
- end
- end
- end)
- end
- ---@private
- ---@param _win integer
- ---@param buf integer
- ---@param line integer
- function TSHighlighter._on_line(_, _win, buf, line, _)
- local self = TSHighlighter.active[buf]
- if not self then
- return
- end
- on_line_impl(self, buf, line, false)
- end
- ---@private
- ---@param buf integer
- ---@param srow integer
- ---@param erow integer
- function TSHighlighter._on_spell_nav(_, _, buf, srow, _, erow, _)
- local self = TSHighlighter.active[buf]
- if not self then
- return
- end
- -- Do not affect potentially populated highlight state. Here we just want a temporary
- -- empty state so the C code can detect whether the region should be spell checked.
- local highlight_states = self._highlight_states
- self:prepare_highlight_states(srow, erow)
- for row = srow, erow do
- on_line_impl(self, buf, row, true)
- end
- self._highlight_states = highlight_states
- end
- ---@private
- ---@param buf integer
- ---@param topline integer
- ---@param botline integer
- function TSHighlighter._on_win(_, _, buf, topline, botline)
- local self = TSHighlighter.active[buf]
- if not self or self.parsing then
- return false
- end
- self.parsing = self.tree:parse({ topline, botline + 1 }, function(_, trees)
- if trees and self.parsing then
- self.parsing = false
- api.nvim__redraw({ buf = buf, valid = false, flush = false })
- end
- end) == nil
- self.redraw_count = self.redraw_count + 1
- self:prepare_highlight_states(topline, botline)
- return #self._highlight_states > 0
- end
- api.nvim_set_decoration_provider(ns, {
- on_win = TSHighlighter._on_win,
- on_line = TSHighlighter._on_line,
- _on_spell_nav = TSHighlighter._on_spell_nav,
- })
- return TSHighlighter
|