mirror of
				https://github.com/neovim/neovim.git
				synced 2025-11-04 09:44:31 +00:00 
			
		
		
		
	fix(treesitter): scope highlight state per window
**Problem:** There is a lot of distracting highlight flickering when editing a buffer with multiple open windows. This is because the parsing/highlighting state is shared across all windows. **Solution:** Greatly reduce flicker in window splits by scoping the highlighter state object and the `parsing` state object to each individual window, so there is no cross-window interference.
This commit is contained in:
		
				
					committed by
					
						
						Christian Clason
					
				
			
			
				
	
			
			
			
						parent
						
							8d3b7b57c8
						
					
				
				
					commit
					8183eb32e1
				
			@@ -63,15 +63,16 @@ end
 | 
				
			|||||||
---@field active table<integer,vim.treesitter.highlighter>
 | 
					---@field active table<integer,vim.treesitter.highlighter>
 | 
				
			||||||
---@field bufnr integer
 | 
					---@field bufnr integer
 | 
				
			||||||
---@field private orig_spelloptions string
 | 
					---@field private orig_spelloptions string
 | 
				
			||||||
--- A map of highlight states.
 | 
					--- A map from window ID to highlight states.
 | 
				
			||||||
--- This state is kept during rendering across each line update.
 | 
					--- This state is kept during rendering across each line update.
 | 
				
			||||||
---@field private _highlight_states vim.treesitter.highlighter.State[]
 | 
					---@field private _highlight_states table<integer, vim.treesitter.highlighter.State[]>
 | 
				
			||||||
---@field private _queries table<string,vim.treesitter.highlighter.Query>
 | 
					---@field private _queries table<string,vim.treesitter.highlighter.Query>
 | 
				
			||||||
---@field  _conceal_line boolean?
 | 
					---@field  _conceal_line boolean?
 | 
				
			||||||
---@field  _conceal_checked table<integer, boolean>
 | 
					---@field  _conceal_checked table<integer, boolean>
 | 
				
			||||||
---@field tree vim.treesitter.LanguageTree
 | 
					---@field tree vim.treesitter.LanguageTree
 | 
				
			||||||
---@field private redraw_count integer
 | 
					---@field private redraw_count integer
 | 
				
			||||||
---@field parsing boolean true if we are parsing asynchronously
 | 
					--- A map from window ID to whether we are currently parsing that window asynchronously
 | 
				
			||||||
 | 
					---@field parsing table<integer, boolean>
 | 
				
			||||||
