aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim/health.lua
diff options
context:
space:
mode:
Diffstat (limited to 'runtime/lua/vim/health.lua')
-rw-r--r--runtime/lua/vim/health.lua235
1 files changed, 200 insertions, 35 deletions
diff --git a/runtime/lua/vim/health.lua b/runtime/lua/vim/health.lua
index 6e47a22d03..f6f7abef8f 100644
--- a/runtime/lua/vim/health.lua
+++ b/runtime/lua/vim/health.lua
@@ -1,8 +1,8 @@
local M = {}
-local s_output = {}
+local s_output = {} ---@type string[]
--- Returns the fold text of the current healthcheck section
+--- Returns the fold text of the current healthcheck section
function M.foldtext()
local foldtext = vim.fn.foldtext()
@@ -36,12 +36,13 @@ function M.foldtext()
return vim.b.failedchecks[foldtext] and '+WE' .. foldtext:sub(4) or foldtext
end
--- From a path return a list [{name}, {func}, {type}] representing a healthcheck
+--- @param path string path to search for the healthcheck
+--- @return string[] { name, func, type } representing a healthcheck
local function filepath_to_healthcheck(path)
path = vim.fs.normalize(path)
- local name
- local func
- local filetype
+ local name --- @type string
+ local func --- @type string
+ local filetype --- @type string
if path:find('vim$') then
name = vim.fs.basename(path):gsub('%.vim$', '')
func = 'health#' .. name .. '#check'
@@ -50,10 +51,10 @@ local function filepath_to_healthcheck(path)
local subpath = path:gsub('.*lua/', '')
if vim.fs.basename(subpath) == 'health.lua' then
-- */health.lua
- name = vim.fs.dirname(subpath)
+ name = assert(vim.fs.dirname(subpath))
else
-- */health/init.lua
- name = vim.fs.dirname(vim.fs.dirname(subpath))
+ name = assert(vim.fs.dirname(assert(vim.fs.dirname(subpath))))
end
name = name:gsub('/', '.')
@@ -63,11 +64,12 @@ local function filepath_to_healthcheck(path)
return { name, func, filetype }
end
--- Returns { {name, func, type}, ... } representing healthchecks
+--- @param plugin_names string
+--- @return table<any,string[]> { {name, func, type}, ... } representing healthchecks
local function get_healthcheck_list(plugin_names)
- local healthchecks = {}
- plugin_names = vim.split(plugin_names, ' ')
- for _, p in pairs(plugin_names) do
+ local healthchecks = {} --- @type table<any,string[]>
+ local plugin_names_list = vim.split(plugin_names, ' ')
+ for _, p in pairs(plugin_names_list) do
-- support vim/lsp/health{/init/}.lua as :checkhealth vim.lsp
p = p:gsub('%.', '/')
@@ -83,7 +85,7 @@ local function get_healthcheck_list(plugin_names)
if vim.tbl_count(paths) == 0 then
healthchecks[#healthchecks + 1] = { p, '', '' } -- healthcheck not found
else
- local unique_paths = {}
+ local unique_paths = {} --- @type table<string, boolean>
for _, v in pairs(paths) do
unique_paths[v] = true
end
@@ -100,10 +102,11 @@ local function get_healthcheck_list(plugin_names)
return healthchecks
end
--- Returns {name: [func, type], ..} representing healthchecks
+--- @param plugin_names string
+--- @return table<string, string[]> {name: [func, type], ..} representing healthchecks
local function get_healthcheck(plugin_names)
local health_list = get_healthcheck_list(plugin_names)
- local healthchecks = {}
+ local healthchecks = {} --- @type table<string, string[]>
for _, c in pairs(health_list) do
if c[1] ~= 'vim' then
healthchecks[c[1]] = { c[2], c[3] }
@@ -113,7 +116,11 @@ local function get_healthcheck(plugin_names)
return healthchecks
end
--- Indents lines *except* line 1 of a string if it contains newlines.
+--- Indents lines *except* line 1 of a string if it contains newlines.
+---
+--- @param s string
+--- @param columns integer
+--- @return string
local function indent_after_line1(s, columns)
local lines = vim.split(s, '\n')
local indent = string.rep(' ', columns)
@@ -123,13 +130,20 @@ local function indent_after_line1(s, columns)
return table.concat(lines, '\n')
end
--- Changes ':h clipboard' to ':help |clipboard|'.
+--- Changes ':h clipboard' to ':help |clipboard|'.
+---
+--- @param s string
+--- @return string
local function help_to_link(s)
return vim.fn.substitute(s, [[\v:h%[elp] ([^|][^"\r\n ]+)]], [[:help |\1|]], [[g]])
end
--- Format a message for a specific report item.
--- Variable args: Optional advice (string or list)
+--- Format a message for a specific report item.
+---
+--- @param status string
+--- @param msg string
+--- @param ... string|string[] Optional advice
+--- @return string
local function format_report_message(status, msg, ...)
local output = '- ' .. status
if status ~= '' then
@@ -159,42 +173,54 @@ local function format_report_message(status, msg, ...)
return help_to_link(output)
end
+--- @param output string
local function collect_output(output)
vim.list_extend(s_output, vim.split(output, '\n'))
end
--- Starts a new report.
+--- Starts a new report.
+---
+--- @param name string
function M.start(name)
local input = string.format('\n%s ~', name)
collect_output(input)
end
--- Reports a message in the current section.
+--- Reports a message in the current section.
+---
+--- @param msg string
function M.info(msg)
local input = format_report_message('', msg)
collect_output(input)
end
--- Reports a successful healthcheck.
+--- Reports a successful healthcheck.
+---
+--- @param msg string
function M.ok(msg)
local input = format_report_message('OK', msg)
collect_output(input)
end
--- Reports a health warning.
--- ...: Optional advice (string or table)
+--- Reports a health warning.
+---
+--- @param msg string
+--- @param ... string|string[] Optional advice
function M.warn(msg, ...)
local input = format_report_message('WARNING', msg, ...)
collect_output(input)
end
--- Reports a failed healthcheck.
--- ...: Optional advice (string or table)
+--- Reports a failed healthcheck.
+---
+--- @param msg string
+--- @param ... string|string[] Optional advice
function M.error(msg, ...)
local input = format_report_message('ERROR', msg, ...)
collect_output(input)
end
+--- @param type string
local function deprecate(type)
local before = string.format('vim.health.report_%s()', type)
local after = string.format('vim.health.%s()', type)
@@ -206,27 +232,149 @@ local function deprecate(type)
vim.print('Running healthchecks...')
end
+--- @deprecated
+--- @param name string
function M.report_start(name)
deprecate('start')
M.start(name)
end
+
+--- @deprecated
+--- @param msg string
function M.report_info(msg)
deprecate('info')
M.info(msg)
end
+
+--- @deprecated
+--- @param msg string
function M.report_ok(msg)
deprecate('ok')
M.ok(msg)
end
+
+--- @deprecated
+--- @param msg string
function M.report_warn(msg, ...)
deprecate('warn')
M.warn(msg, ...)
end
+
+--- @deprecated
+--- @param msg string
function M.report_error(msg, ...)
deprecate('error')
M.error(msg, ...)
end
+function M.provider_disabled(provider)
+ local loaded_var = 'loaded_' .. provider .. '_provider'
+ local v = vim.g[loaded_var]
+ if v == 0 then
+ M.info('Disabled (' .. loaded_var .. '=' .. v .. ').')
+ return true
+ end
+ return false
+end
+
+-- Handler for s:system() function.
+local function system_handler(self, _, data, event)
+ if event == 'stderr' then
+ if self.add_stderr_to_output then
+ self.output = self.output .. table.concat(data, '')
+ else
+ self.stderr = self.stderr .. table.concat(data, '')
+ end
+ elseif event == 'stdout' then
+ self.output = self.output .. table.concat(data, '')
+ end
+end
+
+-- Attempts to construct a shell command from an args list.
+-- Only for display, to help users debug a failed command.
+local function shellify(cmd)
+ if type(cmd) ~= 'table' then
+ return cmd
+ end
+ local escaped = {}
+ for i, v in ipairs(cmd) do
+ if v:match('[^A-Za-z_/.-]') then
+ escaped[i] = vim.fn.shellescape(v)
+ else
+ escaped[i] = v
+ end
+ end
+ return table.concat(escaped, ' ')
+end
+
+function M.cmd_ok(cmd)
+ local out = vim.fn.system(cmd)
+ return vim.v.shell_error == 0, out
+end
+
+--- Run a system command and timeout after 30 seconds.
+---
+--- @param cmd table List of command arguments to execute
+--- @param args? table Optional arguments:
+--- - stdin (string): Data to write to the job's stdin
+--- - stderr (boolean): Append stderr to stdout
+--- - ignore_error (boolean): If true, ignore error output
+--- - timeout (number): Number of seconds to wait before timing out (default 30)
+function M.system(cmd, args)
+ args = args or {}
+ local stdin = args.stdin or ''
+ local stderr = vim.F.if_nil(args.stderr, false)
+ local ignore_error = vim.F.if_nil(args.ignore_error, false)
+
+ local shell_error_code = 0
+ local opts = {
+ add_stderr_to_output = stderr,
+ output = '',
+ stderr = '',
+ on_stdout = system_handler,
+ on_stderr = system_handler,
+ on_exit = function(_, data)
+ shell_error_code = data
+ end,
+ }
+ local jobid = vim.fn.jobstart(cmd, opts)
+
+ if jobid < 1 then
+ local message =
+ string.format('Command error (job=%d): %s (in %s)', jobid, shellify(cmd), vim.loop.cwd())
+ error(message)
+ return opts.output, 1
+ end
+
+ if stdin:find('^%s$') then
+ vim.fn.chansend(jobid, stdin)
+ end
+
+ local res = vim.fn.jobwait({ jobid }, vim.F.if_nil(args.timeout, 30) * 1000)
+ if res[1] == -1 then
+ error('Command timed out: ' .. shellify(cmd))
+ vim.fn.jobstop(jobid)
+ elseif shell_error_code ~= 0 and not ignore_error then
+ local emsg = string.format(
+ 'Command error (job=%d, exit code %d): %s (in %s)',
+ jobid,
+ shell_error_code,
+ shellify(cmd),
+ vim.loop.cwd()
+ )
+ if opts.output:find('%S') then
+ emsg = string.format('%s\noutput: %s', emsg, opts.output)
+ end
+ if opts.stderr:find('%S') then
+ emsg = string.format('%s\nstderr: %s', emsg, opts.stderr)
+ end
+ error(emsg)
+ end
+
+ -- return opts.output
+ return vim.trim(vim.fn.system(cmd)), shell_error_code
+end
+
local path2name = function(path)
if path:match('%.lua$') then
-- Lua: transform "../lua/vim/lsp/health.lua" into "vim.lsp"
@@ -251,7 +399,7 @@ local path2name = function(path)
end
local PATTERNS = { '/autoload/health/*.vim', '/lua/**/**/health.lua', '/lua/**/**/health/init.lua' }
--- :checkhealth completion function used by ex_getln.c get_healthcheck_names()
+--- :checkhealth completion function used by cmdexpand.c get_healthcheck_names()
M._complete = function()
local names = vim.tbl_flatten(vim.tbl_map(function(pattern)
return vim.tbl_map(path2name, vim.api.nvim_get_runtime_file(pattern, true))
@@ -266,16 +414,25 @@ M._complete = function()
return vim.tbl_keys(unique)
end
--- Runs the specified healthchecks.
--- Runs all discovered healthchecks if plugin_names is empty.
-function M._check(plugin_names)
+--- Runs the specified healthchecks.
+--- Runs all discovered healthchecks if plugin_names is empty.
+---
+--- @param mods string command modifiers that affect splitting a window.
+--- @param plugin_names string glob of plugin names, split on whitespace. For example, using
+--- `:checkhealth vim.* nvim` will healthcheck `vim.lsp`, `vim.treesitter`
+--- and `nvim` modules.
+function M._check(mods, plugin_names)
local healthchecks = plugin_names == '' and get_healthcheck('*') or get_healthcheck(plugin_names)
- -- Create buffer and open in a tab, unless this is the default buffer when Nvim starts.
local emptybuf = vim.fn.bufnr('$') == 1 and vim.fn.getline(1) == '' and 1 == vim.fn.line('$')
- local mod = emptybuf and 'buffer' or 'tab sbuffer'
+
+ -- When no command modifiers are used:
+ -- - If the current buffer is empty, open healthcheck directly.
+ -- - If not specified otherwise open healthcheck in a tab.
+ local buf_cmd = #mods > 0 and (mods .. ' sbuffer') or emptybuf and 'buffer' or 'tab sbuffer'
+
local bufnr = vim.api.nvim_create_buf(true, true)
- vim.cmd(mod .. ' ' .. bufnr)
+ vim.cmd(buf_cmd .. ' ' .. bufnr)
if vim.fn.bufexists('health://') == 1 then
vim.cmd.bwipe('health://')
@@ -283,7 +440,8 @@ function M._check(plugin_names)
vim.cmd.file('health://')
vim.cmd.setfiletype('checkhealth')
- if healthchecks == nil or next(healthchecks) == nil then
+ -- This should only happen when doing `:checkhealth vim`
+ if next(healthchecks) == nil then
vim.fn.setline(1, 'ERROR: No healthchecks found.')
return
end
@@ -319,7 +477,7 @@ function M._check(plugin_names)
local header = { string.rep('=', 78), name .. ': ' .. func, '' }
-- remove empty line after header from report_start
if s_output[1] == '' then
- local tmp = {}
+ local tmp = {} ---@type string[]
for i = 2, #s_output do
tmp[#tmp + 1] = s_output[i]
end
@@ -339,4 +497,11 @@ function M._check(plugin_names)
vim.print('')
end
+local fn_bool = function(key)
+ return function(...)
+ return vim.fn[key](...) == 1
+ end
+end
+M.executable = fn_bool('executable')
+
return M