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