aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim/provider/health.lua
diff options
context:
space:
mode:
Diffstat (limited to 'runtime/lua/vim/provider/health.lua')
-rw-r--r--runtime/lua/vim/provider/health.lua259
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, ' '),