mirror of
https://github.com/neovim/neovim
synced 2025-07-15 16:51:49 +00:00
see0887ad1cbb
for the original commit on master, and80bbba94d6
for the backport on the branch, which forgot to backup the original setting value.
296 lines
7.5 KiB
Lua
296 lines
7.5 KiB
Lua
local a = vim.api
|
|
local query = require('vim.treesitter.query')
|
|
|
|
-- support reload for quick experimentation
|
|
---@class TSHighlighter
|
|
local TSHighlighter = rawget(vim.treesitter, 'TSHighlighter') or {}
|
|
TSHighlighter.__index = TSHighlighter
|
|
|
|
TSHighlighter.active = TSHighlighter.active or {}
|
|
|
|
local TSHighlighterQuery = {}
|
|
TSHighlighterQuery.__index = TSHighlighterQuery
|
|
|
|
local ns = a.nvim_create_namespace('treesitter/highlighter')
|
|
|
|
---@private
|
|
function TSHighlighterQuery.new(lang, query_string)
|
|
local self = setmetatable({}, { __index = TSHighlighterQuery })
|
|
|
|
self.hl_cache = setmetatable({}, {
|
|
__index = function(table, capture)
|
|
local name = self._query.captures[capture]
|
|
local id = 0
|
|
if not vim.startswith(name, '_') then
|
|
id = a.nvim_get_hl_id_by_name('@' .. name .. '.' .. lang)
|
|
end
|
|
|
|
rawset(table, capture, id)
|
|
return id
|
|
end,
|
|
})
|
|
|
|
if query_string then
|
|
self._query = query.parse_query(lang, query_string)
|
|
else
|
|
self._query = query.get_query(lang, 'highlights')
|
|
end
|
|
|
|
return self
|
|
end
|
|
|
|
---@private
|
|
function TSHighlighterQuery:query()
|
|
return self._query
|
|
end
|
|
|
|
--- Creates a new highlighter using @param tree
|
|
---
|
|
---@param tree LanguageTree |LanguageTree| parser object to use for highlighting
|
|
---@param opts (table|nil) Configuration of the highlighter:
|
|
--- - queries table overwrite queries used by the highlighter
|
|
---@return TSHighlighter Created highlighter object
|
|
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 {}
|
|
self.tree = tree
|
|
tree:register_cbs({
|
|
on_changedtree = function(...)
|
|
self:on_changedtree(...)
|
|
end,
|
|
on_bytes = function(...)
|
|
self:on_bytes(...)
|
|
end,
|
|
on_detach = function(...)
|
|
self:on_detach(...)
|
|
end,
|
|
})
|
|
|
|
self.bufnr = tree:source()
|
|
self.edit_count = 0
|
|
self.redraw_count = 0
|
|
self.line_count = {}
|
|
-- A map of highlight states.
|
|
-- This state is kept during rendering across each line update.
|
|
self._highlight_states = {}
|
|
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.
|
|
if vim.g.syntax_on ~= 1 then
|
|
vim.cmd.runtime({ 'syntax/synload.vim', bang = true })
|
|
end
|
|
|
|
a.nvim_buf_call(self.bufnr, function()
|
|
vim.opt_local.spelloptions:append('noplainbuffer')
|
|
end)
|
|
|
|
self.tree:parse()
|
|
|
|
return self
|
|
end
|
|
|
|
--- Removes all internal references to the highlighter
|
|
function TSHighlighter:destroy()
|
|
if TSHighlighter.active[self.bufnr] then
|
|
TSHighlighter.active[self.bufnr] = nil
|
|
end
|
|
|
|
if vim.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
|
|
a.nvim_exec_autocmds('FileType', { group = 'syntaxset', buffer = self.bufnr })
|
|
end
|
|
end
|
|
end
|
|
|
|
---@private
|
|
function TSHighlighter:get_highlight_state(tstree)
|
|
if not self._highlight_states[tstree] then
|
|
self._highlight_states[tstree] = {
|
|
next_row = 0,
|
|
iter = nil,
|
|
}
|
|
end
|
|
|
|
return self._highlight_states[tstree]
|
|
end
|
|
|
|
---@private
|
|
function TSHighlighter:reset_highlight_state()
|
|
self._highlight_states = {}
|
|
end
|
|
|
|
---@private
|
|
function TSHighlighter:on_bytes(_, _, start_row, _, _, _, _, _, new_end)
|
|
a.nvim__buf_redraw_range(self.bufnr, start_row, start_row + new_end + 1)
|
|
end
|
|
|
|
---@private
|
|
function TSHighlighter:on_detach()
|
|
self:destroy()
|
|
end
|
|
|
|
---@private
|
|
function TSHighlighter:on_changedtree(changes)
|
|
for _, ch in ipairs(changes or {}) do
|
|
a.nvim__buf_redraw_range(self.bufnr, ch[1], ch[3] + 1)
|
|
end
|
|
end
|
|
|
|
--- Gets the query used for @param lang
|
|
--
|
|
---@private
|
|
---@param lang string Language used by the highlighter.
|
|
---@return Query
|
|
function TSHighlighter:get_query(lang)
|
|
if not self._queries[lang] then
|
|
self._queries[lang] = TSHighlighterQuery.new(lang)
|
|
end
|
|
|
|
return self._queries[lang]
|
|
end
|
|
|
|
---@private
|
|
local function on_line_impl(self, buf, line, is_spell_nav)
|
|
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 worry about trees within the line range
|
|
if root_start_row > line or root_end_row < line then
|
|
return
|
|
end
|
|
|
|
local state = self:get_highlight_state(tstree)
|
|
local highlighter_query = self:get_query(tree:lang())
|
|
|
|
-- Some injected languages may not have highlight queries.
|
|
if not highlighter_query:query() then
|
|
return
|
|
end
|
|
|
|
if state.iter == nil or state.next_row < line then
|
|
state.iter =
|
|
highlighter_query:query():iter_captures(root_node, self.bufnr, line, root_end_row + 1)
|
|
end
|
|
|
|
while line >= state.next_row do
|
|
local capture, node, metadata = state.iter()
|
|
|
|
if capture == nil then
|
|
break
|
|
end
|
|
|
|
local start_row, start_col, end_row, end_col = node:range()
|
|
local hl = highlighter_query.hl_cache[capture]
|
|
|
|
local capture_name = highlighter_query:query().captures[capture]
|
|
local spell = nil
|
|
if capture_name == 'spell' then
|
|
spell = true
|
|
elseif capture_name == 'nospell' then
|
|
spell = false
|
|
end
|
|
|
|
-- Give nospell a higher priority so it always overrides spell captures.
|
|
local spell_pri_offset = capture_name == 'nospell' and 1 or 0
|
|
|
|
if hl and end_row >= line and (not is_spell_nav or spell ~= nil) then
|
|
a.nvim_buf_set_extmark(buf, ns, start_row, start_col, {
|
|
end_line = end_row,
|
|
end_col = end_col,
|
|
hl_group = hl,
|
|
ephemeral = true,
|
|
priority = (tonumber(metadata.priority) or 100) + spell_pri_offset, -- Low but leaves room below
|
|
conceal = metadata.conceal,
|
|
spell = spell,
|
|
})
|
|
end
|
|
if start_row > line then
|
|
state.next_row = start_row
|
|
end
|
|
end
|
|
end, true)
|
|
end
|
|
|
|
---@private
|
|
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
|
|
function TSHighlighter._on_spell_nav(_, _, buf, srow, _, erow, _)
|
|
local self = TSHighlighter.active[buf]
|
|
if not self then
|
|
return
|
|
end
|
|
|
|
self:reset_highlight_state()
|
|
|
|
for row = srow, erow do
|
|
on_line_impl(self, buf, row, true)
|
|
end
|
|
end
|
|
|
|
---@private
|
|
function TSHighlighter._on_buf(_, buf)
|
|
local self = TSHighlighter.active[buf]
|
|
if self then
|
|
self.tree:parse()
|
|
end
|
|
end
|
|
|
|
---@private
|
|
function TSHighlighter._on_win(_, _win, buf, _topline)
|
|
local self = TSHighlighter.active[buf]
|
|
if not self then
|
|
return false
|
|
end
|
|
|
|
self:reset_highlight_state()
|
|
self.redraw_count = self.redraw_count + 1
|
|
return true
|
|
end
|
|
|
|
a.nvim_set_decoration_provider(ns, {
|
|
on_buf = TSHighlighter._on_buf,
|
|
on_win = TSHighlighter._on_win,
|
|
on_line = TSHighlighter._on_line,
|
|
_on_spell_nav = TSHighlighter._on_spell_nav,
|
|
})
|
|
|
|
return TSHighlighter
|