mirror of
https://github.com/neovim/neovim
synced 2025-07-16 17:21:49 +00:00
fix(treesitter): redraw added/removed injections properly (#23287)
When injections are added or removed make sure to: - invoke 'changedtree' callbacks for when new trees are added. - invoke 'changedtree' callbacks for when trees are invalidated - redraw regions when languagetree children are removed
This commit is contained in:
@ -1149,21 +1149,24 @@ LanguageTree:parse({self}) *LanguageTree:parse()*
|
|||||||
Return: ~
|
Return: ~
|
||||||
TSTree[]
|
TSTree[]
|
||||||
|
|
||||||
LanguageTree:register_cbs({self}, {cbs}) *LanguageTree:register_cbs()*
|
*LanguageTree:register_cbs()*
|
||||||
|
LanguageTree:register_cbs({self}, {cbs}, {recursive})
|
||||||
Registers callbacks for the |LanguageTree|.
|
Registers callbacks for the |LanguageTree|.
|
||||||
|
|
||||||
Parameters: ~
|
Parameters: ~
|
||||||
• {cbs} (table) An |nvim_buf_attach()|-like table argument with the
|
• {cbs} (table) An |nvim_buf_attach()|-like table argument with
|
||||||
following handlers:
|
the following handlers:
|
||||||
• `on_bytes` : see |nvim_buf_attach()|, but this will be called after the parsers callback.
|
• `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
|
• `on_changedtree` : a callback that will be called
|
||||||
the tree has syntactical changes. It will only be passed one
|
every time the tree has syntactical changes. It will
|
||||||
argument, which is a table of the ranges (as node ranges)
|
only be passed one argument, which is a table of the
|
||||||
that changed.
|
ranges (as node ranges) that changed.
|
||||||
• `on_child_added` : emitted when a child is added to the
|
• `on_child_added` : emitted when a child is added to
|
||||||
tree.
|
|
||||||
• `on_child_removed` : emitted when a child is removed from
|
|
||||||
the tree.
|
the tree.
|
||||||
|
• `on_child_removed` : emitted when a child is removed
|
||||||
|
from the tree.
|
||||||
|
• {recursive?} boolean Apply callbacks recursively for all children.
|
||||||
|
Any new children will also inherit the callbacks.
|
||||||
• {self}
|
• {self}
|
||||||
|
|
||||||
LanguageTree:source({self}) *LanguageTree:source()*
|
LanguageTree:source({self}) *LanguageTree:source()*
|
||||||
|
@ -76,9 +76,6 @@ function TSHighlighter.new(tree, opts)
|
|||||||
opts = opts or {} ---@type { queries: table<string,string> }
|
opts = opts or {} ---@type { queries: table<string,string> }
|
||||||
self.tree = tree
|
self.tree = tree
|
||||||
tree:register_cbs({
|
tree:register_cbs({
|
||||||
on_changedtree = function(...)
|
|
||||||
self:on_changedtree(...)
|
|
||||||
end,
|
|
||||||
on_bytes = function(...)
|
on_bytes = function(...)
|
||||||
self:on_bytes(...)
|
self:on_bytes(...)
|
||||||
end,
|
end,
|
||||||
@ -87,6 +84,17 @@ function TSHighlighter.new(tree, opts)
|
|||||||
end,
|
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)
|
||||||
|
|
||||||
self.bufnr = tree:source() --[[@as integer]]
|
self.bufnr = tree:source() --[[@as integer]]
|
||||||
self.edit_count = 0
|
self.edit_count = 0
|
||||||
self.redraw_count = 0
|
self.redraw_count = 0
|
||||||
@ -177,10 +185,10 @@ function TSHighlighter:on_detach()
|
|||||||
end
|
end
|
||||||
|
|
||||||
---@package
|
---@package
|
||||||
---@param changes integer[][]?
|
---@param changes Range6[][]
|
||||||
function TSHighlighter:on_changedtree(changes)
|
function TSHighlighter:on_changedtree(changes)
|
||||||
for _, ch in ipairs(changes or {}) do
|
for _, ch in ipairs(changes) do
|
||||||
api.nvim__buf_redraw_range(self.bufnr, ch[1], ch[3] + 1)
|
api.nvim__buf_redraw_range(self.bufnr, ch[1], ch[4] + 1)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -51,8 +51,18 @@ local Range = require('vim.treesitter._range')
|
|||||||
---| 'on_child_added'
|
---| 'on_child_added'
|
||||||
---| 'on_child_removed'
|
---| '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',
|
||||||
|
}
|
||||||
|
|
||||||
---@class LanguageTree
|
---@class LanguageTree
|
||||||
---@field private _callbacks table<TSCallbackName,function[]> Callback handlers
|
---@field private _callbacks table<TSCallbackName,function[]> Callback handlers
|
||||||
|
---@field package _callbacks_rec table<TSCallbackName,function[]> Callback handlers (recursive)
|
||||||
---@field private _children table<string,LanguageTree> Injected languages
|
---@field private _children table<string,LanguageTree> Injected languages
|
||||||
---@field private _injection_query Query Queries defining injected languages
|
---@field private _injection_query Query Queries defining injected languages
|
||||||
---@field private _opts table Options
|
---@field private _opts table Options
|
||||||
@ -79,7 +89,7 @@ LanguageTree.__index = LanguageTree
|
|||||||
--- "injected" language parsers, which themselves may inject other languages, recursively.
|
--- "injected" language parsers, which themselves may inject other languages, recursively.
|
||||||
---
|
---
|
||||||
---@param source (integer|string) Buffer or text string to parse
|
---@param source (integer|string) Buffer or text string to parse
|
||||||
---@param lang string|nil Root language of this tree
|
---@param lang string Root language of this tree
|
||||||
---@param opts (table|nil) Optional arguments:
|
---@param opts (table|nil) Optional arguments:
|
||||||
--- - injections table Map of language to injection query strings. Overrides the
|
--- - injections table Map of language to injection query strings. Overrides the
|
||||||
--- built-in runtime file searching for language injections.
|
--- built-in runtime file searching for language injections.
|
||||||
@ -100,15 +110,15 @@ function LanguageTree.new(source, lang, opts)
|
|||||||
or query.get(lang, 'injections'),
|
or query.get(lang, 'injections'),
|
||||||
_valid = false,
|
_valid = false,
|
||||||
_parser = vim._create_ts_parser(lang),
|
_parser = vim._create_ts_parser(lang),
|
||||||
_callbacks = {
|
_callbacks = {},
|
||||||
changedtree = {},
|
_callbacks_rec = {},
|
||||||
bytes = {},
|
|
||||||
detach = {},
|
|
||||||
child_added = {},
|
|
||||||
child_removed = {},
|
|
||||||
},
|
|
||||||
}, LanguageTree)
|
}, LanguageTree)
|
||||||
|
|
||||||
|
for _, name in pairs(TSCallbackNames) do
|
||||||
|
self._callbacks[name] = {}
|
||||||
|
self._callbacks_rec[name] = {}
|
||||||
|
end
|
||||||
|
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -121,6 +131,7 @@ local function tcall(f, ...)
|
|||||||
local start = vim.loop.hrtime()
|
local start = vim.loop.hrtime()
|
||||||
---@diagnostic disable-next-line
|
---@diagnostic disable-next-line
|
||||||
local r = { f(...) }
|
local r = { f(...) }
|
||||||
|
--- @type number
|
||||||
local duration = (vim.loop.hrtime() - start) / 1000000
|
local duration = (vim.loop.hrtime() - start) / 1000000
|
||||||
return duration, unpack(r)
|
return duration, unpack(r)
|
||||||
end
|
end
|
||||||
@ -161,6 +172,9 @@ function LanguageTree:invalidate(reload)
|
|||||||
|
|
||||||
-- buffer was reloaded, reparse all trees
|
-- buffer was reloaded, reparse all trees
|
||||||
if reload then
|
if reload then
|
||||||
|
for _, t in ipairs(self._trees) do
|
||||||
|
self:_do_callback('changedtree', t:included_ranges(true), t)
|
||||||
|
end
|
||||||
self._trees = {}
|
self._trees = {}
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -245,9 +259,12 @@ function LanguageTree:parse()
|
|||||||
if not self._valid or not self._valid[i] then
|
if not self._valid or not self._valid[i] then
|
||||||
self._parser:set_included_ranges(ranges)
|
self._parser:set_included_ranges(ranges)
|
||||||
local parse_time, tree, tree_changes =
|
local parse_time, tree, tree_changes =
|
||||||
tcall(self._parser.parse, self._parser, self._trees[i], self._source)
|
tcall(self._parser.parse, self._parser, self._trees[i], self._source, true)
|
||||||
|
|
||||||
self:_do_callback('changedtree', tree_changes, tree)
|
-- Pass ranges if this is an initial parse
|
||||||
|
local cb_changes = self._trees[i] and tree_changes or ranges
|
||||||
|
|
||||||
|
self:_do_callback('changedtree', cb_changes, tree)
|
||||||
self._trees[i] = tree
|
self._trees[i] = tree
|
||||||
vim.list_extend(changes, tree_changes)
|
vim.list_extend(changes, tree_changes)
|
||||||
|
|
||||||
@ -341,7 +358,14 @@ function LanguageTree:add_child(lang)
|
|||||||
self:remove_child(lang)
|
self:remove_child(lang)
|
||||||
end
|
end
|
||||||
|
|
||||||
self._children[lang] = LanguageTree.new(self._source, lang, self._opts)
|
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
|
||||||
|
|
||||||
|
self._children[lang] = child
|
||||||
self:invalidate()
|
self:invalidate()
|
||||||
self:_do_callback('child_added', self._children[lang])
|
self:_do_callback('child_added', self._children[lang])
|
||||||
|
|
||||||
@ -453,6 +477,10 @@ function LanguageTree:set_included_regions(new_regions)
|
|||||||
end
|
end
|
||||||
|
|
||||||
if #self:included_regions() ~= #new_regions then
|
if #self:included_regions() ~= #new_regions then
|
||||||
|
-- TODO(lewis6991): inefficient; invalidate trees incrementally
|
||||||
|
for _, t in ipairs(self._trees) do
|
||||||
|
self:_do_callback('changedtree', t:included_ranges(true), t)
|
||||||
|
end
|
||||||
self._trees = {}
|
self._trees = {}
|
||||||
self:invalidate()
|
self:invalidate()
|
||||||
else
|
else
|
||||||
@ -707,6 +735,9 @@ function LanguageTree:_do_callback(cb_name, ...)
|
|||||||
for _, cb in ipairs(self._callbacks[cb_name]) do
|
for _, cb in ipairs(self._callbacks[cb_name]) do
|
||||||
cb(...)
|
cb(...)
|
||||||
end
|
end
|
||||||
|
for _, cb in ipairs(self._callbacks_rec[cb_name]) do
|
||||||
|
cb(...)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
---@package
|
---@package
|
||||||
@ -855,30 +886,26 @@ end
|
|||||||
--- changed.
|
--- changed.
|
||||||
--- - `on_child_added` : emitted when a child is added to the 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_child_removed` : emitted when a child is removed from the tree.
|
||||||
function LanguageTree:register_cbs(cbs)
|
--- @param recursive? boolean Apply callbacks recursively for all children. Any new children will
|
||||||
|
--- also inherit the callbacks.
|
||||||
|
function LanguageTree:register_cbs(cbs, recursive)
|
||||||
---@cast cbs table<TSCallbackNameOn,function>
|
---@cast cbs table<TSCallbackNameOn,function>
|
||||||
if not cbs then
|
if not cbs then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
if cbs.on_changedtree then
|
local callbacks = recursive and self._callbacks_rec or self._callbacks
|
||||||
table.insert(self._callbacks.changedtree, cbs.on_changedtree)
|
|
||||||
|
for name, cbname in pairs(TSCallbackNames) do
|
||||||
|
if cbs[name] then
|
||||||
|
table.insert(callbacks[cbname], cbs[name])
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if cbs.on_bytes then
|
if recursive then
|
||||||
table.insert(self._callbacks.bytes, cbs.on_bytes)
|
self:for_each_child(function(child)
|
||||||
end
|
child:register_cbs(cbs, true)
|
||||||
|
end)
|
||||||
if cbs.on_detach then
|
|
||||||
table.insert(self._callbacks.detach, cbs.on_detach)
|
|
||||||
end
|
|
||||||
|
|
||||||
if cbs.on_child_added then
|
|
||||||
table.insert(self._callbacks.child_added, cbs.on_child_added)
|
|
||||||
end
|
|
||||||
|
|
||||||
if cbs.on_child_removed then
|
|
||||||
table.insert(self._callbacks.child_removed, cbs.on_child_removed)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ local eq = helpers.eq
|
|||||||
|
|
||||||
before_each(clear)
|
before_each(clear)
|
||||||
|
|
||||||
local hl_query = [[
|
local hl_query_c = [[
|
||||||
(ERROR) @error
|
(ERROR) @error
|
||||||
|
|
||||||
"if" @keyword
|
"if" @keyword
|
||||||
@ -47,7 +47,7 @@ local hl_query = [[
|
|||||||
(comment) @comment
|
(comment) @comment
|
||||||
]]
|
]]
|
||||||
|
|
||||||
local hl_text = [[
|
local hl_text_c = [[
|
||||||
/// Schedule Lua callback on main loop's event queue
|
/// Schedule Lua callback on main loop's event queue
|
||||||
static int nlua_schedule(lua_State *const lstate)
|
static int nlua_schedule(lua_State *const lstate)
|
||||||
{
|
{
|
||||||
@ -64,7 +64,7 @@ static int nlua_schedule(lua_State *const lstate)
|
|||||||
return 0;
|
return 0;
|
||||||
}]]
|
}]]
|
||||||
|
|
||||||
local test_text = [[
|
local test_text_c = [[
|
||||||
void ui_refresh(void)
|
void ui_refresh(void)
|
||||||
{
|
{
|
||||||
int width = INT_MAX, height = INT_MAX;
|
int width = INT_MAX, height = INT_MAX;
|
||||||
@ -85,7 +85,7 @@ void ui_refresh(void)
|
|||||||
}
|
}
|
||||||
}]]
|
}]]
|
||||||
|
|
||||||
describe('treesitter highlighting', function()
|
describe('treesitter highlighting (C)', function()
|
||||||
local screen
|
local screen
|
||||||
|
|
||||||
before_each(function()
|
before_each(function()
|
||||||
@ -105,13 +105,13 @@ describe('treesitter highlighting', function()
|
|||||||
[11] = {foreground = Screen.colors.Cyan4};
|
[11] = {foreground = Screen.colors.Cyan4};
|
||||||
}
|
}
|
||||||
|
|
||||||
exec_lua([[ hl_query = ... ]], hl_query)
|
exec_lua([[ hl_query = ... ]], hl_query_c)
|
||||||
command [[ hi link @error ErrorMsg ]]
|
command [[ hi link @error ErrorMsg ]]
|
||||||
command [[ hi link @warning WarningMsg ]]
|
command [[ hi link @warning WarningMsg ]]
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('is updated with edits', function()
|
it('is updated with edits', function()
|
||||||
insert(hl_text)
|
insert(hl_text_c)
|
||||||
screen:expect{grid=[[
|
screen:expect{grid=[[
|
||||||
/// Schedule Lua callback on main loop's event queue |
|
/// Schedule Lua callback on main loop's event queue |
|
||||||
static int nlua_schedule(lua_State *const lstate) |
|
static int nlua_schedule(lua_State *const lstate) |
|
||||||
@ -274,7 +274,7 @@ describe('treesitter highlighting', function()
|
|||||||
end)
|
end)
|
||||||
|
|
||||||
it('is updated with :sort', function()
|
it('is updated with :sort', function()
|
||||||
insert(test_text)
|
insert(test_text_c)
|
||||||
exec_lua [[
|
exec_lua [[
|
||||||
local parser = vim.treesitter.get_parser(0, "c")
|
local parser = vim.treesitter.get_parser(0, "c")
|
||||||
test_hl = vim.treesitter.highlighter.new(parser, {queries = {c = hl_query}})
|
test_hl = vim.treesitter.highlighter.new(parser, {queries = {c = hl_query}})
|
||||||
@ -351,7 +351,7 @@ describe('treesitter highlighting', function()
|
|||||||
[1] = {bold = true, foreground = Screen.colors.SeaGreen4};
|
[1] = {bold = true, foreground = Screen.colors.SeaGreen4};
|
||||||
}
|
}
|
||||||
|
|
||||||
insert(test_text)
|
insert(test_text_c)
|
||||||
|
|
||||||
screen:expect{ grid= [[
|
screen:expect{ grid= [[
|
||||||
int width = INT_MAX, height = INT_MAX; |
|
int width = INT_MAX, height = INT_MAX; |
|
||||||
@ -510,7 +510,7 @@ describe('treesitter highlighting', function()
|
|||||||
end)
|
end)
|
||||||
|
|
||||||
it("supports highlighting with custom highlight groups", function()
|
it("supports highlighting with custom highlight groups", function()
|
||||||
insert(hl_text)
|
insert(hl_text_c)
|
||||||
|
|
||||||
exec_lua [[
|
exec_lua [[
|
||||||
local parser = vim.treesitter.get_parser(0, "c")
|
local parser = vim.treesitter.get_parser(0, "c")
|
||||||
@ -692,7 +692,7 @@ describe('treesitter highlighting', function()
|
|||||||
end)
|
end)
|
||||||
|
|
||||||
it("supports conceal attribute", function()
|
it("supports conceal attribute", function()
|
||||||
insert(hl_text)
|
insert(hl_text_c)
|
||||||
|
|
||||||
-- conceal can be empty or a single cchar.
|
-- conceal can be empty or a single cchar.
|
||||||
exec_lua [=[
|
exec_lua [=[
|
||||||
@ -753,3 +753,65 @@ describe('treesitter highlighting', function()
|
|||||||
eq(nil, get_hl"@total.nonsense.but.a.lot.of.dots")
|
eq(nil, get_hl"@total.nonsense.but.a.lot.of.dots")
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
describe('treesitter highlighting (help)', function()
|
||||||
|
local screen
|
||||||
|
|
||||||
|
before_each(function()
|
||||||
|
screen = Screen.new(40, 6)
|
||||||
|
screen:attach()
|
||||||
|
screen:set_default_attr_ids {
|
||||||
|
[1] = {foreground = Screen.colors.Blue1};
|
||||||
|
[2] = {bold = true, foreground = Screen.colors.Blue1};
|
||||||
|
[3] = {bold = true, foreground = Screen.colors.Brown};
|
||||||
|
[4] = {foreground = Screen.colors.Cyan4};
|
||||||
|
[5] = {foreground = Screen.colors.Magenta1};
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("correctly redraws added/removed injections", function()
|
||||||
|
insert[[
|
||||||
|
>ruby
|
||||||
|
-- comment
|
||||||
|
local this_is = 'actually_lua'
|
||||||
|
<
|
||||||
|
]]
|
||||||
|
|
||||||
|
exec_lua [[
|
||||||
|
vim.bo.filetype = 'help'
|
||||||
|
vim.treesitter.start()
|
||||||
|
]]
|
||||||
|
|
||||||
|
screen:expect{grid=[[
|
||||||
|
{1:>ruby} |
|
||||||
|
{1: -- comment} |
|
||||||
|
{1: local this_is = 'actually_lua'} |
|
||||||
|
< |
|
||||||
|
^ |
|
||||||
|
|
|
||||||
|
]]}
|
||||||
|
|
||||||
|
helpers.curbufmeths.set_text(0, 1, 0, 5, {'lua'})
|
||||||
|
|
||||||
|
screen:expect{grid=[[
|
||||||
|
{1:>lua} |
|
||||||
|
{1: -- comment} |
|
||||||
|
{1: }{3:local}{1: }{4:this_is}{1: }{3:=}{1: }{5:'actually_lua'} |
|
||||||
|
< |
|
||||||
|
^ |
|
||||||
|
|
|
||||||
|
]]}
|
||||||
|
|
||||||
|
helpers.curbufmeths.set_text(0, 1, 0, 4, {'ruby'})
|
||||||
|
|
||||||
|
screen:expect{grid=[[
|
||||||
|
{1:>ruby} |
|
||||||
|
{1: -- comment} |
|
||||||
|
{1: local this_is = 'actually_lua'} |
|
||||||
|
< |
|
||||||
|
^ |
|
||||||
|
|
|
||||||
|
]]}
|
||||||
|
end)
|
||||||
|
|
||||||
|
end)
|
||||||
|
Reference in New Issue
Block a user