mirror of
https://github.com/neovim/neovim.git
synced 2026-07-14 13:20:35 +00:00
fix(lsp): define autocmds for capabilities once per buffer #40435
fix(lsp): define autocmds for capabilities in new(), not on_attach() Problem: Defining autocmds in on_attach() caused issues when multiple clients provide the same capability for a buffer. Each attaching client would "replace" the previously defined one since they are all identical. Then the first one to detach clears them out and any remaining attached clients would no longer trigger the autocmd for the capability. Further, the semantic tokens module itself didn't quite work with multiple clients since any LspNotify (from any client) would send a token request to all attached clients with no differentiation and the debounce timer was shared across all clients. Solution: Always define the buffer-local autocmds in the capability's `new()` function, and don't mess with the autocmds in on_attach or on_detach. The capability framework itself will clear the autocmds when the last client detaches. Also, refactor a bit of the semantic tokens module so that the various methods take a specific client_id to perform the work on, and split out timers so each client has its own.
This commit is contained in:
@@ -164,6 +164,25 @@ function LinkedEditor:refresh()
|
||||
end)
|
||||
end
|
||||
|
||||
---@package
|
||||
function LinkedEditor:new(bufnr)
|
||||
self = Capability.new(self, bufnr)
|
||||
|
||||
nvim_on({ 'TextChanged', 'TextChangedI' }, self.augroup, { buf = self.bufnr }, function()
|
||||
for _, state in pairs(self.client_state) do
|
||||
update_ranges(self.bufnr, state)
|
||||
end
|
||||
|
||||
self:refresh()
|
||||
end)
|
||||
|
||||
nvim_on('CursorMoved', self.augroup, { buf = self.bufnr }, function()
|
||||
self:refresh()
|
||||
end)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
---@package
|
||||
---@param client_id integer
|
||||
function LinkedEditor:on_attach(client_id)
|
||||
@@ -176,15 +195,6 @@ function LinkedEditor:on_attach(client_id)
|
||||
self.client_state[client_id] = state
|
||||
end
|
||||
|
||||
nvim_on({ 'TextChanged', 'TextChangedI' }, self.augroup, { buf = self.bufnr }, function()
|
||||
update_ranges(self.bufnr, state)
|
||||
self:refresh()
|
||||
end)
|
||||
|
||||
nvim_on('CursorMoved', self.augroup, { buf = self.bufnr }, function()
|
||||
self:refresh()
|
||||
end)
|
||||
|
||||
self:refresh()
|
||||
end
|
||||
|
||||
@@ -195,7 +205,6 @@ function LinkedEditor:on_detach(client_id)
|
||||
if client_state then
|
||||
--TODO: delete namespace if/when that becomes possible
|
||||
clear_ranges(self.bufnr, client_state)
|
||||
api.nvim_clear_autocmds({ group = self.augroup })
|
||||
self.client_state[client_id] = nil
|
||||
end
|
||||
end
|
||||
|
||||
@@ -36,12 +36,12 @@ local M = {}
|
||||
--- @field active_request STActiveRequest
|
||||
--- @field active_range_request STActiveRequest
|
||||
--- @field current_result STCurrentResult
|
||||
--- @field timer uv.uv_timer_t? uv_timer for debouncing requests for new tokens
|
||||
--- @field has_full_result boolean
|
||||
|
||||
---@class (private) STHighlighter : vim.lsp.Capability
|
||||
---@field active table<integer, STHighlighter>
|
||||
---@field debounce integer milliseconds to debounce requests for new tokens
|
||||
---@field timer uv.uv_timer_t? uv_timer for debouncing requests for new tokens
|
||||
---@field client_state table<integer, STClientState>
|
||||
local STHighlighter = {
|
||||
name = 'semantic_tokens',
|
||||
@@ -207,6 +207,34 @@ local function tokens_to_ranges(data, bufnr, client, request, ranges)
|
||||
return ranges
|
||||
end
|
||||
|
||||
---@package
|
||||
function STHighlighter:new(bufnr)
|
||||
self = Capability.new(self, bufnr)
|
||||
|
||||
nvim_on('LspNotify', self.augroup, { buf = self.bufnr }, function(opts)
|
||||
local client_id = opts.data.client_id ---@type integer
|
||||
if opts.data.method == 'textDocument/didClose' then
|
||||
self:reset(client_id)
|
||||
end
|
||||
|
||||
if
|
||||
opts.data.method == 'textDocument/didChange' or opts.data.method == 'textDocument/didOpen'
|
||||
then
|
||||
self:send_request(client_id)
|
||||
end
|
||||
end)
|
||||
|
||||
nvim_on('WinScrolled', self.augroup, { buf = self.bufnr }, function()
|
||||
for client_id, state in pairs(self.client_state) do
|
||||
if state.supports_range then
|
||||
self:debounce_request(client_id)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
---@package
|
||||
function STHighlighter:on_attach(client_id)
|
||||
local client = vim.lsp.get_client_by_id(client_id)
|
||||
@@ -228,25 +256,7 @@ function STHighlighter:on_attach(client_id)
|
||||
self.client_state[client_id] = state
|
||||
end
|
||||
|
||||
nvim_on('LspNotify', self.augroup, { buf = self.bufnr }, function(opts)
|
||||
if opts.data.method == 'textDocument/didClose' then
|
||||
self:reset()
|
||||
end
|
||||
|
||||
if
|
||||
opts.data.method == 'textDocument/didChange' or opts.data.method == 'textDocument/didOpen'
|
||||
then
|
||||
self:send_request()
|
||||
end
|
||||
end)
|
||||
|
||||
if state.supports_range then
|
||||
nvim_on('WinScrolled', self.augroup, { buf = self.bufnr }, function()
|
||||
self:debounce_request()
|
||||
end)
|
||||
end
|
||||
|
||||
self:send_request()
|
||||
self:send_request(client_id)
|
||||
end
|
||||
|
||||
---@package
|
||||
@@ -255,7 +265,6 @@ function STHighlighter:on_detach(client_id)
|
||||
if state then
|
||||
--TODO: delete namespace if/when that becomes possible
|
||||
api.nvim_buf_clear_namespace(self.bufnr, state.namespace, 0, -1)
|
||||
api.nvim_clear_autocmds({ group = self.augroup })
|
||||
self.client_state[client_id] = nil
|
||||
end
|
||||
end
|
||||
@@ -277,28 +286,30 @@ end
|
||||
--- facilitate document synchronization in the response.
|
||||
---
|
||||
---@package
|
||||
function STHighlighter:send_request()
|
||||
---@param client_id integer
|
||||
function STHighlighter:send_request(client_id)
|
||||
local version = util.buf_versions[self.bufnr]
|
||||
|
||||
self:reset_timer()
|
||||
self:reset_timer(client_id)
|
||||
|
||||
for client_id, state in pairs(self.client_state) do
|
||||
local client = vim.lsp.get_client_by_id(client_id)
|
||||
if client then
|
||||
-- If the server supports range and there's no full result yet, then start with a range
|
||||
-- request
|
||||
if state.supports_range and not state.has_full_result then
|
||||
self:send_range_request(client, state, version)
|
||||
end
|
||||
local client = vim.lsp.get_client_by_id(client_id)
|
||||
local state = self.client_state[client_id]
|
||||
|
||||
if
|
||||
(not state.has_full_result or state.current_result.version ~= version)
|
||||
and state.active_request.version ~= version
|
||||
then
|
||||
self:send_full_delta_request(client, state, version)
|
||||
end
|
||||
if client and state then
|
||||
-- If the server supports range and there's no full result yet, then start with a range
|
||||
-- request
|
||||
if state.supports_range and not state.has_full_result then
|
||||
self:send_range_request(client, state, version)
|
||||
end
|
||||
|
||||
if
|
||||
(not state.has_full_result or state.current_result.version ~= version)
|
||||
and state.active_request.version ~= version
|
||||
then
|
||||
self:send_full_delta_request(client, state, version)
|
||||
end
|
||||
end
|
||||
-- end
|
||||
end
|
||||
|
||||
--- Send a range request for the visible area
|
||||
@@ -713,13 +724,13 @@ end
|
||||
--- Reset the buffer's highlighting state and clears the extmark highlights.
|
||||
---
|
||||
---@package
|
||||
function STHighlighter:reset()
|
||||
for client_id, state in pairs(self.client_state) do
|
||||
api.nvim_buf_clear_namespace(self.bufnr, state.namespace, 0, -1)
|
||||
state.current_result = {}
|
||||
state.has_full_result = false
|
||||
self:cancel_active_request(client_id)
|
||||
end
|
||||
---@param client_id integer
|
||||
function STHighlighter:reset(client_id)
|
||||
local state = assert(self.client_state[client_id])
|
||||
api.nvim_buf_clear_namespace(self.bufnr, state.namespace, 0, -1)
|
||||
state.current_result = {}
|
||||
state.has_full_result = false
|
||||
self:cancel_active_request(client_id)
|
||||
end
|
||||
|
||||
--- Mark a client's results as dirty. This method will cancel any active
|
||||
@@ -747,22 +758,27 @@ function STHighlighter:mark_dirty(client_id)
|
||||
end
|
||||
|
||||
---@package
|
||||
function STHighlighter:debounce_request()
|
||||
self:reset_timer()
|
||||
---@param client_id integer
|
||||
function STHighlighter:debounce_request(client_id)
|
||||
local state = assert(self.client_state[client_id])
|
||||
|
||||
self:reset_timer(client_id)
|
||||
if self.debounce > 0 then
|
||||
self.timer = vim.defer_fn(function()
|
||||
self:send_request()
|
||||
state.timer = vim.defer_fn(function()
|
||||
self:send_request(client_id)
|
||||
end, self.debounce)
|
||||
else
|
||||
self:send_request()
|
||||
self:send_request(client_id)
|
||||
end
|
||||
end
|
||||
|
||||
---@private
|
||||
function STHighlighter:reset_timer()
|
||||
local timer = self.timer
|
||||
---@param client_id integer
|
||||
function STHighlighter:reset_timer(client_id)
|
||||
local state = self.client_state[client_id]
|
||||
local timer = state.timer
|
||||
if timer then
|
||||
self.timer = nil
|
||||
state.timer = nil
|
||||
if not timer:is_closing() then
|
||||
timer:stop()
|
||||
timer:close()
|
||||
@@ -914,8 +930,10 @@ function M.force_refresh(bufnr)
|
||||
for _, buffer in ipairs(buffers) do
|
||||
local highlighter = STHighlighter.active[buffer]
|
||||
if highlighter then
|
||||
highlighter:reset()
|
||||
highlighter:send_request()
|
||||
for client_id, _ in pairs(highlighter.client_state) do
|
||||
highlighter:reset(client_id)
|
||||
highlighter:send_request(client_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -974,7 +992,7 @@ function M._refresh(err, _, ctx)
|
||||
|
||||
if not vim.tbl_isempty(vim.fn.win_findbuf(bufnr)) then
|
||||
-- some LSPs send rapid fire refresh notifications, so we'll debounce them with debounce_request()
|
||||
highlighter:debounce_request()
|
||||
highlighter:debounce_request(ctx.client_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user