mirror of
https://github.com/neovim/neovim.git
synced 2026-08-27 01:21:48 +00:00
fix(treesitter): make TSHighlighter.new() idempotent for an active buffer #41090
Problem: Calling vim.treesitter.start() a second time on a buffer that already has an active TSHighlighter creates a brand new instance instead of reusing it, whether the parser tree is unchanged (e.g. calling start() twice) or different (e.g. switching languages). Either way the old instance is silently discarded without calling :destroy() on it, so its on_bytes/on_changedtree/on_detach callbacks stay registered and its buffer-local state (spelloptions, decoration namespace) is never restored, both leaking indefinitely for an orphaned instance that nothing references anymore. Solution: Return the existing instance when TSHighlighter.active[source] is already set for the same parser tree, instead of unconditionally constructing a new one. When the tree differs instead (e.g. a language switch), destroy() the old instance first, matching stop() semantics, before constructing the new one.
This commit is contained in:
@@ -90,12 +90,26 @@ TSHighlighter.__index = TSHighlighter
|
||||
--- - 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
|
||||
local source = tree:source()
|
||||
if type(source) ~= 'number' then
|
||||
error('TSHighlighter can not be used with a string parser source.')
|
||||
end
|
||||
|
||||
-- Calling start() again on an already-highlighted buffer must be a no-op: a second instance
|
||||
-- would register duplicate on_bytes/on_changedtree/on_detach callbacks that destroy() never
|
||||
-- unregisters, leaking callbacks that keep firing for the orphaned instance.
|
||||
local existing = TSHighlighter.active[source]
|
||||
if existing then
|
||||
if existing.tree == tree then
|
||||
return existing
|
||||
end
|
||||
-- Different tree (e.g. a language switch): the old instance would otherwise be silently
|
||||
-- orphaned with the same leaked callbacks, so destroy() it first, matching stop() semantics.
|
||||
existing:destroy()
|
||||
end
|
||||
|
||||
local self = setmetatable({}, TSHighlighter)
|
||||
|
||||
opts = opts or {} ---@type { queries: table<string,string> }
|
||||
self.tree = tree
|
||||
tree:register_cbs({
|
||||
@@ -136,9 +150,6 @@ function TSHighlighter.new(tree, opts)
|
||||
end,
|
||||
}, true)
|
||||
|
||||
local source = tree:source()
|
||||
assert(type(source) == 'number')
|
||||
|
||||
self.bufnr = source
|
||||
self.redraw_count = 0
|
||||
self._conceal_checked = {}
|
||||
|
||||
@@ -182,6 +182,19 @@ describe('treesitter highlighting (C)', function()
|
||||
-- treesitter highlighting is used
|
||||
screen:expect(hl_grid_ts_c)
|
||||
|
||||
-- A second start() on an already-highlighted buffer is a no-op: it returns the existing
|
||||
-- highlighter instead of replacing it (replacing it would leak the old instance's
|
||||
-- on_bytes/on_changedtree callbacks, since destroy() is never called on it).
|
||||
eq(
|
||||
true,
|
||||
exec_lua(function()
|
||||
local buf = vim.api.nvim_get_current_buf()
|
||||
local first = vim.treesitter.highlighter.active[buf]
|
||||
vim.treesitter.start()
|
||||
return first == vim.treesitter.highlighter.active[buf]
|
||||
end)
|
||||
)
|
||||
|
||||
exec_lua(function()
|
||||
vim.treesitter.stop()
|
||||
end)
|
||||
|
||||
Reference in New Issue
Block a user