diff options
Diffstat (limited to 'runtime/lua/vim/provider/health.lua')
-rw-r--r-- | runtime/lua/vim/provider/health.lua | 259 |
1 files changed, 187 insertions, 72 deletions
diff --git a/runtime/lua/vim/provider/health.lua b/runtime/lua/vim/provider/health.lua index 63e0da448a..47c2080e3c 100644 --- a/runtime/lua/vim/provider/health.lua +++ b/runtime/lua/vim/provider/health.lua @@ -1,8 +1,114 @@ local health = vim.health -local iswin = vim.uv.os_uname().sysname == 'Windows_NT' +local iswin = vim.fn.has('win32') == 1 local M = {} +local function cmd_ok(cmd) + local out = vim.fn.system(cmd) + return vim.v.shell_error == 0, out +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 + +-- 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 + +--- @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) +local function 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.uv.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.uv.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 vim.trim(vim.fn.system(cmd)), shell_error_code +end + +---@param provider string +local function provider_disabled(provider) + local loaded_var = 'loaded_' .. provider .. '_provider' + local v = vim.g[loaded_var] + if v == 0 then + health.info('Disabled (' .. loaded_var .. '=' .. v .. ').') + return true + end + return false +end + local function clipboard() health.start('Clipboard (optional)') @@ -10,7 +116,7 @@ local function clipboard() os.getenv('TMUX') and vim.fn.executable('tmux') == 1 and vim.fn.executable('pbpaste') == 1 - and not health._cmd_ok('pbpaste') + and not cmd_ok('pbpaste') then local tmux_version = string.match(vim.fn.system('tmux -V'), '%d+%.%d+') local advice = { @@ -20,9 +126,9 @@ local function clipboard() health.error('pbcopy does not work with tmux version: ' .. tmux_version, advice) end - local clipboard_tool = vim.fn['provider#clipboard#Executable']() + local clipboard_tool = vim.fn['provider#clipboard#Executable']() ---@type string if vim.g.clipboard ~= nil and clipboard_tool == '' then - local error_message = vim.fn['provider#clipboard#Error']() + local error_message = vim.fn['provider#clipboard#Error']() ---@type string health.error( error_message, "Use the example in :help g:clipboard as a template, or don't set g:clipboard at all." @@ -40,7 +146,7 @@ end local function node() health.start('Node.js provider (optional)') - if health._provider_disabled('node') then + if provider_disabled('node') then return end @@ -60,7 +166,7 @@ local function node() end -- local node_v = vim.fn.split(system({'node', '-v'}), "\n")[1] or '' - local ok, node_v = health._cmd_ok({ 'node', '-v' }) + local ok, node_v = cmd_ok({ 'node', '-v' }) health.info('Node.js: ' .. node_v) if not ok or vim.version.lt(node_v, '6.0.0') then health.warn('Nvim node.js host does not support Node ' .. node_v) @@ -73,7 +179,7 @@ local function node() ) end - local node_detect_table = vim.fn['provider#node#Detect']() + local node_detect_table = vim.fn['provider#node#Detect']() ---@type string[] local host = node_detect_table[1] if host:find('^%s*$') then health.warn('Missing "neovim" npm (or yarn, pnpm) package.', { @@ -97,7 +203,7 @@ local function node() iswin and 'cmd /c ' .. manager .. ' info neovim --json' or manager .. ' info neovim --json' ) local latest_npm - ok, latest_npm = health._cmd_ok(vim.split(latest_npm_cmd, ' ')) + ok, latest_npm = cmd_ok(vim.split(latest_npm_cmd, ' ')) if not ok or latest_npm:find('^%s$') then health.error( 'Failed to run: ' .. latest_npm_cmd, @@ -115,7 +221,7 @@ local function node() local current_npm_cmd = { 'node', host, '--version' } local current_npm - ok, current_npm = health._cmd_ok(current_npm_cmd) + ok, current_npm = cmd_ok(current_npm_cmd) if not ok then health.error( 'Failed to run: ' .. table.concat(current_npm_cmd, ' '), @@ -143,7 +249,7 @@ end local function perl() health.start('Perl provider (optional)') - if health._provider_disabled('perl') then + if provider_disabled('perl') then return end @@ -162,7 +268,7 @@ local function perl() -- we cannot use cpanm that is on the path, as it may not be for the perl -- set with g:perl_host_prog - local ok = health._cmd_ok({ perl_exec, '-W', '-MApp::cpanminus', '-e', '' }) + local ok = cmd_ok({ perl_exec, '-W', '-MApp::cpanminus', '-e', '' }) if not ok then return { perl_exec, '"App::cpanminus" module is not installed' } end @@ -174,7 +280,7 @@ local function perl() 'my $app = App::cpanminus::script->new; $app->parse_options ("--info", "-q", "Neovim::Ext"); exit $app->doit', } local latest_cpan - ok, latest_cpan = health._cmd_ok(latest_cpan_cmd) + ok, latest_cpan = cmd_ok(latest_cpan_cmd) if not ok or latest_cpan:find('^%s*$') then health.error( 'Failed to run: ' .. table.concat(latest_cpan_cmd, ' '), @@ -184,7 +290,7 @@ local function perl() elseif latest_cpan[1] == '!' then local cpanm_errs = vim.split(latest_cpan, '!') if cpanm_errs[1]:find("Can't write to ") then - local advice = {} + local advice = {} ---@type string[] for i = 2, #cpanm_errs do advice[#advice + 1] = cpanm_errs[i] end @@ -197,7 +303,7 @@ local function perl() return end end - latest_cpan = vim.fn.matchstr(latest_cpan, [[\(\.\?\d\)\+]]) + latest_cpan = tostring(vim.fn.matchstr(latest_cpan, [[\(\.\?\d\)\+]])) if latest_cpan:find('^%s*$') then health.error('Cannot parse version number from cpanm output: ' .. latest_cpan) return @@ -205,7 +311,7 @@ local function perl() local current_cpan_cmd = { perl_exec, '-W', '-MNeovim::Ext', '-e', 'print $Neovim::Ext::VERSION' } local current_cpan - ok, current_cpan = health._cmd_ok(current_cpan_cmd) + ok, current_cpan = cmd_ok(current_cpan_cmd) if not ok then health.error( 'Failed to run: ' .. table.concat(current_cpan_cmd, ' '), @@ -243,9 +349,11 @@ local function python_exepath(invocation) return vim.fs.normalize(vim.trim(p.stdout)) end --- Check if pyenv is available and a valid pyenv root can be found, then return --- their respective paths. If either of those is invalid, return two empty --- strings, effectively ignoring pyenv. +--- Check if pyenv is available and a valid pyenv root can be found, then return +--- their respective paths. If either of those is invalid, return two empty +--- strings, effectively ignoring pyenv. +--- +--- @return [string, string] local function check_for_pyenv() local pyenv_path = vim.fn.resolve(vim.fn.exepath('pyenv')) @@ -258,7 +366,17 @@ local function check_for_pyenv() local pyenv_root = vim.fn.resolve(os.getenv('PYENV_ROOT') or '') if pyenv_root == '' then - pyenv_root = vim.fn.system({ pyenv_path, 'root' }) + local p = vim.system({ pyenv_path, 'root' }):wait() + if p.code ~= 0 then + local message = string.format( + 'pyenv: Failed to infer the root of pyenv by running `%s root` : %s. Ignoring pyenv for all following checks.', + pyenv_path, + p.stderr + ) + health.warn(message) + return { '', '' } + end + pyenv_root = vim.trim(p.stdout) health.info('pyenv: $PYENV_ROOT is not set. Infer from `pyenv root`.') end @@ -288,24 +406,29 @@ local function check_bin(bin) return true end --- Fetch the contents of a URL. +--- Fetch the contents of a URL. +--- +--- @param url string local function download(url) local has_curl = vim.fn.executable('curl') == 1 if has_curl and vim.fn.system({ 'curl', '-V' }):find('Protocols:.*https') then - local out, rc = health._system({ 'curl', '-sL', url }, { stderr = true, ignore_error = true }) + local out, rc = system({ 'curl', '-sL', url }, { stderr = true, ignore_error = true }) if rc ~= 0 then return 'curl error with ' .. url .. ': ' .. rc else return out end elseif vim.fn.executable('python') == 1 then - local script = "try:\n\ - from urllib.request import urlopen\n\ - except ImportError:\n\ - from urllib2 import urlopen\n\ - response = urlopen('" .. url .. "')\n\ - print(response.read().decode('utf8'))\n" - local out, rc = health._system({ 'python', '-c', script }) + local script = ([[ +try: + from urllib.request import urlopen +except ImportError: + from urllib2 import urlopen + +response = urlopen('%s') +print(response.read().decode('utf8')) +]]):format(url) + local out, rc = system({ 'python', '-c', script }) if out == '' and rc ~= 0 then return 'python urllib.request error: ' .. rc else @@ -323,25 +446,24 @@ local function download(url) return message end --- Get the latest Nvim Python client (pynvim) version from PyPI. +--- Get the latest Nvim Python client (pynvim) version from PyPI. local function latest_pypi_version() local pypi_version = 'unable to get pypi response' local pypi_response = download('https://pypi.python.org/pypi/pynvim/json') if pypi_response ~= '' then local pcall_ok, output = pcall(vim.fn.json_decode, pypi_response) - local pypi_data - if pcall_ok then - pypi_data = output - else + if not pcall_ok then return 'error: ' .. pypi_response end + local pypi_data = output local pypi_element = pypi_data['info'] or {} pypi_version = pypi_element['version'] or 'unable to parse' end return pypi_version end +--- @param s string local function is_bad_response(s) local lower = s:lower() return vim.startswith(lower, 'unable') @@ -349,20 +471,22 @@ local function is_bad_response(s) or vim.startswith(lower, 'outdated') end --- Get version information using the specified interpreter. The interpreter is --- used directly in case breaking changes were introduced since the last time --- Nvim's Python client was updated. --- --- Returns: { --- {python executable version}, --- {current nvim version}, --- {current pypi nvim status}, --- {installed version status} --- } +--- Get version information using the specified interpreter. The interpreter is +--- used directly in case breaking changes were introduced since the last time +--- Nvim's Python client was updated. +--- +--- @param python string +--- +--- Returns: { +--- {python executable version}, +--- {current nvim version}, +--- {current pypi nvim status}, +--- {installed version status} +--- } local function version_info(python) local pypi_version = latest_pypi_version() - local python_version, rc = health._system({ + local python_version, rc = system({ python, '-c', 'import sys; print(".".join(str(x) for x in sys.version_info[:3]))', @@ -373,7 +497,7 @@ local function version_info(python) end local nvim_path - nvim_path, rc = health._system({ + nvim_path, rc = system({ python, '-c', 'import sys; sys.path = [p for p in sys.path if p != ""]; import neovim; print(neovim.__file__)', @@ -398,7 +522,7 @@ local function version_info(python) -- Try to get neovim.VERSION (added in 0.1.11dev). local nvim_version - nvim_version, rc = health._system({ + nvim_version, rc = system({ python, '-c', 'from neovim import VERSION as v; print("{}.{}.{}{}".format(v.major, v.minor, v.patch, v.prerelease))', @@ -406,9 +530,9 @@ local function version_info(python) if rc ~= 0 or nvim_version == '' then nvim_version = 'unable to find pynvim module version' local base = vim.fs.basename(nvim_path) - local metas = vim.fn.glob(base .. '-*/METADATA', 1, 1) - vim.list_extend(metas, vim.fn.glob(base .. '-*/PKG-INFO', 1, 1)) - vim.list_extend(metas, vim.fn.glob(base .. '.egg-info/PKG-INFO', 1, 1)) + local metas = vim.fn.glob(base .. '-*/METADATA', true, 1) + vim.list_extend(metas, vim.fn.glob(base .. '-*/PKG-INFO', true, 1)) + vim.list_extend(metas, vim.fn.glob(base .. '.egg-info/PKG-INFO', true, 1)) metas = table.sort(metas, compare) if metas and next(metas) ~= nil then @@ -438,14 +562,13 @@ end local function python() health.start('Python 3 provider (optional)') - local pyname = 'python3' ---@type string? local python_exe = '' local virtual_env = os.getenv('VIRTUAL_ENV') local venv = virtual_env and vim.fn.resolve(virtual_env) or '' - local host_prog_var = pyname .. '_host_prog' - local python_multiple = {} + local host_prog_var = 'python3_host_prog' + local python_multiple = {} ---@type string[] - if health._provider_disabled(pyname) then + if provider_disabled('python3') then return end @@ -458,8 +581,7 @@ local function python() health.info(message) end - local pythonx_warnings - pyname, pythonx_warnings = vim.provider.python.detect_by_module('neovim') + local pyname, pythonx_warnings = vim.provider.python.detect_by_module('neovim') if not pyname then health.warn( @@ -487,7 +609,7 @@ local function python() end if pyenv ~= '' then - python_exe = health._system({ pyenv, 'which', pyname }, { stderr = true }) + python_exe = system({ pyenv, 'which', pyname }, { stderr = true }) if python_exe == '' then health.warn('pyenv could not find ' .. pyname .. '.') end @@ -547,12 +669,7 @@ local function python() ) health.warn('pyenv is not set up optimally.', advice) elseif venv ~= '' then - local venv_root - if pyenv_root ~= '' then - venv_root = pyenv_root - else - venv_root = vim.fs.dirname(venv) - end + local venv_root = pyenv_root ~= '' and pyenv_root or vim.fs.dirname(venv) if vim.startswith(vim.fn.resolve(python_exe), venv_root .. '/') then local advice = string.format( @@ -637,9 +754,9 @@ local function python() health.ok('no $VIRTUAL_ENV') return end - local errors = {} + local errors = {} ---@type string[] -- Keep hints as dict keys in order to discard duplicates. - local hints = {} + local hints = {} ---@type table<string, boolean> -- The virtualenv should contain some Python executables, and those -- executables should be first both on Nvim's $PATH and the $PATH of -- subshells launched from Nvim. @@ -647,7 +764,7 @@ local function python() local venv_bins = vim.fn.glob(string.format('%s/%s/python*', virtual_env, bin_dir), true, true) venv_bins = vim.tbl_filter(function(v) -- XXX: Remove irrelevant executables found in bin/. - return not v:match('python%-config') + return not v:match('python.*%-config') end, venv_bins) if vim.tbl_count(venv_bins) > 0 then for _, venv_bin in pairs(venv_bins) do @@ -710,9 +827,7 @@ local function python() health.info(msg) health.info( 'Python version: ' - .. health._system( - 'python -c "import platform, sys; sys.stdout.write(platform.python_version())"' - ) + .. system('python -c "import platform, sys; sys.stdout.write(platform.python_version())"') ) health.ok('$VIRTUAL_ENV provides :!python.') end @@ -721,7 +836,7 @@ end local function ruby() health.start('Ruby provider (optional)') - if health._provider_disabled('ruby') then + if provider_disabled('ruby') then return end @@ -732,7 +847,7 @@ local function ruby() ) return end - health.info('Ruby: ' .. health._system({ 'ruby', '-v' })) + health.info('Ruby: ' .. system({ 'ruby', '-v' })) local host, _ = vim.provider.ruby.detect() if (not host) or host:find('^%s*$') then @@ -748,7 +863,7 @@ local function ruby() health.info('Host: ' .. host) local latest_gem_cmd = (iswin and 'cmd /c gem list -ra "^^neovim$"' or 'gem list -ra ^neovim$') - local ok, latest_gem = health._cmd_ok(vim.split(latest_gem_cmd, ' ')) + local ok, latest_gem = cmd_ok(vim.split(latest_gem_cmd, ' ')) if not ok or latest_gem:find('^%s*$') then health.error( 'Failed to run: ' .. latest_gem_cmd, @@ -761,7 +876,7 @@ local function ruby() local current_gem_cmd = { host, '--version' } local current_gem - ok, current_gem = health._cmd_ok(current_gem_cmd) + ok, current_gem = cmd_ok(current_gem_cmd) if not ok then health.error( 'Failed to run: ' .. table.concat(current_gem_cmd, ' '), |