From 3dd3a6ceab1d14f3cb2b231134f6d9e4dfcebf9c Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Sat, 20 Jun 2026 16:19:02 -0400 Subject: [PATCH] backport: fix(lsp): make LspNotify more robust (#40341) 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. (cherry picked from commit 54188fa242582808fc1d0651786a4aa2f6295fa6) Co-authored-by: jdrouhard --- runtime/doc/lsp.txt | 5 +-- runtime/lua/vim/lsp.lua | 4 +-- runtime/lua/vim/lsp/_changetracking.lua | 8 ++--- runtime/lua/vim/lsp/client.lua | 32 +++++++++---------- runtime/lua/vim/lsp/semantic_tokens.lua | 10 +----- .../plugin/lsp/semantic_tokens_spec.lua | 1 + 6 files changed, 27 insertions(+), 33 deletions(-) diff --git a/runtime/doc/lsp.txt b/runtime/doc/lsp.txt index 8c2889d92d..3a5327226c 100644 --- a/runtime/doc/lsp.txt +++ b/runtime/doc/lsp.txt @@ -1743,7 +1743,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|. @@ -1967,12 +1967,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 diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index cc6518aa01..3958e73896 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -922,7 +922,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 = @@ -961,7 +961,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 diff --git a/runtime/lua/vim/lsp/_changetracking.lua b/runtime/lua/vim/lsp/_changetracking.lua index 7411ae7dee..8b0ebe504c 100644 --- a/runtime/lua/vim/lsp/_changetracking.lua +++ b/runtime/lua/vim/lsp/_changetracking.lua @@ -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) end end end @@ -327,7 +327,7 @@ local function send_changes(bufnr, sync_kind, state, buf_state) version = util.buf_versions[bufnr], }, contentChanges = changes, - }) + }, bufnr) end end end diff --git a/runtime/lua/vim/lsp/client.lua b/runtime/lua/vim/lsp/client.lua index d4464e9f0f..a3fb3edef8 100644 --- a/runtime/lua/vim/lsp/client.lua +++ b/runtime/lua/vim/lsp/client.lua @@ -825,9 +825,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 @@ -835,16 +836,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 @@ -1116,14 +1116,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. @@ -1145,7 +1145,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() diff --git a/runtime/lua/vim/lsp/semantic_tokens.lua b/runtime/lua/vim/lsp/semantic_tokens.lua index 2726752d77..57264db127 100644 --- a/runtime/lua/vim/lsp/semantic_tokens.lua +++ b/runtime/lua/vim/lsp/semantic_tokens.lua @@ -245,14 +245,6 @@ function STHighlighter:on_attach(client_id) end, }) - api.nvim_create_autocmd({ 'BufWinEnter', 'InsertLeave' }, { - buf = self.bufnr, - group = self.augroup, - callback = function() - self:send_request() - end, - }) - if state.supports_range then api.nvim_create_autocmd('WinScrolled', { buf = self.bufnr, @@ -811,7 +803,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 diff --git a/test/functional/plugin/lsp/semantic_tokens_spec.lua b/test/functional/plugin/lsp/semantic_tokens_spec.lua index 3c705532c0..dabffcd639 100644 --- a/test/functional/plugin/lsp/semantic_tokens_spec.lua +++ b/test/functional/plugin/lsp/semantic_tokens_spec.lua @@ -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,