fix(treesitter): highlight injections properly

`on_line_impl` doesn't highlight single lines, so using pattern indexes
to offset priority doesn't work.
This commit is contained in:
Lewis Russell 2024-03-13 14:40:41 +00:00 committed by Lewis Russell
parent 274e414c94
commit 12faaf40f4
3 changed files with 56 additions and 20 deletions

View File

@ -57,6 +57,7 @@ end
---@field next_row integer ---@field next_row integer
---@field iter vim.treesitter.highlighter.Iter? ---@field iter vim.treesitter.highlighter.Iter?
---@field highlighter_query vim.treesitter.highlighter.Query ---@field highlighter_query vim.treesitter.highlighter.Query
---@field level integer Injection level
---@nodoc ---@nodoc
---@class vim.treesitter.highlighter ---@class vim.treesitter.highlighter
@ -192,12 +193,20 @@ function TSHighlighter:prepare_highlight_states(srow, erow)
return return
end end
local level = 0
local t = tree
while t do
t = t:parent()
level = level + 1
end
-- _highlight_states should be a list so that the highlights are added in the same order as -- _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. -- for_each_tree traversal. This ensures that parents' highlight don't override children's.
table.insert(self._highlight_states, { table.insert(self._highlight_states, {
tstree = tstree, tstree = tstree,
next_row = 0, next_row = 0,
iter = nil, iter = nil,
level = level,
highlighter_query = highlighter_query, highlighter_query = highlighter_query,
}) })
end) end)
@ -248,14 +257,10 @@ end
---@param line integer ---@param line integer
---@param is_spell_nav boolean ---@param is_spell_nav boolean
local function on_line_impl(self, buf, line, is_spell_nav) local function on_line_impl(self, buf, line, is_spell_nav)
-- Track the maximum pattern index encountered in each tree. For subsequent
-- trees, the subpriority passed to nvim_buf_set_extmark is offset by the
-- largest pattern index from the prior tree. This ensures that extmarks
-- from subsequent trees always appear "on top of" extmarks from previous
-- trees (e.g. injections should always appear over base highlights).
local pattern_offset = 0
self:for_each_highlight_state(function(state) self:for_each_highlight_state(function(state)
-- Use the injection level to offset the subpriority passed to nvim_buf_set_extmark
-- so injections always appear over base highlights.
local pattern_offset = state.level * 1000
local root_node = state.tstree:root() local root_node = state.tstree:root()
local root_start_row, _, root_end_row, _ = root_node:range() local root_start_row, _, root_end_row, _ = root_node:range()
@ -270,14 +275,9 @@ local function on_line_impl(self, buf, line, is_spell_nav)
:iter_matches(root_node, self.bufnr, line, root_end_row + 1, { all = true }) :iter_matches(root_node, self.bufnr, line, root_end_row + 1, { all = true })
end end
local max_pattern_index = 0
while line >= state.next_row do while line >= state.next_row do
local pattern, match, metadata = state.iter() local pattern, match, metadata = state.iter()
if pattern and pattern > max_pattern_index then
max_pattern_index = pattern
end
if not match then if not match then
state.next_row = root_end_row + 1 state.next_row = root_end_row + 1
end end
@ -343,8 +343,6 @@ local function on_line_impl(self, buf, line, is_spell_nav)
end end
end end
end end
pattern_offset = pattern_offset + max_pattern_index
end) end)
end end

View File

