neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

health.lua (30827B)


      1 local health = vim.health
      2 local iswin = vim.fn.has('win32') == 1
      3 
      4 local M = {}
      5 
      6 local function cmd_ok(cmd)
      7  local result = vim.system(cmd, { text = true }):wait()
      8  return result.code == 0, result.stdout
      9 end
     10 
     11 local function cli_version(cmd)
     12  local ok, out = cmd_ok(cmd)
     13  return ok, vim.version.parse(out, { strict = false })
     14 end
     15 
     16 -- Attempts to construct a shell command from an args list.
     17 -- Only for display, to help users debug a failed command.
     18 --- @param cmd string|string[]
     19 local function shellify(cmd)
     20  if type(cmd) ~= 'table' then
     21    return cmd
     22  end
     23  local escaped = {} --- @type string[]
     24  for i, v in ipairs(cmd) do
     25    escaped[i] = v:match('[^A-Za-z_/.-]') and vim.fn.shellescape(v) or v
     26  end
     27  return table.concat(escaped, ' ')
     28 end
     29 
     30 -- Handler for s:system() function.
     31 --- @param self {output: string, stderr: string, add_stderr_to_output: boolean}
     32 local function system_handler(self, _, data, event)
     33  if event == 'stderr' then
     34    if self.add_stderr_to_output then
     35      self.output = self.output .. table.concat(data, '')
     36    else
     37      self.stderr = self.stderr .. table.concat(data, '')
     38    end
     39  elseif event == 'stdout' then
     40    self.output = self.output .. table.concat(data, '')
     41  end
     42 end
     43 
     44 --- @param cmd string|string[] List of command arguments to execute
     45 --- @param args? table Optional arguments:
     46 ---                   - stdin (string): Data to write to the job's stdin
     47 ---                   - stderr (boolean): Append stderr to stdout
     48 ---                   - ignore_error (boolean): If true, ignore error output
     49 ---                   - timeout (number): Number of seconds to wait before timing out (default 30)
     50 local function system(cmd, args)
     51  args = args or {}
     52  local stdin = args.stdin or ''
     53  local stderr = args.stderr or false
     54  local ignore_error = args.ignore_error or false
     55 
     56  local shell_error_code = 0
     57  local opts = {
     58    add_stderr_to_output = stderr,
     59    output = '',
     60    stderr = '',
     61    on_stdout = system_handler,
     62    on_stderr = system_handler,
     63    on_exit = function(_, data)
     64      shell_error_code = data
     65    end,
     66  }
     67  local jobid = vim.fn.jobstart(cmd, opts)
     68 
     69  if jobid < 1 then
     70    local message =
     71      string.format('Command error (job=%d): %s (in %s)', jobid, shellify(cmd), vim.uv.cwd())
     72    error(message)
     73    return opts.output, 1
     74  end
     75 
     76  if stdin:find('^%s$') then
     77    vim.fn.chansend(jobid, stdin)
     78  end
     79 
     80  local res = vim.fn.jobwait({ jobid }, vim.F.if_nil(args.timeout, 30) * 1000)
     81  if res[1] == -1 then
     82    error('Command timed out: ' .. shellify(cmd))
     83    vim.fn.jobstop(jobid)
     84  elseif shell_error_code ~= 0 and not ignore_error then
     85    local emsg = string.format(
     86      'Command error (job=%d, exit code %d): %s (in %s)',
     87      jobid,
     88      shell_error_code,
     89      shellify(cmd),
     90      vim.uv.cwd()
     91    )
     92    if opts.output:find('%S') then
     93      emsg = string.format('%s\noutput: %s', emsg, opts.output)
     94    end
     95    if opts.stderr:find('%S') then
     96      emsg = string.format('%s\nstderr: %s', emsg, opts.stderr)
     97    end
     98    error(emsg)
     99  end
    100 
    101  return vim.trim(vim.fn.system(cmd)), shell_error_code
    102 end
    103 
    104 ---@param provider string
    105 local function provider_disabled(provider)
    106  local loaded_var = 'loaded_' .. provider .. '_provider'
    107  local v = vim.g[loaded_var]
    108  if v == 0 then
    109    health.info('Disabled (' .. loaded_var .. '=' .. v .. ').')
    110    return true
    111  end
    112  return false
    113 end
    114 
    115 --- Checks the hygiene of a `g:loaded_xx_provider` variable.
    116 local function check_loaded_var(var)
    117  if vim.g[var] == 1 then
    118    health.error(('`g:%s=1` may have been set by mistake.'):format(var), {
    119      ('Remove `vim.g.%s=1` from your config.'):format(var),
    120      'To disable the provider, set this to 0, not 1.',
    121      'If you want to enable the provider but skip automatic detection, set the respective `g:…_host_prog` var. See :help provider',
    122    })
    123  end
    124 end
    125 
    126 local function clipboard()
    127  health.start('Clipboard (optional)')
    128 
    129  check_loaded_var('loaded_clipboard_provider')
    130 
    131  if
    132    os.getenv('TMUX')
    133    and vim.fn.executable('tmux') == 1
    134    and vim.fn.executable('pbpaste') == 1
    135    and not cmd_ok({ 'pbpaste' })
    136  then
    137    local _, tmux_version = cli_version({ 'tmux', '-V' })
    138    local advice = {
    139      'Install tmux 2.6+.  https://superuser.com/q/231130',
    140      'or use tmux with reattach-to-user-namespace.  https://superuser.com/a/413233',
    141    }
    142    health.error('pbcopy does not work with tmux version: ' .. tostring(tmux_version), advice)
    143  end
    144 
    145  local clipboard_tool = vim.fn['provider#clipboard#Executable']() ---@type string
    146  if vim.g.clipboard ~= nil and clipboard_tool == '' then
    147    local error_message = vim.fn['provider#clipboard#Error']() ---@type string
    148    health.error(
    149      error_message,
    150      "Use the example in :help g:clipboard as a template, or don't set g:clipboard at all."
    151    )
    152  elseif clipboard_tool:find('^%s*$') then
    153    health.warn(
    154      'No clipboard tool found. Clipboard registers (`"+` and `"*`) will not work.',
    155      ':help clipboard'
    156    )
    157  else
    158    health.ok('Clipboard tool found: ' .. clipboard_tool)
    159  end
    160 end
    161 
    162 local function node()
    163  health.start('Node.js provider (optional)')
    164 
    165  check_loaded_var('loaded_node_provider')
    166 
    167  if provider_disabled('node') then
    168    return
    169  end
    170 
    171  if
    172    vim.fn.executable('node') == 0
    173    or (
    174      vim.fn.executable('npm') == 0
    175      and vim.fn.executable('yarn') == 0
    176      and vim.fn.executable('pnpm') == 0
    177    )
    178  then
    179    health.warn(
    180      '`node` and `npm` (or `yarn`, `pnpm`) must be in $PATH.',
    181      'Install Node.js and verify that `node` and `npm` (or `yarn`, `pnpm`) commands work.'
    182    )
    183    return
    184  end
    185 
    186  local ok, node_v = cli_version({ 'node', '-v' })
    187  health.info('Node.js: ' .. tostring(node_v))
    188  if not ok or vim.version.lt(node_v, '6.0.0') then
    189    health.warn('Nvim node.js host does not support Node ' .. node_v)
    190    -- Skip further checks, they are nonsense if nodejs is too old.
    191    return
    192  end
    193  if vim.fn['provider#node#can_inspect']() == 0 then
    194    health.warn(
    195      'node.js on this system does not support --inspect-brk so $NVIM_NODE_HOST_DEBUG is ignored.'
    196    )
    197  end
    198 
    199  local node_detect_table = vim.fn['provider#node#Detect']() ---@type string[]
    200  local host = node_detect_table[1]
    201  if host:find('^%s*$') then
    202    health.warn('Missing "neovim" npm (or yarn, pnpm) package.', {
    203      'Run in shell: npm install -g neovim',
    204      'Run in shell (if you use yarn): yarn global add neovim',
    205      'Run in shell (if you use pnpm): pnpm install -g neovim',
    206      'You may disable this provider (and warning) by adding `let g:loaded_node_provider = 0` to your init.vim',
    207    })
    208    return
    209  end
    210  health.info('Nvim node.js host: ' .. host)
    211 
    212  local manager = 'npm'
    213  if vim.fn.executable('yarn') == 1 then
    214    manager = 'yarn'
    215  elseif vim.fn.executable('pnpm') == 1 then
    216    manager = 'pnpm'
    217  end
    218 
    219  local latest_npm_cmd = (
    220    iswin and { 'cmd', '/c', manager, 'info', 'neovim', '--json' }
    221    or { manager, 'info', 'neovim', '--json' }
    222  )
    223  local latest_npm
    224  ok, latest_npm = cmd_ok(latest_npm_cmd)
    225  if not ok or latest_npm:find('^%s$') then
    226    health.error(
    227      'Failed to run: ' .. shellify(latest_npm_cmd),
    228      { "Make sure you're connected to the internet.", 'Are you behind a firewall or proxy?' }
    229    )
    230    return
    231  end
    232 
    233  local pcall_ok, pkg_data = pcall(vim.json.decode, latest_npm)
    234  if not pcall_ok then
    235    return 'error: ' .. latest_npm
    236  end
    237  local latest_npm_subtable = pkg_data['dist-tags'] or {}
    238  latest_npm = latest_npm_subtable['latest'] or 'unable to parse'
    239 
    240  local current_npm_cmd = { 'node', host, '--version' }
    241  local current_npm
    242  ok, current_npm = cmd_ok(current_npm_cmd)
    243  if not ok then
    244    health.error(
    245      'Failed to run: ' .. shellify(current_npm_cmd),
    246      { 'Report this issue with the output of: ', shellify(current_npm_cmd) }
    247    )
    248    return
    249  end
    250 
    251  if latest_npm ~= 'unable to parse' and vim.version.lt(current_npm, latest_npm) then
    252    local message = 'Package "neovim" is out-of-date. Installed: '
    253      .. current_npm:gsub('%\n$', '')
    254      .. ', latest: '
    255      .. latest_npm:gsub('%\n$', '')
    256 
    257    health.warn(message, {
    258      'Run in shell: npm install -g neovim',
    259      'Run in shell (if you use yarn): yarn global add neovim',
    260      'Run in shell (if you use pnpm): pnpm install -g neovim',
    261    })
    262  else
    263    health.ok('Latest "neovim" npm/yarn/pnpm package is installed: ' .. current_npm)
    264  end
    265 end
    266 
    267 local function perl()
    268  health.start('Perl provider (optional)')
    269 
    270  check_loaded_var('loaded_perl_provider')
    271 
    272  if provider_disabled('perl') then
    273    return
    274  end
    275 
    276  local perl_exec, perl_warnings = vim.provider.perl.detect()
    277 
    278  if not perl_exec then
    279    health.warn(assert(perl_warnings), {
    280      'See :help provider-perl for more information.',
    281      'You can disable this provider (and warning) by adding `let g:loaded_perl_provider = 0` to your init.vim',
    282    })
    283    health.warn('No usable perl executable found')
    284    return
    285  end
    286 
    287  health.info('perl executable: ' .. perl_exec)
    288 
    289  -- we cannot use cpanm that is on the path, as it may not be for the perl
    290  -- set with g:perl_host_prog
    291  local ok = cmd_ok({ perl_exec, '-W', '-MApp::cpanminus', '-e', '' })
    292  if not ok then
    293    return { perl_exec, '"App::cpanminus" module is not installed' }
    294  end
    295 
    296  local latest_cpan_cmd = {
    297    perl_exec,
    298    '-MApp::cpanminus::fatscript',
    299    '-e',
    300    'my $app = App::cpanminus::script->new; $app->parse_options ("--info", "-q", "Neovim::Ext"); exit $app->doit',
    301  }
    302  local latest_cpan
    303  ok, latest_cpan = cmd_ok(latest_cpan_cmd)
    304  if not ok or latest_cpan:find('^%s*$') then
    305    health.error(
    306      'Failed to run: ' .. shellify(latest_cpan_cmd),
    307      { "Make sure you're connected to the internet.", 'Are you behind a firewall or proxy?' }
    308    )
    309    return
    310  elseif latest_cpan[1] == '!' then
    311    local cpanm_errs = vim.split(latest_cpan, '!')
    312    if cpanm_errs[1]:find("Can't write to ") then
    313      local advice = {} ---@type string[]
    314      for i = 2, #cpanm_errs do
    315        advice[#advice + 1] = cpanm_errs[i]
    316      end
    317 
    318      health.warn(cpanm_errs[1], advice)
    319      -- Last line is the package info
    320      latest_cpan = cpanm_errs[#cpanm_errs]
    321    else
    322      health.error('Unknown warning from command: ' .. latest_cpan_cmd, cpanm_errs)
    323      return
    324    end
    325  end
    326  latest_cpan = tostring(vim.fn.matchstr(latest_cpan, [[\(\.\?\d\)\+]]))
    327  if latest_cpan:find('^%s*$') then
    328    health.error('Cannot parse version number from cpanm output: ' .. latest_cpan)
    329    return
    330  end
    331 
    332  local current_cpan_cmd = { perl_exec, '-W', '-MNeovim::Ext', '-e', 'print $Neovim::Ext::VERSION' }
    333  local current_cpan
    334  ok, current_cpan = cmd_ok(current_cpan_cmd)
    335  if not ok then
    336    health.error(
    337      'Failed to run: ' .. shellify(current_cpan_cmd),
    338      { 'Report this issue with the output of: ', shellify(current_cpan_cmd) }
    339    )
    340    return
    341  end
    342 
    343  if vim.version.lt(current_cpan, latest_cpan) then
    344    local message = 'Module "Neovim::Ext" is out-of-date. Installed: '
    345      .. current_cpan
    346      .. ', latest: '
    347      .. latest_cpan
    348    health.warn(message, 'Run in shell: cpanm -n Neovim::Ext')
    349  else
    350    health.ok('Latest "Neovim::Ext" cpan module is installed: ' .. current_cpan)
    351  end
    352 end
    353 
    354 local function is(path, ty)
    355  if not path then
    356    return false
    357  end
    358  local stat = vim.uv.fs_stat(path)
    359  if not stat then
    360    return false
    361  end
    362  return stat.type == ty
    363 end
    364 
    365 -- Resolves Python executable path by invoking and checking `sys.executable`.
    366 local function python_exepath(invocation)
    367  if invocation == '' or invocation == nil then
    368    return nil
    369  end
    370  local p = vim.system({ invocation, '-c', 'import sys; sys.stdout.write(sys.executable)' }):wait()
    371  if p.code ~= 0 then
    372    health.warn(p.stderr)
    373    return nil
    374  end
    375  return vim.fs.normalize(vim.trim(p.stdout))
    376 end
    377 
    378 --- Check if pyenv is available and a valid pyenv root can be found, then return
    379 --- their respective paths. If either of those is invalid, return two empty
    380 --- strings, effectively ignoring pyenv.
    381 ---
    382 --- @return [string, string]
    383 local function check_for_pyenv()
    384  local pyenv_path = vim.fn.resolve(vim.fn.exepath('pyenv'))
    385 
    386  if pyenv_path == '' then
    387    return { '', '' }
    388  end
    389 
    390  health.info('pyenv: Path: ' .. pyenv_path)
    391 
    392  local pyenv_root = vim.fn.resolve(os.getenv('PYENV_ROOT') or '')
    393 
    394  if pyenv_root == '' then
    395    local p = vim.system({ pyenv_path, 'root' }):wait()
    396    if p.code ~= 0 then
    397      local message = string.format(
    398        'pyenv: Failed to infer the root of pyenv by running `%s root` : %s. Ignoring pyenv for all following checks.',
    399        pyenv_path,
    400        p.stderr
    401      )
    402      health.warn(message)
    403      return { '', '' }
    404    end
    405    pyenv_root = vim.trim(p.stdout)
    406    health.info('pyenv: $PYENV_ROOT is not set. Infer from `pyenv root`.')
    407  end
    408 
    409  if not is(pyenv_root, 'directory') then
    410    local message = string.format(
    411      'pyenv: Root does not exist: %s. Ignoring pyenv for all following checks.',
    412      pyenv_root
    413    )
    414    health.warn(message)
    415    return { '', '' }
    416  end
    417 
    418  health.info('pyenv: Root: ' .. pyenv_root)
    419 
    420  return { pyenv_path, pyenv_root }
    421 end
    422 
    423 -- Check the Python interpreter's usability.
    424 local function check_bin(bin)
    425  if not is(bin, 'file') and (not iswin or not is(bin .. '.exe', 'file')) then
    426    health.error('"' .. bin .. '" was not found.')
    427    return false
    428  elseif vim.fn.executable(bin) == 0 then
    429    health.error('"' .. bin .. '" is not executable.')
    430    return false
    431  end
    432  return true
    433 end
    434 
    435 --- Fetch the contents of a URL.
    436 ---
    437 --- @param url string
    438 local function download(url)
    439  local has_curl = vim.fn.executable('curl') == 1
    440  if has_curl then
    441    local ok, out = cmd_ok({ 'curl', '-V' })
    442    if ok and out:find('Protocols:.*https') then
    443      local content, rc = system({ 'curl', '-sL', url }, { stderr = true, ignore_error = true })
    444      if rc ~= 0 then
    445        return 'curl error with ' .. url .. ': ' .. rc
    446      else
    447        return content
    448      end
    449    end
    450  elseif vim.fn.executable('python') == 1 then
    451    local script = ([[
    452 try:
    453    from urllib.request import urlopen
    454 except ImportError:
    455    from urllib2 import urlopen
    456 
    457 response = urlopen('%s')
    458 print(response.read().decode('utf8'))
    459 ]]):format(url)
    460    local out, rc = system({ 'python', '-c', script })
    461    if out == '' and rc ~= 0 then
    462      return 'python urllib.request error: ' .. rc
    463    else
    464      return out
    465    end
    466  end
    467 
    468  local message = 'missing `curl` '
    469 
    470  if has_curl then
    471    message = message .. '(with HTTPS support) '
    472  end
    473  message = message .. 'and `python`, cannot make web request'
    474 
    475  return message
    476 end
    477 
    478 --- Get the latest Nvim Python client (pynvim) version from PyPI.
    479 local function latest_pypi_version()
    480  local pypi_version = 'unable to get pypi response'
    481  local pypi_response = download('https://pypi.org/pypi/pynvim/json')
    482  if pypi_response ~= '' then
    483    local pcall_ok, output = pcall(vim.fn.json_decode, pypi_response)
    484    if not pcall_ok then
    485      return 'error: ' .. pypi_response
    486    end
    487 
    488    local pypi_data = output
    489    local pypi_element = pypi_data['info'] or {}
    490    pypi_version = pypi_element['version'] or 'unable to parse'
    491  end
    492  return pypi_version
    493 end
    494 
    495 --- @param s string
    496 local function is_bad_response(s)
    497  local lower = s:lower()
    498  return vim.startswith(lower, 'unable')
    499    or vim.startswith(lower, 'error')
    500    or vim.startswith(lower, 'outdated')
    501 end
    502 
    503 --- Get version information using the specified interpreter.  The interpreter is
    504 --- used directly in case breaking changes were introduced since the last time
    505 --- Nvim's Python client was updated.
    506 ---
    507 --- @param python string
    508 ---
    509 --- Returns: {
    510 ---     {python executable version},
    511 ---     {current nvim version},
    512 ---     {current pypi nvim status},
    513 ---     {installed version status}
    514 --- }
    515 local function version_info(python)
    516  local pypi_version = latest_pypi_version()
    517 
    518  local python_version, rc = system({
    519    python,
    520    '-c',
    521    'import sys; print(".".join(str(x) for x in sys.version_info[:3]))',
    522  })
    523 
    524  if rc ~= 0 or python_version == '' then
    525    python_version = 'unable to parse ' .. python .. ' response'
    526  end
    527 
    528  local nvim_path
    529  nvim_path, rc = system({
    530    python,
    531    '-c',
    532    'import sys; sys.path = [p for p in sys.path if p != ""]; import neovim; print(neovim.__file__)',
    533  })
    534  if rc ~= 0 or nvim_path == '' then
    535    return { python_version, 'unable to load neovim Python module', pypi_version, nvim_path }
    536  end
    537 
    538  -- Assuming that multiple versions of a package are installed as
    539  -- `<semver>/<metapath>`, sort them on semantic version in descending order.
    540  local function compare(metapath1, metapath2)
    541    local dir1 = vim.fs.basename(vim.fs.dirname(vim.fs.abspath(metapath1)))
    542    local dir2 = vim.fs.basename(vim.fs.dirname(vim.fs.abspath(metapath2)))
    543    return vim.version.cmp(dir1, dir2)
    544  end
    545 
    546  -- Try to get neovim.VERSION (added in 0.1.11dev).
    547  local nvim_version
    548  nvim_version, rc = system({
    549    python,
    550    '-c',
    551    'from neovim import VERSION as v; print("{}.{}.{}{}".format(v.major, v.minor, v.patch, v.prerelease))',
    552  }, { stderr = true, ignore_error = true })
    553  if rc ~= 0 or nvim_version == '' then
    554    nvim_version = 'unable to find pynvim module version'
    555    local base = vim.fs.basename(nvim_path)
    556    local metas = vim.fn.glob(base .. '-*/METADATA', true, true)
    557    vim.list_extend(metas, vim.fn.glob(base .. '-*/PKG-INFO', true, true))
    558    vim.list_extend(metas, vim.fn.glob(base .. '.egg-info/PKG-INFO', true, true))
    559    metas = table.sort(metas, compare)
    560 
    561    if metas and next(metas) ~= nil then
    562      for line in io.lines(metas[1]) do
    563        --- @cast line string
    564        local version = line:match('^Version: (%S+)')
    565        if version then
    566          nvim_version = version
    567          break
    568        end
    569      end
    570    end
    571  end
    572 
    573  -- vim.fs.relpath does not prepend '~/' while fnamemodify does
    574  local nvim_path_base = vim.fn.fnamemodify(nvim_path, [[:~:h]])
    575  local version_status = 'unknown; ' .. nvim_path_base
    576  if not is_bad_response(nvim_version) and not is_bad_response(pypi_version) then
    577    if vim.version.lt(nvim_version, pypi_version) then
    578      version_status = 'outdated; from ' .. nvim_path_base
    579    else
    580      version_status = 'up to date'
    581    end
    582  end
    583 
    584  return { python_version, nvim_version, pypi_version, version_status }
    585 end
    586 
    587 local function python()
    588  health.start('Python 3 provider (optional)')
    589 
    590  check_loaded_var('loaded_python3_provider')
    591 
    592  local python_exe = ''
    593  local virtual_env = os.getenv('VIRTUAL_ENV')
    594  local venv = virtual_env and vim.fn.resolve(virtual_env) or ''
    595  local host_prog_var = 'python3_host_prog'
    596  local python_multiple = {} ---@type string[]
    597 
    598  if provider_disabled('python3') then
    599    return
    600  end
    601 
    602  local pyenv_table = check_for_pyenv()
    603  local pyenv = pyenv_table[1]
    604  local pyenv_root = pyenv_table[2]
    605 
    606  if vim.g[host_prog_var] then
    607    local message = string.format('Using: g:%s = "%s"', host_prog_var, vim.g[host_prog_var])
    608    health.info(message)
    609  end
    610 
    611  local pyname, pythonx_warnings = vim.provider.python.detect_by_module('neovim')
    612 
    613  if not pyname then
    614    health.warn(
    615      'No Python executable found that can `import neovim`. '
    616        .. 'Using the first available executable for diagnostics.'
    617    )
    618  elseif vim.g[host_prog_var] then
    619    python_exe = pyname
    620  end
    621 
    622  -- No Python executable could `import neovim`, or host_prog_var was used.
    623  if pythonx_warnings then
    624    health.warn(pythonx_warnings, {
    625      'See :help provider-python for more information.',
    626      'You can disable this provider (and warning) by adding `let g:loaded_python3_provider = 0` to your init.vim',
    627    })
    628  elseif pyname and pyname ~= '' and python_exe == '' then
    629    if not vim.g[host_prog_var] then
    630      local message = string.format(
    631        '`g:%s` is not set. Searching for %s in the environment.',
    632        host_prog_var,
    633        pyname
    634      )
    635      health.info(message)
    636    end
    637 
    638    if pyenv ~= '' then
    639      python_exe = system({ pyenv, 'which', pyname }, { stderr = true })
    640      if python_exe == '' then
    641        health.warn('pyenv could not find ' .. pyname .. '.')
    642      end
    643    end
    644 
    645    if python_exe == '' then
    646      python_exe = vim.fn.exepath(pyname)
    647 
    648      if os.getenv('PATH') then
    649        local path_sep = iswin and ';' or ':'
    650        local paths = vim.split(os.getenv('PATH') or '', path_sep)
    651 
    652        for _, path in ipairs(paths) do
    653          local path_bin = vim.fs.normalize(path .. '/' .. pyname)
    654          if
    655            path_bin ~= vim.fs.normalize(python_exe)
    656            and vim.tbl_contains(python_multiple, path_bin)
    657            and vim.fn.executable(path_bin) == 1
    658          then
    659            python_multiple[#python_multiple + 1] = path_bin
    660          end
    661        end
    662 
    663        if vim.tbl_count(python_multiple) > 0 then
    664          -- This is worth noting since the user may install something
    665          -- that changes $PATH, like homebrew.
    666          local message = string.format(
    667            'Multiple %s executables found. Set `g:%s` to avoid surprises.',
    668            pyname,
    669            host_prog_var
    670          )
    671          health.info(message)
    672        end
    673 
    674        if python_exe:find('shims') then
    675          local message = string.format('`%s` appears to be a pyenv shim.', python_exe)
    676          local advice = string.format(
    677            '`pyenv` is not in $PATH, your pyenv installation is broken. Set `g:%s` to avoid surprises.',
    678            host_prog_var
    679          )
    680          health.warn(message, advice)
    681        end
    682      end
    683    end
    684  end
    685 
    686  if python_exe ~= '' and not vim.g[host_prog_var] then
    687    if
    688      venv == ''
    689      and pyenv ~= ''
    690      and pyenv_root ~= ''
    691      and vim.startswith(vim.fn.resolve(python_exe), pyenv_root .. '/')
    692    then
    693      local advice = string.format(
    694        'Create a virtualenv specifically for Nvim using pyenv, and set `g:%s`.  This will avoid the need to install the pynvim module in each version/virtualenv.',
    695        host_prog_var
    696      )
    697      health.warn('pyenv is not set up optimally.', advice)
    698    elseif venv ~= '' then
    699      local venv_root = pyenv_root ~= '' and pyenv_root or vim.fs.dirname(venv)
    700 
    701      if vim.startswith(vim.fn.resolve(python_exe), venv_root .. '/') then
    702        local advice = string.format(
    703          'Create a virtualenv specifically for Nvim and use `g:%s`.  This will avoid the need to install the pynvim module in each virtualenv.',
    704          host_prog_var
    705        )
    706        health.warn('Your virtualenv is not set up optimally.', advice)
    707      end
    708    end
    709  end
    710 
    711  if pyname and python_exe == '' and pyname ~= '' then
    712    -- An error message should have already printed.
    713    health.error('`' .. pyname .. '` was not found.')
    714  elseif python_exe ~= '' and not check_bin(python_exe) then
    715    python_exe = ''
    716  end
    717 
    718  -- Diagnostic output
    719  health.info('Executable: ' .. (python_exe == '' and 'Not found' or python_exe))
    720  if vim.tbl_count(python_multiple) > 0 then
    721    for _, path_bin in ipairs(python_multiple) do
    722      health.info('Other python executable: ' .. path_bin)
    723    end
    724  end
    725 
    726  if python_exe == '' then
    727    -- No Python executable can import 'neovim'. Check if any Python executable
    728    -- can import 'pynvim'. If so, that Python failed to import 'neovim' as
    729    -- well, which is most probably due to a failed pip upgrade:
    730    -- https://github.com/neovim/neovim/wiki/Following-HEAD#20181118
    731    local pynvim_exe = vim.provider.python.detect_by_module('pynvim')
    732    if pynvim_exe then
    733      local message = 'Detected pip upgrade failure: Python executable can import "pynvim" but not "neovim": '
    734        .. pynvim_exe
    735      local advice = {
    736        'Use that Python version to uninstall any "pynvim" or "neovim", e.g.:',
    737        pynvim_exe .. ' -m pip uninstall pynvim neovim',
    738        'Then see :help provider-python for "pynvim" installation steps.',
    739      }
    740      health.error(message, advice)
    741    end
    742  else
    743    local version_info_table = version_info(python_exe)
    744    local pyversion = version_info_table[1]
    745    local current = version_info_table[2]
    746    local latest = version_info_table[3]
    747    local status = version_info_table[4]
    748 
    749    if not vim.version.range('~3'):has(pyversion) then
    750      health.warn('Unexpected Python version. This could lead to confusing error messages.')
    751    end
    752 
    753    health.info('Python version: ' .. pyversion)
    754 
    755    if is_bad_response(status) then
    756      health.info('pynvim version: ' .. current .. ' (' .. status .. ')')
    757    else
    758      health.info('pynvim version: ' .. current)
    759    end
    760 
    761    if is_bad_response(current) then
    762      health.error(
    763        'pynvim is not installed.\nError: ' .. current,
    764        'See :help provider-python for "pynvim" installation steps.'
    765      )
    766    end
    767 
    768    if is_bad_response(latest) then
    769      health.warn('Could not contact PyPI to get latest version.')
    770      health.error('HTTP request failed: ' .. latest)
    771    elseif is_bad_response(status) then
    772      health.warn('Latest pynvim is NOT installed: ' .. latest)
    773    elseif not is_bad_response(current) then
    774      health.ok('Latest pynvim is installed.')
    775    end
    776  end
    777 
    778  health.start('Python virtualenv')
    779  if not virtual_env then
    780    health.ok('no $VIRTUAL_ENV')
    781    return
    782  end
    783  local errors = {} ---@type string[]
    784  -- Keep hints as dict keys in order to discard duplicates.
    785  local hints = {} ---@type table<string, boolean>
    786  -- The virtualenv should contain some Python executables, and those
    787  -- executables should be first both on Nvim's $PATH and the $PATH of
    788  -- subshells launched from Nvim.
    789  local bin_dir = iswin and 'Scripts' or 'bin'
    790  local venv_bins = vim.fn.glob(string.format('%s/%s/python*', virtual_env, bin_dir), true, true)
    791  --- @param v string
    792  venv_bins = vim.tbl_filter(function(v)
    793    -- XXX: Remove irrelevant executables found in bin/.
    794    return not v:match('python.*%-config') and not v:match('python%-argcomplete')
    795  end, venv_bins)
    796  if vim.tbl_count(venv_bins) > 0 then
    797    for _, venv_bin in pairs(venv_bins) do
    798      venv_bin = vim.fs.normalize(venv_bin)
    799      local py_bin_basename = vim.fs.basename(venv_bin)
    800      local nvim_py_bin = python_exepath(vim.fn.exepath(py_bin_basename))
    801      if nvim_py_bin then
    802        local subshell_py_bin = python_exepath(py_bin_basename)
    803        local bintable = {
    804          ['nvim'] = {
    805            ['path'] = nvim_py_bin,
    806            ['hint'] = '$PATH ambiguities arise if the virtualenv is not '
    807              .. 'properly activated prior to launching Nvim. Close Nvim, activate the virtualenv, '
    808              .. 'check that invoking Python from the command line launches the correct one, '
    809              .. 'then relaunch Nvim.',
    810          },
    811          ['subshell'] = {
    812            ['path'] = subshell_py_bin,
    813            ['hint'] = '$PATH ambiguities in subshells typically are '
    814              .. 'caused by your shell config overriding the $PATH previously set by the '
    815              .. 'virtualenv. Either prevent them from doing so, or use this workaround: '
    816              .. 'https://vi.stackexchange.com/a/34996',
    817          },
    818        }
    819        for bintype, bin in pairs(bintable) do
    820          if vim.fn.resolve(venv_bin) ~= vim.fn.resolve(bin['path']) then
    821            local type_of_path = bintype == 'subshell' and '$PATH' or '$PATH in subshell'
    822            errors[#errors + 1] = type_of_path
    823              .. ' yields this '
    824              .. py_bin_basename
    825              .. ' executable: '
    826              .. bin['path']
    827            hints[bin['hint']] = true
    828          end
    829        end
    830      end
    831    end
    832  else
    833    errors[#errors + 1] = 'no Python executables found in the virtualenv '
    834      .. bin_dir
    835      .. ' directory.'
    836  end
    837 
    838  local msg = '$VIRTUAL_ENV is set to: ' .. virtual_env
    839  if vim.tbl_count(errors) > 0 then
    840    if vim.tbl_count(venv_bins) > 0 then
    841      msg = string.format(
    842        '%s\nAnd its %s directory contains: %s',
    843        msg,
    844        bin_dir,
    845        table.concat(
    846          --- @param v string
    847          vim.tbl_map(function(v)
    848            return vim.fs.basename(v)
    849          end, venv_bins),
    850          ', '
    851        )
    852      )
    853    end
    854    local conj = '\nBut '
    855    local msgs = {} --- @type string[]
    856    for _, err in ipairs(errors) do
    857      msgs[#msgs + 1] = msg
    858      msgs[#msgs + 1] = conj
    859      msgs[#msgs + 1] = err
    860      msgs[#msgs + 1] = '\n'
    861      conj = '\nAnd '
    862    end
    863    msgs[#msgs + 1] = '\nSo invoking Python may lead to unexpected results.'
    864    health.warn(table.concat(msgs), vim.tbl_keys(hints))
    865  else
    866    health.info(msg)
    867    health.info(
    868      'Python version: '
    869        .. system('python -c "import platform, sys; sys.stdout.write(platform.python_version())"')
    870    )
    871    health.ok('$VIRTUAL_ENV provides :!python.')
    872  end
    873 end
    874 
    875 local function ruby()
    876  health.start('Ruby provider (optional)')
    877 
    878  check_loaded_var('loaded_ruby_provider')
    879 
    880  if provider_disabled('ruby') then
    881    return
    882  end
    883 
    884  if vim.fn.executable('ruby') == 0 or vim.fn.executable('gem') == 0 then
    885    health.warn(
    886      '`ruby` and `gem` must be in $PATH.',
    887      'Install Ruby and verify that `ruby` and `gem` commands work.'
    888    )
    889    return
    890  end
    891  local _, ruby_v = cli_version({ 'ruby', '-v' })
    892  health.info('Ruby: ' .. tostring(ruby_v))
    893 
    894  local host, _ = vim.provider.ruby.detect()
    895  if (not host) or host:find('^%s*$') then
    896    health.warn('`neovim-ruby-host` not found.', {
    897      'Run `gem install neovim` to ensure the neovim RubyGem is installed.',
    898      'Run `gem environment` to ensure the gem bin directory is in $PATH.',
    899      'If you are using rvm/rbenv/chruby, try "rehashing".',
    900      'See :help g:ruby_host_prog for non-standard gem installations.',
    901      'You can disable this provider (and warning) by adding `let g:loaded_ruby_provider = 0` to your init.vim',
    902    })
    903    return
    904  end
    905  health.info('Host: ' .. host)
    906 
    907  local latest_gem_cmd = (
    908    iswin and { 'cmd', '/c', 'gem', 'list', '-ra', '"^^neovim$"' }
    909    or { 'gem', 'list', '-ra', '^neovim$' }
    910  )
    911  local ok, latest_gem = cmd_ok(latest_gem_cmd)
    912  if not ok or latest_gem:find('^%s*$') then
    913    health.error(
    914      'Failed to run: ' .. shellify(latest_gem_cmd),
    915      { "Make sure you're connected to the internet.", 'Are you behind a firewall or proxy?' }
    916    )
    917    return
    918  end
    919  local gem_split = vim.split(latest_gem, [[neovim (\|, \|)$]])
    920  latest_gem = gem_split[1] or 'not found'
    921 
    922  local current_gem_cmd = { host, '--version' }
    923  local current_gem
    924  ok, current_gem = cmd_ok(current_gem_cmd)
    925  if not ok then
    926    health.error(
    927      'Failed to run: ' .. shellify(current_gem_cmd),
    928      { 'Report this issue with the output of: ', shellify(current_gem_cmd) }
    929    )
    930    return
    931  end
    932 
    933  if vim.version.lt(current_gem, latest_gem) then
    934    local message = 'Gem "neovim" is out-of-date. Installed: '
    935      .. current_gem
    936      .. ', latest: '
    937      .. latest_gem
    938    health.warn(message, 'Run in shell: gem update neovim')
    939  else
    940    health.ok('Latest "neovim" gem is installed: ' .. current_gem)
    941  end
    942 end
    943 
    944 function M.check()
    945  clipboard()
    946  node()
    947  perl()
    948  python()
    949  ruby()
    950 end
    951 
    952 return M