fix(lsp): make LspNotify more robust #40332

Problem: LspNotify never passed a buffer when executing the autocmds, so
buffer-local LspNotify autocmd subscriptions didn't have the correct buf
in the event metadata. It was also wrapped in a schedule() so the actual
autocmd was delayed until after the event loop.

This could result in the wrong buffer receiving the notification if
multiple LspNotify autocmds with buffer filters were added. Only the
"latest" one would actually receive non-buffer-filtered autocmds, not
the matching one. It also caused listeners to receive the notification
"out of sync" with when the notification is actually sent. If a buffer
is being deleted (which fires a textDocument/didClose notification), the
notification is scheduled and fired after the buffer is already gone.

Solution: For LSP notifications that pertain to a particular buffer, set
it when executing the LspNotify autocmds so the callback functions that
are filtered on that buffer will get the correct notifications and the
metadata buf field will be correct. Additionally, there is no need to
wrap the LspNotify callback in vim.schedule when it can be called inline
when the notification to the rpc server is fired.

This is tested by removing now-unnecessary autocmds from semantic tokens
(InsertEnter and BufWinEnter should no longer be necessary now that
requests are fired by LspNotify). Without this fix, simply modifying a
buffer doesn't actually trigger LspNotify correctly, and the test for
that fails.
This commit is contained in:
jdrouhard
2026-06-20 11:46:58 -05:00
committed by GitHub
parent 6bc6461eac
commit 54188fa242
6 changed files with 27 additions and 29 deletions

View File

@@ -1780,7 +1780,7 @@ Lua module: vim.lsp.client *lsp-client*
• {is_stopped} (`fun(self: vim.lsp.Client): boolean`) See
|Client:is_stopped()|.
• {name} (`string`) See |vim.lsp.ClientConfig|.
• {notify} (`fun(self: vim.lsp.Client, method: string, params: table?): boolean`)
• {notify} (`fun(self: vim.lsp.Client, method: string, params: table?, bufnr: integer?): boolean`)
See |Client:notify()|.
• {offset_encoding} (`'utf-8'|'utf-16'|'utf-32'`) See
|vim.lsp.ClientConfig|.
@@ -2015,12 +2015,13 @@ Client:is_stopped() *Client:is_stopped()*
(`boolean`) true if client is stopped or in the process of being
stopped; false otherwise
Client:notify({method}, {params}) *Client:notify()*
Client:notify({method}, {params}, {bufnr}) *Client:notify()*
Sends a notification to an LSP server.
Parameters: ~
• {method} (`string`) LSP method name.
• {params} (`table?`) LSP request params.
• {bufnr} (`integer?`) Buffer associated with notification.
Return: ~
(`boolean`) status indicating if the notification was successful. If

View File

@@ -919,7 +919,7 @@ local function buf_attach(bufnr)
reason = protocol.TextDocumentSaveReason.Manual, ---@type integer
}
if client:supports_method('textDocument/willSave') then
client:notify('textDocument/willSave', params)
client:notify('textDocument/willSave', params, bufnr)
end
if client:supports_method('textDocument/willSaveWaitUntil') then
local result, err =
@@ -955,7 +955,7 @@ local function buf_attach(bufnr)
for _, client in ipairs(clients) do
changetracking.reset_buf(client, bufnr)
if client:supports_method('textDocument/didClose') then
client:notify('textDocument/didClose', params)
client:notify('textDocument/didClose', params, bufnr)
end
end
for _, client in ipairs(clients) do

View File

@@ -193,7 +193,7 @@ function M._send_did_save(bufnr)
textDocument = {
uri = vim.uri_from_fname(old_name),
},
})
}, bufnr)
client:notify('textDocument/didOpen', {
textDocument = {
version = 0,
@@ -201,7 +201,7 @@ function M._send_did_save(bufnr)
languageId = client.get_language_id(bufnr, vim.bo[bufnr].filetype),
text = vim.lsp._buf_get_full_text(bufnr),
},
})
}, bufnr)
util.buf_versions[bufnr] = 0
end
local save_capability = vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'save')
@@ -215,7 +215,7 @@ function M._send_did_save(bufnr)
uri = uri,
},
text = included_text,
})
}, bufnr)
else
M.flush(client, bufnr)
end
@@ -329,7 +329,7 @@ local function send_changes(bufnr, sync_kind, state, buf_state)
version = util.buf_versions[bufnr],
},
contentChanges = changes,
})
}, bufnr)
end
end
end

View File

@@ -847,9 +847,10 @@ end
---
--- @param method vim.lsp.protocol.Method.ClientToServer.Notification LSP method name.
--- @param params table? LSP request params.
--- @param bufnr integer? Buffer associated with notification.
--- @return boolean status indicating if the notification was successful.
--- If it is false, then the client has shutdown.
function Client:notify(method, params)
function Client:notify(method, params, bufnr)
if method ~= 'textDocument/didChange' then
changetracking.flush(self)
end
@@ -857,16 +858,15 @@ function Client:notify(method, params)
local client_active = self.rpc.notify(method, params)
if client_active then
vim.schedule(function()
api.nvim_exec_autocmds('LspNotify', {
modeline = false,
data = {
client_id = self.id,
method = method,
params = params,
},
})
end)
api.nvim_exec_autocmds('LspNotify', {
buf = bufnr,
modeline = false,
data = {
client_id = self.id,
method = method,
params = params,
},
})
end
return client_active
@@ -1137,14 +1137,14 @@ end
--- Default handler for the 'textDocument/didClose' LSP notification.
---
--- @param buf integer Number of the buffer, or 0 for current
function Client:_text_document_did_close_handler(buf)
--- @param bufnr integer Number of the buffer, or 0 for current
function Client:_text_document_did_close_handler(bufnr)
if not self:supports_method('textDocument/didClose') then
return
end
local uri = vim.uri_from_bufnr(buf)
local uri = vim.uri_from_bufnr(bufnr)
local params = { textDocument = { uri = uri } }
self:notify('textDocument/didClose', params)
self:notify('textDocument/didClose', params, bufnr)
end
--- Default handler for the 'textDocument/didOpen' LSP notification.
@@ -1166,7 +1166,7 @@ function Client:_text_document_did_open_handler(bufnr)
languageId = self:_get_language_id(bufnr),
text = lsp._buf_get_full_text(bufnr),
},
})
}, bufnr)
-- Next chance we get, we should re-do the diagnostics
vim.schedule(function()

View File

@@ -242,10 +242,6 @@ function STHighlighter:on_attach(client_id)
end
end)
nvim_on({ 'BufWinEnter', 'InsertLeave' }, self.augroup, { buf = self.bufnr }, function()
self:send_request()
end)
if state.supports_range then
nvim_on('WinScrolled', self.augroup, { buf = self.bufnr }, function()
self:debounce_request()
@@ -802,7 +798,7 @@ function M.start(bufnr, client_id, opts)
M.enable(true, { bufnr = bufnr, client_id = client_id })
if opts and opts.debounce then
if opts and opts.debounce and STHighlighter.active[bufnr] then
local highlighter = STHighlighter.active[bufnr]
local prev = rawget(highlighter, 'debounce')
highlighter.debounce = prev and math.max(prev, opts.debounce) or opts.debounce

View File

@@ -311,6 +311,7 @@ describe('semantic token highlighting', function()
exec_lua(function()
_G.server_full = _G._create_server({
capabilities = {
textDocumentSync = vim.lsp.protocol.TextDocumentSyncKind.Full,
semanticTokensProvider = {
full = { delta = false },
range = true,