@ -81,7 +81,7 @@ local TSCallbackNames = {
---List of regions this tree should manage and parse. If nil then regions are ---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() ---taken from _trees. This is mostly a short-lived cache for included_regions()
---@field private _lang string Language name ---@field private _lang string Language name
---@field private _parent_lang? string Parent language name ---@field private _parent? vim.treesitter.LanguageTree Parent LanguageTree
---@field private _source (integer|string) Buffer or string to parse ---@field private _source (integer|string) Buffer or string to parse
---@field private _trees table<integer, TSTree> Reference to parsed tree (one for each language). ---@field private _trees table<integer, TSTree> Reference to parsed tree (one for each language).
---Each key is the index of region, which is synced with _regions and _valid. ---Each key is the index of region, which is synced with _regions and _valid.
@ -106,9 +106,8 @@ LanguageTree.__index = LanguageTree
---@param source (integer|string) Buffer or text string to parse ---@param source (integer|string) Buffer or text string to parse
---@param lang string Root language of this tree ---@param lang string Root language of this tree
---@param opts vim.treesitter.LanguageTree.new.Opts? ---@param opts vim.treesitter.LanguageTree.new.Opts?
---@param parent_lang? string Parent language name of this tree
---@return vim.treesitter.LanguageTree parser object ---@return vim.treesitter.LanguageTree parser object
function LanguageTree.new(source, lang, opts, parent_lang) function LanguageTree.new(source, lang, opts)
language.add(lang) language.add(lang)
opts = opts or {} opts = opts or {}
@ -122,7 +121,6 @@ function LanguageTree.new(source, lang, opts, parent_lang)
local self = { local self = {
_source = source, _source = source,
_lang = lang, _lang = lang,
_parent_lang = parent_lang,
_children = {}, _children = {},
_trees = {}, _trees = {},
_opts = opts, _opts = opts,
@ -505,19 +503,25 @@ function LanguageTree:add_child(lang)
self:remove_child(lang) self:remove_child(lang)
end end
local child = LanguageTree.new(self._source, lang, self._opts, self:lang()) local child = LanguageTree.new(self._source, lang, self._opts)
-- Inherit recursive callbacks -- Inherit recursive callbacks
for nm, cb in pairs(self._callbacks_rec) do for nm, cb in pairs(self._callbacks_rec) do
vim.list_extend(child._callbacks_rec[nm], cb) vim.list_extend(child._callbacks_rec[nm], cb)
end end
child._parent = self
self._children[lang] = child self._children[lang] = child
self:_do_callback('child_added', self._children[lang]) self:_do_callback('child_added', self._children[lang])
return self._children[lang] return self._children[lang]
end end
--- @package
function LanguageTree:parent()
return self._parent
end
--- Removes a child language from this |LanguageTree|. --- Removes a child language from this |LanguageTree|.
--- ---
---@private ---@private
@ -792,7 +796,7 @@ function LanguageTree:_get_injection(match, metadata)
local combined = metadata['injection.combined'] ~= nil local combined = metadata['injection.combined'] ~= nil
local injection_lang = metadata['injection.language'] --[[@as string?]] local injection_lang = metadata['injection.language'] --[[@as string?]]
local lang = metadata['injection.self'] ~= nil and self:lang() local lang = metadata['injection.self'] ~= nil and self:lang()
or metadata['injection.parent'] ~= nil and self._parent_lang or metadata['injection.parent'] ~= nil and self._parent
or (injection_lang and resolve_lang(injection_lang)) or (injection_lang and resolve_lang(injection_lang))
local include_children = metadata['injection.include-children'] ~= nil local include_children = metadata['injection.include-children'] ~= nil

View File

@ -867,6 +867,40 @@ describe('treesitter highlighting (help)', function()
]], ]],
} }
end) end)
it('correctly redraws injections subpriorities', function()
-- The top level string node will be highlighted first
-- with an extmark spanning multiple lines.
-- When the next line is drawn, which includes an injection,
-- make sure the highlight appears above the base tree highlight
insert([=[
local s = [[
local also = lua
]]
]=])
exec_lua [[
parser = vim.treesitter.get_parser(0, "lua", {
injections = {
lua = '(string content: (_) @injection.content (#set! injection.language lua))'
}
})
vim.treesitter.highlighter.new(parser)
]]
screen:expect {
grid = [=[
{3:local} {4:s} {3:=} {5:[[} |
{5: }{3:local}{5: }{4:also}{5: }{3:=}{5: }{4:lua} |
{5:]]} |
^ |
{2:~ }|
|
]=],
}
end)
end) end)
describe('treesitter highlighting (nested injections)', function() describe('treesitter highlighting (nested injections)', function()