diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 3a9a6151d1..9f47044f1c 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -443,10 +443,16 @@ local function validate_config(config) validate('filetypes', config.filetypes, 'table', true) end +--- Returns true if: +--- 1. the config is managed by vim.lsp, +--- 2. it applies to the given buffer, and +--- 3. its config is valid (in particular: its `cmd` isn't broken). +--- --- @param bufnr integer --- @param config vim.lsp.Config --- @param logging boolean local function can_start(bufnr, config, logging) + assert(config) if type(config.filetypes) == 'table' and not vim.tbl_contains(config.filetypes, vim.bo[bufnr].filetype) @@ -485,7 +491,13 @@ local function lsp_enable_callback(bufnr) -- Stop any clients that no longer apply to this buffer. local clients = lsp.get_clients({ bufnr = bufnr, _uninitialized = true }) for _, client in ipairs(clients) do - if lsp.is_enabled(client.name) and not can_start(bufnr, lsp.config[client.name], false) then + -- Don't index into lsp.config[…] unless is_enabled() is true. + if + 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) end end diff --git a/test/functional/plugin/lsp_spec.lua b/test/functional/plugin/lsp_spec.lua index 5a6754f225..0ca8b47586 100644 --- a/test/functional/plugin/lsp_spec.lua +++ b/test/functional/plugin/lsp_spec.lua @@ -6649,6 +6649,35 @@ describe('LSP', function() ) end) + it('handle nil config (some clients may not have a config!)', function() + exec_lua(create_server_definition) + exec_lua(function() + local server = _G._create_server() + vim.bo.filetype = 'lua' + -- Attach a client without defining a config. + local client_id = vim.lsp.start({ + name = 'test_ls', + cmd = function(dispatchers, config) + _G.test_resolved_root = config.root_dir --[[@type string]] + return server.cmd(dispatchers, config) + end, + }, { bufnr = 0 }) + + local bufnr = vim.api.nvim_get_current_buf() + local client = vim.lsp.get_client_by_id(client_id) + assert(client.attached_buffers[bufnr]) + + -- Exercise the codepath which had a regression: + vim.lsp.enable('test_ls') + vim.api.nvim_exec_autocmds('FileType', { buffer = bufnr }) + + -- enable() does _not_ detach the client since it doesn't actually have a config. + -- XXX: otoh, is it confusing to allow `enable("foo")` if there a "foo" _client_ without a "foo" _config_? + assert(client.attached_buffers[bufnr]) + assert(client_id == vim.lsp.get_client_by_id(bufnr).id) + end) + end) + it('attaches to buffers when they are opened', function() exec_lua(create_server_definition)