local TSHighlighter = {
 | 
					local TSHighlighter = {
 | 
				
			||||||
  active = {},
 | 
					  active = {},
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -132,6 +133,7 @@ function TSHighlighter.new(tree, opts)
 | 
				
			|||||||
  self._conceal_checked = {}
 | 
					  self._conceal_checked = {}
 | 
				
			||||||
  self._queries = {}
 | 
					  self._queries = {}
 | 
				
			||||||
  self._highlight_states = {}
 | 
					  self._highlight_states = {}
 | 
				
			||||||
 | 
					  self.parsing = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  -- Queries for a specific language can be overridden by a custom
 | 
					  -- 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.
 | 
					  -- string query... if one is not provided it will be looked up by file.
 | 
				
			||||||
@@ -185,11 +187,12 @@ function TSHighlighter:destroy()
 | 
				
			|||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					---@param win integer
 | 
				
			||||||
---@param srow integer
 | 
					---@param srow integer
 | 
				
			||||||
---@param erow integer exclusive
 | 
					---@param erow integer exclusive
 | 
				
			||||||
---@private
 | 
					---@private
 | 
				
			||||||
function TSHighlighter:prepare_highlight_states(srow, erow)
 | 
					function TSHighlighter:prepare_highlight_states(win, srow, erow)
 | 
				
			||||||
  self._highlight_states = {}
 | 
					  self._highlight_states[win] = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  self.tree:for_each_tree(function(tstree, tree)
 | 
					  self.tree:for_each_tree(function(tstree, tree)
 | 
				
			||||||
    if not tstree then
 | 
					    if not tstree then
 | 
				
			||||||
@@ -212,7 +215,7 @@ function TSHighlighter:prepare_highlight_states(srow, erow)
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    -- _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[win], {
 | 
				
			||||||
      tstree = tstree,
 | 
					      tstree = tstree,
 | 
				
			||||||
      next_row = 0,
 | 
					      next_row = 0,
 | 
				
			||||||
      iter = nil,
 | 
					      iter = nil,
 | 
				
			||||||
@@ -221,10 +224,11 @@ function TSHighlighter:prepare_highlight_states(srow, erow)
 | 
				
			|||||||
  end)
 | 
					  end)
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					---@param win integer
 | 
				
			||||||
---@param fn fun(state: vim.treesitter.highlighter.State)
 | 
					---@param fn fun(state: vim.treesitter.highlighter.State)
 | 
				
			||||||
---@package
 | 
					---@package
 | 
				
			||||||
function TSHighlighter:for_each_highlight_state(fn)
 | 
					function TSHighlighter:for_each_highlight_state(win, fn)
 | 
				
			||||||
  for _, state in ipairs(self._highlight_states) do
 | 
					  for _, state in ipairs(self._highlight_states[win] or {}) do
 | 
				
			||||||
    fn(state)
 | 
					    fn(state)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
@@ -308,13 +312,14 @@ local function get_spell(capture_name)
 | 
				
			|||||||
end
 | 
					end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
---@param self vim.treesitter.highlighter
 | 
					---@param self vim.treesitter.highlighter
 | 
				
			||||||
 | 
					---@param win integer
 | 
				
			||||||
---@param buf integer
 | 
					---@param buf integer
 | 
				
			||||||
---@param line integer
 | 
					---@param line integer
 | 
				
			||||||
---@param on_spell boolean
 | 
					---@param on_spell boolean
 | 
				
			||||||
---@param on_conceal boolean
 | 
					---@param on_conceal boolean
 | 
				
			||||||
local function on_line_impl(self, buf, line, on_spell, on_conceal)
 | 
					local function on_line_impl(self, win, buf, line, on_spell, on_conceal)
 | 
				
			||||||
  self._conceal_checked[line] = true
 | 
					  self._conceal_checked[line] = true
 | 
				
			||||||
  self:for_each_highlight_state(function(state)
 | 
					  self:for_each_highlight_state(win, function(state)
 | 
				
			||||||
    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()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -394,23 +399,24 @@ local function on_line_impl(self, buf, line, on_spell, on_conceal)
 | 
				
			|||||||
end
 | 
					end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
---@private
 | 
					---@private
 | 
				
			||||||
---@param _win integer
 | 
					---@param win integer
 | 
				
			||||||
---@param buf integer
 | 
					---@param buf integer
 | 
				
			||||||
---@param line integer
 | 
					---@param line integer
 | 
				
			||||||
function TSHighlighter._on_line(_, _win, buf, line, _)
 | 
					function TSHighlighter._on_line(_, win, buf, line, _)
 | 
				
			||||||
  local self = TSHighlighter.active[buf]
 | 
					  local self = TSHighlighter.active[buf]
 | 
				
			||||||
  if not self then
 | 
					  if not self then
 | 
				
			||||||
    return
 | 
					    return
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  on_line_impl(self, buf, line, false, false)
 | 
					  on_line_impl(self, win, buf, line, false, false)
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
---@private
 | 
					---@private
 | 
				
			||||||
 | 
					---@param win integer
 | 
				
			||||||
---@param buf integer
 | 
					---@param buf integer
 | 
				
			||||||
---@param srow integer
 | 
					---@param srow integer
 | 
				
			||||||
---@param erow integer
 | 
					---@param erow integer
 | 
				
			||||||
function TSHighlighter._on_spell_nav(_, _, buf, srow, _, erow, _)
 | 
					function TSHighlighter._on_spell_nav(_, win, buf, srow, _, erow, _)
 | 
				
			||||||
  local self = TSHighlighter.active[buf]
 | 
					  local self = TSHighlighter.active[buf]
 | 
				
			||||||
  if not self then
 | 
					  if not self then
 | 
				
			||||||
    return
 | 
					    return
 | 
				
			||||||
@@ -418,30 +424,31 @@ function TSHighlighter._on_spell_nav(_, _, buf, srow, _, erow, _)
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  -- Do not affect potentially populated highlight state. Here we just want a temporary
 | 
					  -- 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.
 | 
					  -- empty state so the C code can detect whether the region should be spell checked.
 | 
				
			||||||
  local highlight_states = self._highlight_states
 | 
					  local highlight_states = self._highlight_states[win]
 | 
				
			||||||
  self:prepare_highlight_states(srow, erow)
 | 
					  self:prepare_highlight_states(win, srow, erow)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  for row = srow, erow do
 | 
					  for row = srow, erow do
 | 
				
			||||||
    on_line_impl(self, buf, row, true, false)
 | 
					    on_line_impl(self, win, buf, row, true, false)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
  self._highlight_states = highlight_states
 | 
					  self._highlight_states[win] = highlight_states
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
---@private
 | 
					---@private
 | 
				
			||||||
 | 
					---@param win integer
 | 
				
			||||||
---@param buf integer
 | 
					---@param buf integer
 | 
				
			||||||
---@param row integer
 | 
					---@param row integer
 | 
				
			||||||
function TSHighlighter._on_conceal_line(_, _, buf, row)
 | 
					function TSHighlighter._on_conceal_line(_, win, buf, row)
 | 
				
			||||||
  local self = TSHighlighter.active[buf]
 | 
					  local self = TSHighlighter.active[buf]
 | 
				
			||||||
  if not self or not self._conceal_line or self._conceal_checked[row] then
 | 
					  if not self or not self._conceal_line or self._conceal_checked[row] then
 | 
				
			||||||
    return
 | 
					    return
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  -- Do not affect potentially populated highlight state.
 | 
					  -- Do not affect potentially populated highlight state.
 | 
				
			||||||
  local highlight_states = self._highlight_states
 | 
					  local highlight_states = self._highlight_states[win]
 | 
				
			||||||
  self.tree:parse({ row, row })
 | 
					  self.tree:parse({ row, row })
 | 
				
			||||||
  self:prepare_highlight_states(row, row)
 | 
					  self:prepare_highlight_states(win, row, row)
 | 
				
			||||||
  on_line_impl(self, buf, row, false, true)
 | 
					  on_line_impl(self, win, buf, row, false, true)
 | 
				
			||||||
  self._highlight_states = highlight_states
 | 
					  self._highlight_states[win] = highlight_states
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
---@private
 | 
					---@private
 | 
				
			||||||
@@ -466,33 +473,31 @@ function TSHighlighter._on_win(_, win, buf, topline, botline)
 | 
				
			|||||||
  if not self then
 | 
					  if not self then
 | 
				
			||||||
    return false
 | 
					    return false
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
  self.parsing = self.parsing
 | 
					  self.parsing[win] = self.parsing[win]
 | 
				
			||||||
    or nil
 | 
					    or nil
 | 
				
			||||||
      == self.tree:parse({ topline, botline + 1 }, function(_, trees)
 | 
					      == self.tree:parse({ topline, botline + 1 }, function(_, trees)
 | 
				
			||||||
        if trees and self.parsing then
 | 
					        if trees and self.parsing[win] then
 | 
				
			||||||
          self.parsing = false
 | 
					          self.parsing[win] = false
 | 
				
			||||||
          api.nvim__redraw({ win = win, valid = false, flush = false })
 | 
					          api.nvim__redraw({ win = win, valid = false, flush = false })
 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
      end)
 | 
					      end)
 | 
				
			||||||
  if not self.parsing then
 | 
					  if not self.parsing[win] then
 | 
				
			||||||
    self.redraw_count = self.redraw_count + 1
 | 
					    self.redraw_count = self.redraw_count + 1
 | 
				
			||||||
    self:prepare_highlight_states(topline, botline)
 | 
					    self:prepare_highlight_states(win, topline, botline)
 | 
				
			||||||
  else
 | 
					  else
 | 
				
			||||||
    self:for_each_highlight_state(function(state)
 | 
					    self:for_each_highlight_state(win, function(state)
 | 
				
			||||||
      -- TODO(ribru17): Inefficient. Eventually all marks should be applied in on_buf, and all
 | 
					      -- TODO(ribru17): Inefficient. Eventually all marks should be applied in on_buf, and all
 | 
				
			||||||
      -- non-folded ranges of each open window should be merged, and iterators should only be
 | 
					      -- non-folded ranges of each open window should be merged, and iterators should only be
 | 
				
			||||||
      -- created over those regions. This would also fix #31777.
 | 
					      -- created over those regions. This would also fix #31777.
 | 
				
			||||||
      --
 | 
					      --
 | 
				
			||||||
      -- Currently this is not possible because the parser discards previously parsed injection
 | 
					      -- Currently this is not possible because the parser discards previously parsed injection
 | 
				
			||||||
      -- trees upon parsing a different region.
 | 
					      -- trees upon parsing a different region.
 | 
				
			||||||
      --
 | 
					 | 
				
			||||||
      -- It would also be nice if rather than re-querying extmarks for old trees, we could tell the
 | 
					 | 
				
			||||||
      -- decoration provider to not clear previous ephemeral marks for this redraw cycle.
 | 
					 | 
				
			||||||
      state.iter = nil
 | 
					      state.iter = nil
 | 
				
			||||||
      state.next_row = 0
 | 
					      state.next_row = 0
 | 
				
			||||||
    end)
 | 
					    end)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
  return #self._highlight_states > 0
 | 
					  local hl_states = self._highlight_states[win] or {}
 | 
				
			||||||
 | 
					  return #hl_states > 0
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
api.nvim_set_decoration_provider(ns, {
 | 
					api.nvim_set_decoration_provider(ns, {
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user