mirror of
https://github.com/neovim/nvim-lspconfig
synced 2025-03-29 23:16:21 +00:00
Some checks are pending
docgen / docgen (push) Waiting to run
This reverts commit e118ce58da
.
It turns out `util.available_servers` is used more than anticipated, so
we revert the privatization for the time being.
Closes https://github.com/neovim/nvim-lspconfig/issues/3588
336 lines
11 KiB
Lua
336 lines
11 KiB
Lua
local M = {}
|
|
local health = require('vim.health')
|
|
|
|
local api, fn = vim.api, vim.fn
|
|
local util = require 'lspconfig.util'
|
|
|
|
local error_messages = {
|
|
cmd_not_found = 'Unable to find executable. Check your $PATH and ensure the server is installed.',
|
|
no_filetype_defined = 'No filetypes defined. Define filetypes in setup().',
|
|
root_dir_not_found = 'Not found.',
|
|
async_root_dir_function = 'Asynchronous root_dir functions are not supported by `:checkhealth lspconfig`',
|
|
}
|
|
|
|
local helptags = {
|
|
[error_messages.no_filetype_defined] = { 'lspconfig-setup' },
|
|
[error_messages.root_dir_not_found] = { 'lspconfig-root-detection' },
|
|
}
|
|
|
|
local function trim_blankspace(cmd)
|
|
local trimmed_cmd = {}
|
|
for _, str in ipairs(cmd) do
|
|
trimmed_cmd[#trimmed_cmd + 1] = str:match '^%s*(.*)'
|
|
end
|
|
return trimmed_cmd
|
|
end
|
|
|
|
local function remove_newlines(cmd)
|
|
cmd = trim_blankspace(cmd)
|
|
cmd = table.concat(cmd, ' ')
|
|
cmd = vim.split(cmd, '\n')
|
|
cmd = trim_blankspace(cmd)
|
|
cmd = table.concat(cmd, ' ')
|
|
return cmd
|
|
end
|
|
|
|
--- Tries to run `cmd` and kills it if it takes more than `ms` milliseconds.
|
|
---
|
|
--- This avoids hangs if a command waits for input (especially LSP servers).
|
|
---
|
|
--- @param cmd string[]
|
|
--- @return string? # Command output (stdout+stderr), or `nil` on timeout or nonzero exit.
|
|
local function try_get_cmd_output(cmd)
|
|
local out = nil
|
|
local function on_data(_, data, _)
|
|
out = (out or '') .. table.concat(data, '\n')
|
|
end
|
|
local chanid = vim.fn.jobstart(cmd, {
|
|
-- cwd = ?,
|
|
stdout_buffered = true,
|
|
stderr_buffered = true,
|
|
on_stdout = on_data,
|
|
on_stderr = on_data,
|
|
})
|
|
local rv = vim.fn.jobwait({ chanid }, 300)
|
|
vim.fn.jobstop(chanid)
|
|
return rv[1] == 0 and out or nil
|
|
end
|
|
|
|
--- Finds a "x.y.z" version string from the output of `prog` after attempting to invoke it with `--version`, `-v`,
|
|
--- `--help`, etc.
|
|
---
|
|
--- Returns the whole line.
|
|
---
|
|
--- If a version string is not found, returns the concatenated output.
|
|
---
|
|
--- @param prog string
|
|
local function try_fmt_version(prog)
|
|
local all = nil --- Collected output from all attempts.
|
|
local tried = '' --- Attempted commands.
|
|
for _, v_arg in ipairs { '--version', '-version', 'version', '--help' } do
|
|
local cmd = { prog, v_arg }
|
|
local out = try_get_cmd_output(cmd)
|
|
all = out and ('%s\n%s'):format(all or '', out) or all
|
|
local v_line = out and out:match('[^\r\n]*%d+%.[0-9.]+[^\r\n]*') or nil
|
|
if v_line then
|
|
return ('`%s`'):format(vim.trim(v_line))
|
|
end
|
|
tried = tried .. ('`%s %s`\n'):format(prog, v_arg)
|
|
end
|
|
|
|
all = all and vim.trim(all:sub(1, 80):gsub('[\r\n]', ' ')) .. '…' or '?'
|
|
return ('`%s` (Failed to get version) Tried:\n%s'):format(all, tried)
|
|
end
|
|
|
|
--- Prettify a path for presentation.
|
|
local function fmtpath(p)
|
|
if vim.startswith(p, 'Running') or vim.startswith(p, 'Not') then
|
|
return p
|
|
end
|
|
local isdir = 0 ~= vim.fn.isdirectory(vim.fn.expand(p))
|
|
local r = vim.fn.fnamemodify(p, ':~')
|
|
-- Force directory path to end with "/".
|
|
-- Bonus: avoids wrong highlighting for "~" (because :checkhealth currently uses ft=help).
|
|
return r .. ((isdir and not r:find('[/\\\\]%s*$')) and '/' or '')
|
|
end
|
|
|
|
local cmd_type = {
|
|
['function'] = function(_)
|
|
return '<function>', 'NA'
|
|
end,
|
|
['table'] = function(config)
|
|
local cmd = remove_newlines(config.cmd)
|
|
if vim.fn.executable(config.cmd[1]) == 1 then
|
|
return cmd, 'true'
|
|
end
|
|
return cmd, error_messages.cmd_not_found
|
|
end,
|
|
}
|
|
|
|
--- Builds info displayed by both make_config_info and make_client_info.
|
|
local function make_info(config_or_client)
|
|
local info = vim.deepcopy(config_or_client)
|
|
local config = config_or_client.config and config_or_client.config or config_or_client
|
|
|
|
if config.cmd then
|
|
info.cmd_desc, info.cmd_is_executable = cmd_type[type(config.cmd)](config)
|
|
else
|
|
info.cmd_desc = 'cmd not defined'
|
|
info.cmd_is_executable = 'NA'
|
|
end
|
|
|
|
info.autostart = (config.autostart and 'true') or 'false'
|
|
info.filetypes = table.concat(config.filetypes or {}, ', ')
|
|
|
|
local version = type(config.cmd) == 'function' and '? (cmd is a function)' or try_fmt_version(config.cmd[1])
|
|
local info_lines = {
|
|
'filetypes: ' .. info.filetypes,
|
|
'cmd: ' .. fmtpath(info.cmd_desc),
|
|
('%-18s %s'):format('version:', version),
|
|
'executable: ' .. info.cmd_is_executable,
|
|
'autostart: ' .. info.autostart,
|
|
}
|
|
|
|
return info, info_lines
|
|
end
|
|
|
|
local function make_config_info(config, bufnr)
|
|
local config_info, info_lines = make_info(config)
|
|
config_info.helptags = {}
|
|
|
|
local buffer_dir = api.nvim_buf_call(bufnr, function()
|
|
return vim.fn.expand '%:p:h'
|
|
end)
|
|
|
|
if config.get_root_dir then
|
|
local root_dir
|
|
local co = coroutine.create(function()
|
|
local status, err = pcall(function()
|
|
root_dir = config.get_root_dir(buffer_dir)
|
|
end)
|
|
if not status then
|
|
vim.notify(('[lspconfig] unhandled error: %s'):format(tostring(err), vim.log.levels.WARN))
|
|
end
|
|
end)
|
|
coroutine.resume(co)
|
|
if root_dir then
|
|
config_info.root_dir = root_dir
|
|
elseif coroutine.status(co) == 'suspended' then
|
|
config_info.root_dir = error_messages.async_root_dir_function
|
|
else
|
|
config_info.root_dir = error_messages.root_dir_not_found
|
|
end
|
|
else
|
|
config_info.root_dir = error_messages.root_dir_not_found
|
|
vim.list_extend(config_info.helptags, helptags[error_messages.root_dir_not_found])
|
|
end
|
|
|
|
local handlers = vim.tbl_keys(config.handlers)
|
|
config_info.handlers = table.concat(handlers, ', ')
|
|
|
|
table.insert(info_lines, 1, 'Config: ' .. config_info.name)
|
|
table.insert(info_lines, 'root directory: ' .. fmtpath(config_info.root_dir))
|
|
if #handlers > 0 then
|
|
table.insert(info_lines, 'custom handlers: ' .. config_info.handlers)
|
|
end
|
|
|
|
if vim.tbl_count(config_info.helptags) > 0 then
|
|
local help = vim.tbl_map(function(helptag)
|
|
return string.format(':h %s', helptag)
|
|
end, config_info.helptags)
|
|
table.insert(info_lines, 'Refer to ' .. table.concat(help, ', ') .. ' for help.')
|
|
end
|
|
|
|
return table.concat(info_lines, '\n')
|
|
end
|
|
|
|
---@param client vim.lsp.Client
|
|
---@param fname string
|
|
local function make_client_info(client, fname)
|
|
local client_info, info_lines = make_info(client)
|
|
|
|
local workspace_folders = client.workspace_folders
|
|
fname = vim.fs.normalize(vim.loop.fs_realpath(fname) or fn.fnamemodify(fn.resolve(fname), ':p'))
|
|
|
|
if workspace_folders then
|
|
for _, schema in ipairs(workspace_folders) do
|
|
local matched = true
|
|
local root_dir = vim.loop.fs_realpath(schema.name)
|
|
if root_dir == nil or fname:sub(1, root_dir:len()) ~= root_dir then
|
|
matched = false
|
|
end
|
|
|
|
if matched then
|
|
client_info.root_dir = schema.name
|
|
break
|
|
end
|
|
end
|
|
end
|
|
if not client_info.root_dir then
|
|
client_info.root_dir = 'Running in single file mode.'
|
|
end
|
|
|
|
client_info.attached_bufs = table.concat(vim.lsp.get_buffers_by_client_id(client.id), ', ')
|
|
|
|
info_lines = vim.list_extend({
|
|
('Client: `%s` (id: %s, bufnr: [%s])'):format(client.name, client.id, client_info.attached_bufs),
|
|
'root directory: ' .. fmtpath(client_info.root_dir),
|
|
}, info_lines)
|
|
|
|
return table.concat(info_lines, '\n')
|
|
end
|
|
|
|
local function check_lspconfig(bufnr)
|
|
bufnr = (bufnr and bufnr ~= -1) and bufnr or nil
|
|
|
|
health.start('LSP configs active in this session (globally)')
|
|
health.info('Configured servers: ' .. table.concat(util.available_servers(), ', '))
|
|
local deprecated_servers = {}
|
|
for server_name, deprecate in pairs(require('lspconfig').server_aliases()) do
|
|
table.insert(deprecated_servers, ('%s -> %s'):format(server_name, deprecate.to))
|
|
end
|
|
if #deprecated_servers == 0 then
|
|
health.ok('Deprecated servers: (none)')
|
|
else
|
|
health.warn('Deprecated servers: ' .. table.concat(deprecated_servers, ', '))
|
|
end
|
|
|
|
local buf_clients = not bufnr and {} or util.get_lsp_clients { bufnr = bufnr }
|
|
local clients = util.get_lsp_clients()
|
|
local buffer_filetype = bufnr and vim.fn.getbufvar(bufnr, '&filetype') or '(invalid buffer)'
|
|
local fname = bufnr and api.nvim_buf_get_name(bufnr) or '(invalid buffer)'
|
|
|
|
local buf_client_ids = {}
|
|
for _, client in ipairs(buf_clients) do
|
|
buf_client_ids[#buf_client_ids + 1] = client.id
|
|
end
|
|
|
|
local other_active_clients = {}
|
|
for _, client in ipairs(clients) do
|
|
if not vim.tbl_contains(buf_client_ids, client.id) then
|
|
other_active_clients[#other_active_clients + 1] = client
|
|
end
|
|
end
|
|
|
|
health.start(('LSP configs active in this buffer (bufnr: %s)'):format(bufnr or '(invalid buffer)'))
|
|
health.info('Language client log: ' .. fmtpath(vim.lsp.get_log_path()))
|
|
health.info(('Detected filetype: `%s`'):format(buffer_filetype))
|
|
health.info(('%d client(s) attached to this buffer'):format(#vim.tbl_keys(buf_clients)))
|
|
for _, client in ipairs(buf_clients) do
|
|
health.info(make_client_info(client, fname))
|
|
end
|
|
|
|
if not vim.tbl_isempty(other_active_clients) then
|
|
health.info(('%s active client(s) not attached to this buffer:'):format(#other_active_clients))
|
|
for _, client in ipairs(other_active_clients) do
|
|
health.info(make_client_info(client, fname))
|
|
end
|
|
end
|
|
|
|
local other_matching_configs = not bufnr and {} or util.get_other_matching_providers(buffer_filetype)
|
|
if not vim.tbl_isempty(other_matching_configs) then
|
|
health.info(('Other clients that match the "%s" filetype:'):format(buffer_filetype))
|
|
for _, config in ipairs(other_matching_configs) do
|
|
health.info(make_config_info(config, bufnr))
|
|
end
|
|
end
|
|
|
|
vim.fn.matchadd(
|
|
'Error',
|
|
error_messages.no_filetype_defined
|
|
.. '.\\|'
|
|
.. 'cmd not defined\\|'
|
|
.. error_messages.cmd_not_found
|
|
.. '\\|'
|
|
.. error_messages.root_dir_not_found
|
|
)
|
|
|
|
-- TODO(justimk): enhance :checkhealth's highlighting instead of doing this only for lspconfig.
|
|
vim.cmd [[
|
|
syn keyword String true
|
|
syn keyword Error false
|
|
]]
|
|
|
|
return buf_clients, other_matching_configs
|
|
end
|
|
|
|
local function check_lspdocs(buf_clients, other_matching_configs)
|
|
health.start('Docs for active configs:')
|
|
|
|
local function fmt_doc(config)
|
|
local lines = {}
|
|
if not config then
|
|
return lines
|
|
end
|
|
local desc = vim.tbl_get(config, 'config_def', 'docs', 'description')
|
|
if desc then
|
|
lines[#lines + 1] = string.format('%s docs: >markdown', config.name)
|
|
lines[#lines + 1] = ''
|
|
vim.list_extend(lines, vim.split(desc, '\n'))
|
|
lines[#lines + 1] = ''
|
|
end
|
|
return lines
|
|
end
|
|
|
|
for _, client in ipairs(buf_clients) do
|
|
local config = require('lspconfig.configs')[client.name]
|
|
health.info(table.concat(fmt_doc(config), '\n'))
|
|
end
|
|
|
|
for _, config in ipairs(other_matching_configs) do
|
|
health.info(table.concat(fmt_doc(config), '\n'))
|
|
end
|
|
end
|
|
|
|
function M.check()
|
|
-- XXX: create "q" mapping until :checkhealth has this feature in Nvim stable.
|
|
vim.cmd [[nnoremap <buffer> q <c-w>q]]
|
|
|
|
-- XXX: :checkhealth switches to its buffer before invoking the healthcheck(s).
|
|
local orig_bufnr = vim.fn.bufnr('#')
|
|
local buf_clients, other_matching_configs = check_lspconfig(orig_bufnr)
|
|
check_lspdocs(buf_clients, other_matching_configs)
|
|
end
|
|
|
|
return M
|