diff --git a/runtime/doc/lsp.txt b/runtime/doc/lsp.txt index 98609a0d0b..623fef2962 100644 --- a/runtime/doc/lsp.txt +++ b/runtime/doc/lsp.txt @@ -1723,7 +1723,8 @@ Lua module: vim.lsp.client *lsp-client* *vim.lsp.Client* Fields: ~ - • {attached_buffers} (`table`) + • {attached_buffers} (`table`) Each buffer's last + used `languageId`. • {cancel_request} (`fun(self: vim.lsp.Client, id: integer): boolean`) See |Client:cancel_request()|. • {capabilities} (`lsp.ClientCapabilities`) Capabilities diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 2cf17325ca..af47bbe908 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -88,7 +88,7 @@ EVENTS LSP -• todo +• `client.attached_buffers[buf]` now stores `languageId` string (was boolean). LUA diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 6cbe98b6eb..c64429df41 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -531,9 +531,20 @@ local function lsp_enable_callback(bufnr) lsp.is_enabled(client.name) -- Check that the client is managed by vim.lsp.config before deciding to detach it! and lsp.config[client.name] - and not can_start(bufnr, lsp.config[client.name], false) then - lsp.buf_detach_client(bufnr, client.id) + if can_start(bufnr, lsp.config[client.name], false) then + -- When switch between lsp supported filetype (e.g. json to jsonc like #39498), + -- client should send `textDocument/didClose` + `textDocument/didOpen` with new language id + local new_language_id = client.get_language_id(bufnr, vim.bo[bufnr].filetype) + local old_language_id = client.attached_buffers[bufnr] ---@type string? + if old_language_id and old_language_id ~= new_language_id then + client:_text_document_did_close_handler(bufnr) + client.attached_buffers[bufnr] = new_language_id + client:_text_document_did_open_handler(bufnr) + end + else + lsp.buf_detach_client(bufnr, client.id) + end end end @@ -1002,7 +1013,7 @@ function lsp.buf_attach_client(bufnr, client_id) return true end - client.attached_buffers[bufnr] = true + client.attached_buffers[bufnr] = client.get_language_id(bufnr, vim.bo[bufnr].filetype) -- This is our first time attaching this client to this buffer. -- Send didOpen for the client if it is initialized. If it isn't initialized diff --git a/runtime/lua/vim/lsp/client.lua b/runtime/lua/vim/lsp/client.lua index bc8e68fb2d..7c0d36d5ff 100644 --- a/runtime/lua/vim/lsp/client.lua +++ b/runtime/lua/vim/lsp/client.lua @@ -167,7 +167,8 @@ local all_clients = {} --- @class vim.lsp.Client --- ---- @field attached_buffers table +--- Each buffer's last used `languageId`. +--- @field attached_buffers table --- --- Capabilities provided by the client (editor or tool), at startup. --- @field capabilities lsp.ClientCapabilities @@ -1124,6 +1125,18 @@ function Client:exec_cmd(cmd, context, handler) self:request('workspace/executeCommand', params, handler, context.bufnr) 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) + if not self:supports_method('textDocument/didClose') then + return + end + local uri = vim.uri_from_bufnr(buf) + local params = { textDocument = { uri = uri } } + self:notify('textDocument/didClose', params) +end + --- Default handler for the 'textDocument/didOpen' LSP notification. --- --- @param bufnr integer Number of the buffer, or 0 for current @@ -1192,7 +1205,7 @@ function Client:on_attach(bufnr) end end) - self.attached_buffers[bufnr] = true + self.attached_buffers[bufnr] = self:_get_language_id(bufnr) end --- @private @@ -1383,11 +1396,7 @@ function Client:_on_detach(bufnr) changetracking.reset_buf(self, bufnr) - if self:supports_method('textDocument/didClose') then - local uri = vim.uri_from_bufnr(bufnr) - local params = { textDocument = { uri = uri } } - self:notify('textDocument/didClose', params) - end + self:_text_document_did_close_handler(bufnr) self.attached_buffers[bufnr] = nil diff --git a/test/functional/plugin/lsp_spec.lua b/test/functional/plugin/lsp_spec.lua index 4dbb0dee3e..0e375d62da 100644 --- a/test/functional/plugin/lsp_spec.lua +++ b/test/functional/plugin/lsp_spec.lua @@ -4488,6 +4488,52 @@ describe('LSP', function() eq({ 0, 'foo', 1, 'bar' }, count_clients()) end) + it('sends didClose and didOpen when languageId changes', function() + exec_lua(create_server_definition) + + local tmp1 = t.tmpname(true) + + exec_lua(function() + _G.server = _G._create_server({ + handlers = { + initialize = function(_, _, callback) + callback(nil, { capabilities = { textDocumentSync = { openClose = true } } }) + end, + }, + }) + vim.lsp.config('foo', { cmd = _G.server.cmd, filetypes = { 'foo', 'bar' } }) + vim.lsp.enable('foo') + vim.cmd.edit(tmp1) + end) + + local function test_messages() + local opens = 0 + local closes = 0 + local msgs = exec_lua([[ return _G.server.messages ]]) + local num_clients = exec_lua([[ return #vim.lsp.get_clients() ]]) + for _, msg in ipairs(msgs) do + opens = opens + (msg.method == 'textDocument/didOpen' and 1 or 0) + closes = closes + (msg.method == 'textDocument/didClose' and 1 or 0) + end + return { opens, 'did_open', closes, 'did_close', num_clients, 'clients' } + end + + -- No filetype on the buffer yet. + eq({ 0, 'did_open', 0, 'did_close', 0, 'clients' }, test_messages()) + + -- Set the filetype to 'foo', confirm didOpen is sent. + exec_lua([[vim.bo.filetype = 'foo']]) + retry(nil, 1000, function() + eq({ 1, 'did_open', 0, 'did_close', 1, 'clients' }, test_messages()) + end) + + -- Set to anohter lsp-supported filetype 'bar', confirm didClose and didOpen are sent. + exec_lua([[vim.bo.filetype = 'bar']]) + retry(nil, 1000, function() + eq({ 2, 'did_open', 1, 'did_close', 1, 'clients' }, test_messages()) + end) + end) + it('validates config on attach', function() local tmp1 = t.tmpname(true) exec_lua(function()