diff options
Diffstat (limited to 'runtime/lua/vim/health.lua')
-rw-r--r-- | runtime/lua/vim/health.lua | 235 |
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 |