fix(lsp): handle non-existent configs in lsp.config/enable

This commit is contained in:
Lewis Russell
2025-03-08 10:15:43 +00:00
committed by Lewis Russell
parent c63e794b10
commit ed07167261
3 changed files with 92 additions and 31 deletions

View File

@@ -374,26 +374,31 @@ lsp.config = setmetatable({ _configs = {} }, {
validate('name', name, 'string') validate('name', name, 'string')
local rconfig = lsp._enabled_configs[name] or {} local rconfig = lsp._enabled_configs[name] or {}
self._configs[name] = self._configs[name] or {}
if not rconfig.resolved_config then if not rconfig.resolved_config then
-- Resolve configs from lsp/*.lua -- Resolve configs from lsp/*.lua
-- Calls to vim.lsp.config in lsp/* have a lower precedence than calls from other sites. -- Calls to vim.lsp.config in lsp/* have a lower precedence than calls from other sites.
local rtp_config = {} ---@type vim.lsp.Config local rtp_config --- @type vim.lsp.Config?
for _, v in ipairs(api.nvim_get_runtime_file(('lsp/%s.lua'):format(name), true)) do for _, v in ipairs(api.nvim_get_runtime_file(('lsp/%s.lua'):format(name), true)) do
local config = assert(loadfile(v))() ---@type any? local config = assert(loadfile(v))() ---@type any?
if type(config) == 'table' then if type(config) == 'table' then
rtp_config = vim.tbl_deep_extend('force', rtp_config, config) --- @type vim.lsp.Config?
rtp_config = vim.tbl_deep_extend('force', rtp_config or {}, config)
else else
log.warn(string.format('%s does not return a table, ignoring', v)) log.warn(string.format('%s does not return a table, ignoring', v))
end end
end end
if not rtp_config and not self._configs[name] then
log.warn(string.format('%s does not have a configuration', name))
return
end
rconfig.resolved_config = vim.tbl_deep_extend( rconfig.resolved_config = vim.tbl_deep_extend(
'force', 'force',
lsp.config._configs['*'] or {}, lsp.config._configs['*'] or {},
rtp_config, rtp_config or {},
lsp.config._configs[name] or {} self._configs[name] or {}
) )
rconfig.resolved_config.name = name rconfig.resolved_config.name = name
end end
@@ -424,26 +429,43 @@ lsp.config = setmetatable({ _configs = {} }, {
local lsp_enable_autocmd_id --- @type integer? local lsp_enable_autocmd_id --- @type integer?
--- @param bufnr integer local function validate_cmd(v)
local function lsp_enable_callback(bufnr) if type(v) == 'table' then
-- Only ever attach to buffers that represent an actual file. if vim.fn.executable(v[1]) == 0 then
if vim.bo[bufnr].buftype ~= '' then return false, v[1] .. ' is not executable'
return end
return true
end
return type(v) == 'function'
end end
--- @param config vim.lsp.Config --- @param config vim.lsp.Config
local function can_start(config) local function validate_config(config)
if config.filetypes and not vim.tbl_contains(config.filetypes, vim.bo[bufnr].filetype) then validate('cmd', config.cmd, validate_cmd, 'expected function or table with executable command')
validate('reuse_client', config.reuse_client, 'function', true)
validate('filetypes', config.filetypes, 'table', true)
end
--- @param bufnr integer
--- @param name string
--- @param config vim.lsp.Config
local function can_start(bufnr, name, config)
local config_ok, err = pcall(validate_config, config)
if not config_ok then
log.error(('cannot start %s due to config error: %s'):format(name, err))
return false return false
elseif type(config.cmd) == 'table' and vim.fn.executable(config.cmd[1]) == 0 then end
if config.filetypes and not vim.tbl_contains(config.filetypes, vim.bo[bufnr].filetype) then
return false return false
end end
return true return true
end end
--- @param bufnr integer
--- @param config vim.lsp.Config --- @param config vim.lsp.Config
local function start(config) local function start_config(bufnr, config)
return vim.lsp.start(config, { return vim.lsp.start(config, {
bufnr = bufnr, bufnr = bufnr,
reuse_client = config.reuse_client, reuse_client = config.reuse_client,
@@ -451,12 +473,16 @@ local function lsp_enable_callback(bufnr)
}) })
end end
--- @param bufnr integer
local function lsp_enable_callback(bufnr)
-- Only ever attach to buffers that represent an actual file.
if vim.bo[bufnr].buftype ~= '' then
return
end
for name in vim.spairs(lsp._enabled_configs) do for name in vim.spairs(lsp._enabled_configs) do
local config = lsp.config[name] local config = lsp.config[name]
validate('cmd', config.cmd, { 'function', 'table' }) if config and can_start(bufnr, name, config) then
validate('cmd', config.reuse_client, 'function', true)
if can_start(config) then
-- Deepcopy config so changes done in the client -- Deepcopy config so changes done in the client
-- do not propagate back to the enabled configs. -- do not propagate back to the enabled configs.
config = vim.deepcopy(config) config = vim.deepcopy(config)
@@ -466,11 +492,11 @@ local function lsp_enable_callback(bufnr)
config.root_dir(bufnr, function(root_dir) config.root_dir(bufnr, function(root_dir)
config.root_dir = root_dir config.root_dir = root_dir
vim.schedule(function() vim.schedule(function()
start(config) start_config(bufnr, config)
end) end)
end) end)
else else
start(config) start_config(bufnr, config)
end end
end end
end end

View File

@@ -45,6 +45,11 @@ function log.get_filename()
return logfilename return logfilename
end end
--- @param s string
function log._set_filename(s)
logfilename = s
end
--- @type file*?, string? --- @type file*?, string?
local logfile, openerr local logfile, openerr

View File

@@ -6370,5 +6370,35 @@ describe('LSP', function()
) )
end) end)
end) end)
it('validates config on attach', function()
local tmp1 = t.tmpname(true)
exec_lua(function()
vim.lsp.log._set_filename(fake_lsp_logfile)
end)
local function test_cfg(cfg, err)
exec_lua(function()
vim.lsp.config['foo'] = {}
vim.lsp.config('foo', cfg)
vim.lsp.enable('foo')
vim.cmd.edit(assert(tmp1))
vim.bo.filetype = 'foo'
end)
retry(nil, 1000, function()
assert_log(err, fake_lsp_logfile)
end)
end
test_cfg({
cmd = { 'lolling' },
}, 'cannot start foo due to config error: .* lolling is not executable')
test_cfg({
cmd = { 'cat' },
filetypes = true,
}, 'cannot start foo due to config error: .* filetypes: expected table, got boolean')
end)
end) end)
end) end)