commit bf4710d8c398b0634029f0a5be7622dfc216f50d
parent c8d6f3cf8a121985e6b2d96b27fdb6131cc7309e
Author: Justin M. Keyes <justinkz@gmail.com>
Date: Tue, 14 Oct 2025 20:43:25 -0400
fix(lsp): "attempt to index nil config" #36189
Problem:
If a client doesn't have a config then an error may be thrown.
Probably caused by: 2f78ff816b03661b5f74d0624e973eaca0d64ef1
Lua callback: …/lsp.lua:442: attempt to index local 'config' (a nil value)
stack traceback:
…/lsp.lua:442: in function 'can_start'
…/lsp.lua:479: in function 'lsp_enable_callback'
…/lsp.lua:566: in function <…/lsp.lua:565>
Solution:
Not all clients necessarily have configs.
- Handle `config=nil` in `can_start`.
- If user "enables" an invalid name that happens to match a *client*
name, don't auto-detach the client.
Diffstat:
2 files changed, 42 insertions(+), 1 deletion(-)
diff --git 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
@@ -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)