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')
local rconfig = lsp._enabled_configs[name] or {}
self._configs[name] = self._configs[name] or {}
if not rconfig.resolved_config then
-- Resolve configs from lsp/*.lua
-- 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
local config = assert(loadfile(v))() ---@type any?
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
log.warn(string.format('%s does not return a table, ignoring', v))
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(
'force',
lsp.config._configs['*'] or {},
rtp_config,
lsp.config._configs[name] or {}
rtp_config or {},
self._configs[name] or {}
)
rconfig.resolved_config.name = name
end
@ -424,6 +429,50 @@ lsp.config = setmetatable({ _configs = {} }, {
local lsp_enable_autocmd_id --- @type integer?
local function validate_cmd(v)
if type(v) == 'table' then
if vim.fn.executable(v[1]) == 0 then
return false, v[1] .. ' is not executable'
end
return true
end
return type(v) == 'function'
end
--- @param config vim.lsp.Config
local function validate_config(config)
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
end
if config.filetypes and not vim.tbl_contains(config.filetypes, vim.bo[bufnr].filetype) then
return false
end
return true
end
--- @param bufnr integer
--- @param config vim.lsp.Config
local function start_config(bufnr, config)
return vim.lsp.start(config, {
bufnr = bufnr,
reuse_client = config.reuse_client,
_root_markers = config.root_markers,
})
end
--- @param bufnr integer
local function lsp_enable_callback(bufnr)
-- Only ever attach to buffers that represent an actual file.
@ -431,32 +480,9 @@ local function lsp_enable_callback(bufnr)
return
end
--- @param config vim.lsp.Config
local function can_start(config)
if config.filetypes and not vim.tbl_contains(config.filetypes, vim.bo[bufnr].filetype) then
return false
elseif type(config.cmd) == 'table' and vim.fn.executable(config.cmd[1]) == 0 then
return false
end
return true
end
--- @param config vim.lsp.Config
local function start(config)
return vim.lsp.start(config, {
bufnr = bufnr,
reuse_client = config.reuse_client,
_root_markers = config.root_markers,
})
end
for name in vim.spairs(lsp._enabled_configs) do
local config = lsp.config[name]
validate('cmd', config.cmd, { 'function', 'table' })
validate('cmd', config.reuse_client, 'function', true)
if can_start(config) then
if config and can_start(bufnr, name, config) then
-- Deepcopy config so changes done in the client
-- do not propagate back to the enabled configs.
config = vim.deepcopy(config)
@ -466,11 +492,11 @@ local function lsp_enable_callback(bufnr)
config.root_dir(bufnr, function(root_dir)
config.root_dir = root_dir
vim.schedule(function()
start(config)
start_config(bufnr, config)
end)
end)
else
start(config)
start_config(bufnr, config)
end
end
end

View File

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

View File

@ -6370,5 +6370,35 @@ describe('LSP', function()
)
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)