Merge #5205 'CheckHealth'

This commit is contained in:
Justin M. Keyes 2016-08-21 22:04:15 -04:00
commit b8e6f04e69
11 changed files with 791 additions and 454 deletions

View File

@ -1,468 +1,150 @@
function! s:trim(s) abort
return substitute(a:s, '^\_s*\|\_s*$', '', 'g')
function! s:enhance_syntax() abort
syntax keyword healthError ERROR
highlight link healthError Error
syntax keyword healthWarning WARNING
highlight link healthWarning WarningMsg
syntax keyword healthInfo INFO
highlight link healthInfo ModeMsg
syntax keyword healthSuccess SUCCESS
highlight link healthSuccess Function
syntax keyword healthSuggestion SUGGESTION
highlight link healthSuggestion String
endfunction
" Runs the specified healthchecks.
" Runs all discovered healthchecks if a:plugin_names is empty.
function! health#check(plugin_names) abort
let report = ''
" Simple version comparison.
function! s:version_cmp(a, b) abort
let a = split(a:a, '\.')
let b = split(a:b, '\.')
let healthchecks = empty(a:plugin_names)
\ ? s:discover_health_checks()
\ : s:to_fn_names(a:plugin_names)
for i in range(len(a))
if a[i] > b[i]
return 1
elseif a[i] < b[i]
return -1
endif
endfor
return 0
endfunction
" Fetch the contents of a URL.
function! s:download(url) abort
let content = ''
if executable('curl')
let content = system('curl -sL "'.a:url.'"')
endif
if empty(content) && executable('python')
let script = "
\try:\n
\ from urllib.request import urlopen\n
\except ImportError:\n
\ from urllib2 import urlopen\n
\\n
\try:\n
\ response = urlopen('".a:url."')\n
\ print(response.read().decode('utf8'))\n
\except Exception:\n
\ pass\n
\"
let content = system('python -c "'.script.'" 2>/dev/null')
endif
return content
endfunction
" Get the latest Neovim Python client version from PyPI. The result is
" cached.
function! s:latest_pypi_version()
if exists('s:pypi_version')
return s:pypi_version
endif
let s:pypi_version = 'unknown'
let pypi_info = s:download('https://pypi.python.org/pypi/neovim/json')
if !empty(pypi_info)
let pypi_data = json_decode(pypi_info)
let s:pypi_version = get(get(pypi_data, 'info', {}), 'version', 'unknown')
return s:pypi_version
endif
endfunction
" Get version information using the specified interpreter. The interpreter is
" used directly in case breaking changes were introduced since the last time
" Neovim's Python client was updated.
function! s:version_info(python) abort
let pypi_version = s:latest_pypi_version()
let python_version = s:trim(system(
\ printf('"%s" -c "import sys; print(''.''.join(str(x) '
\ . 'for x in sys.version_info[:3]))"', a:python)))
if empty(python_version)
let python_version = 'unknown'
endif
let nvim_path = s:trim(system(printf('"%s" -c "import sys, neovim;'
\ . 'print(neovim.__file__)" 2>/dev/null', a:python)))
if empty(nvim_path)
return [python_version, 'not found', pypi_version, 'unknown']
endif
let nvim_version = 'unknown'
let base = fnamemodify(nvim_path, ':h')
for meta in glob(base.'-*/METADATA', 1, 1) + glob(base.'-*/PKG-INFO', 1, 1)
for meta_line in readfile(meta)
if meta_line =~# '^Version:'
let nvim_version = matchstr(meta_line, '^Version: \zs\S\+')
endif
endfor
endfor
let version_status = 'unknown'
if nvim_version != 'unknown' && pypi_version != 'unknown'
if s:version_cmp(nvim_version, pypi_version) == -1
let version_status = 'outdated'
else
let version_status = 'up to date'
endif
endif
return [python_version, nvim_version, pypi_version, version_status]
endfunction
" Check the Python interpreter's usability.
function! s:check_bin(bin, notes) abort
if !filereadable(a:bin)
call add(a:notes, printf('Error: "%s" was not found.', a:bin))
return 0
elseif executable(a:bin) != 1
call add(a:notes, printf('Error: "%s" is not executable.', a:bin))
return 0
endif
return 1
endfunction
" Text wrapping that returns a list of lines
function! s:textwrap(text, width) abort
let pattern = '.*\%(\s\+\|\_$\)\zs\%<'.a:width.'c'
return map(split(a:text, pattern), 's:trim(v:val)')
endfunction
" Echo wrapped notes
function! s:echo_notes(notes) abort
if empty(a:notes)
return
endif
echo ' Messages:'
for msg in a:notes
if msg =~# "\n"
let msg_lines = []
for msgl in filter(split(msg, "\n"), 'v:val !~# ''^\s*$''')
call extend(msg_lines, s:textwrap(msgl, 74))
endfor
else
let msg_lines = s:textwrap(msg, 74)
endif
if !len(msg_lines)
continue
endif
echo ' *' msg_lines[0]
if len(msg_lines) > 1
echo join(map(msg_lines[1:], '" ".v:val'), "\n")
endif
endfor
endfunction
" Load the remote plugin manifest file and check for unregistered plugins
function! s:diagnose_manifest() abort
echo 'Checking: Remote Plugins'
let existing_rplugins = {}
for item in remote#host#PluginsForHost('python')
let existing_rplugins[item.path] = 'python'
endfor
for item in remote#host#PluginsForHost('python3')
let existing_rplugins[item.path] = 'python3'
endfor
let require_update = 0
let notes = []
for path in map(split(&rtp, ','), 'resolve(v:val)')
let python_glob = glob(path.'/rplugin/python*', 1, 1)
if empty(python_glob)
continue
endif
let python_dir = python_glob[0]
let python_version = fnamemodify(python_dir, ':t')
for script in glob(python_dir.'/*.py', 1, 1)
\ + glob(python_dir.'/*/__init__.py', 1, 1)
let contents = join(readfile(script))
if contents =~# '\<\%(from\|import\)\s\+neovim\>'
if script =~# '/__init__\.py$'
let script = fnamemodify(script, ':h')
endif
if !has_key(existing_rplugins, script)
let msg = printf('"%s" is not registered.', fnamemodify(path, ':t'))
if python_version == 'pythonx'
if !has('python2') && !has('python3')
let msg .= ' (python2 and python3 not available)'
endif
elseif !has(python_version)
let msg .= printf(' (%s not available)', python_version)
else
let require_update = 1
endif
call add(notes, msg)
endif
break
endif
endfor
endfor
echo ' Status: '
if require_update
echon 'Out of date'
call add(notes, 'Run :UpdateRemotePlugins')
if empty(healthchecks)
let report = "ERROR: No healthchecks found."
else
echon 'Up to date'
endif
call s:echo_notes(notes)
endfunction
function! s:diagnose_python(version) abort
let python_bin_name = 'python'.(a:version == 2 ? '' : '3')
let pyenv = resolve(exepath('pyenv'))
let pyenv_root = exists('$PYENV_ROOT') ? resolve($PYENV_ROOT) : ''
let venv = exists('$VIRTUAL_ENV') ? resolve($VIRTUAL_ENV) : ''
let host_prog_var = python_bin_name.'_host_prog'
let host_skip_var = python_bin_name.'_host_skip_check'
let python_bin = ''
let python_multiple = []
let notes = []
if exists('g:'.host_prog_var)
call add(notes, printf('Using: g:%s = "%s"', host_prog_var, get(g:, host_prog_var)))
endif
let [python_bin_name, pythonx_errs] = provider#pythonx#Detect(a:version)
if empty(python_bin_name)
call add(notes, 'Warning: No Python interpreter was found with the neovim '
\ . 'module. Using the first available for diagnostics.')
if !empty(pythonx_errs)
call add(notes, pythonx_errs)
endif
let old_skip = get(g:, host_skip_var, 0)
let g:[host_skip_var] = 1
let [python_bin_name, pythonx_errs] = provider#pythonx#Detect(a:version)
let g:[host_skip_var] = old_skip
endif
if !empty(python_bin_name)
if exists('g:'.host_prog_var)
let python_bin = exepath(python_bin_name)
endif
let python_bin_name = fnamemodify(python_bin_name, ':t')
endif
if !empty(pythonx_errs)
call add(notes, pythonx_errs)
endif
if !empty(python_bin_name) && empty(python_bin) && empty(pythonx_errs)
if !exists('g:'.host_prog_var)
call add(notes, printf('Warning: "g:%s" is not set. Searching for '
\ . '%s in the environment.', host_prog_var, python_bin_name))
endif
if !empty(pyenv)
if empty(pyenv_root)
call add(notes, 'Warning: pyenv was found, but $PYENV_ROOT '
\ . 'is not set. Did you follow the final install '
\ . 'instructions?')
else
call add(notes, printf('Notice: pyenv found: "%s"', pyenv))
endif
let python_bin = s:trim(system(
\ printf('"%s" which %s 2>/dev/null', pyenv, python_bin_name)))
if empty(python_bin)
call add(notes, printf('Warning: pyenv couldn''t find %s.', python_bin_name))
endif
endif
if empty(python_bin)
let python_bin = exepath(python_bin_name)
if exists('$PATH')
for path in split($PATH, ':')
let path_bin = path.'/'.python_bin_name
if path_bin != python_bin && index(python_multiple, path_bin) == -1
\ && executable(path_bin)
call add(python_multiple, path_bin)
endif
endfor
if len(python_multiple)
" This is worth noting since the user may install something
" that changes $PATH, like homebrew.
call add(notes, printf('Suggestion: There are multiple %s executables found. '
\ . 'Set "g:%s" to avoid surprises.', python_bin_name, host_prog_var))
endif
if python_bin =~# '\<shims\>'
call add(notes, printf('Warning: "%s" appears to be a pyenv shim. '
\ . 'This could mean that a) the "pyenv" executable is not in '
\ . '$PATH, b) your pyenv installation is broken. '
\ . 'You should set "g:%s" to avoid surprises.',
\ python_bin, host_prog_var))
endif
endif
endif
endif
if !empty(python_bin)
if !empty(pyenv) && !exists('g:'.host_prog_var) && !empty(pyenv_root)
\ && resolve(python_bin) !~# '^'.pyenv_root.'/'
call add(notes, printf('Suggestion: Create a virtualenv specifically '
\ . 'for Neovim using pyenv and use "g:%s". This will avoid '
\ . 'the need to install Neovim''s Python client in each '
\ . 'version/virtualenv.', host_prog_var))
endif
if !empty(venv) && exists('g:'.host_prog_var)
if !empty(pyenv_root)
let venv_root = pyenv_root
else
let venv_root = fnamemodify(venv, ':h')
endif
if resolve(python_bin) !~# '^'.venv_root.'/'
call add(notes, printf('Suggestion: Create a virtualenv specifically '
\ . 'for Neovim and use "g:%s". This will avoid '
\ . 'the need to install Neovim''s Python client in each '
\ . 'virtualenv.', host_prog_var))
endif
endif
endif
if empty(python_bin) && !empty(python_bin_name)
" An error message should have already printed.
call add(notes, printf('Error: "%s" was not found.', python_bin_name))
elseif !empty(python_bin) && !s:check_bin(python_bin, notes)
let python_bin = ''
endif
" Check if $VIRTUAL_ENV is active
let virtualenv_inactive = 0
if exists('$VIRTUAL_ENV')
if !empty(pyenv)
let pyenv_prefix = resolve(s:trim(system(printf('"%s" prefix', pyenv))))
if $VIRTUAL_ENV != pyenv_prefix
let virtualenv_inactive = 1
endif
elseif !empty(python_bin_name) && exepath(python_bin_name) !~# '^'.$VIRTUAL_ENV.'/'
let virtualenv_inactive = 1
endif
endif
if virtualenv_inactive
call add(notes, 'Warning: $VIRTUAL_ENV exists but appears to be '
\ . 'inactive. This could lead to unexpected results. If you are '
\ . 'using Zsh, see: http://vi.stackexchange.com/a/7654/5229')
endif
" Diagnostic output
echo 'Checking: Python' a:version
echo ' Executable:' (empty(python_bin) ? 'Not found' : python_bin)
if len(python_multiple)
for path_bin in python_multiple
echo ' (other):' path_bin
for c in healthchecks
let report .= printf("\n%s\n%s", c, repeat('=',80))
try
let report .= execute('call '.c.'()')
catch /^Vim\%((\a\+)\)\=:E117/
let report .= execute(
\ 'call health#report_error(''No healthcheck found for "'
\ .s:to_plugin_name(c)
\ .'" plugin.'')')
catch
let report .= execute(
\ 'call health#report_error(''Failed to run healthcheck for "'
\ .s:to_plugin_name(c)
\ .'" plugin. Exception:''."\n".v:exception)')
endtry
let report .= "\n"
endfor
endif
if !empty(python_bin)
let [pyversion, current, latest, status] = s:version_info(python_bin)
if a:version != str2nr(pyversion)
call add(notes, 'Warning: Got an unexpected version of Python. '
\ . 'This could lead to confusing error messages. Please '
\ . 'consider this before reporting bugs to plugin developers.')
endif
if a:version == 3 && str2float(pyversion) < 3.3
call add(notes, 'Warning: Python 3.3+ is recommended.')
endif
tabnew
setlocal bufhidden=wipe
set filetype=markdown
call s:enhance_syntax()
call setline(1, split(report, "\n"))
setlocal nomodified
endfunction
echo ' Python Version:' pyversion
echo printf(' %s-neovim Version: %s', python_bin_name, current)
" Starts a new report.
function! health#report_start(name) abort
echo "\n## " . a:name
endfunction
if current == 'not found'
call add(notes, 'Error: Neovim Python client is not installed.')
endif
" Indents lines *except* line 1 of a string if it contains newlines.
function! s:indent_after_line1(s, columns) abort
let lines = split(a:s, "\n", 0)
if len(lines) < 2 " We do not indent line 1, so nothing to do.
return a:s
endif
for i in range(1, len(lines)-1) " Indent lines after the first.
let lines[i] = substitute(lines[i], '^\s*', repeat(' ', a:columns), 'g')
endfor
return join(lines, "\n")
endfunction
if latest == 'unknown'
call add(notes, 'Warning: Unable to fetch latest Neovim Python client version.')
endif
" Format a message for a specific report item
function! s:format_report_message(status, msg, ...) abort " {{{
let output = ' - ' . a:status . ': ' . s:indent_after_line1(a:msg, 4)
let suggestions = []
if status == 'outdated'
echon ' (latest: '.latest.')'
else
echon ' ('.status.')'
" Optional parameters
if a:0 > 0
let suggestions = type(a:1) == type("") ? [a:1] : a:1
if type(suggestions) != type([])
echoerr "Expected String or List"
endif
endif
call s:echo_notes(notes)
endfunction
" Report each suggestion
if len(suggestions) > 0
let output .= "\n - SUGGESTIONS:"
endif
for suggestion in suggestions
let output .= "\n - " . s:indent_after_line1(suggestion, 10)
endfor
return output
endfunction " }}}
function! s:diagnose_ruby() abort
echo 'Checking: Ruby'
let ruby_vers = systemlist('ruby -v')[0]
let ruby_prog = provider#ruby#Detect()
let notes = []
" Use {msg} to report information in the current section
function! health#report_info(msg) abort " {{{
echo s:format_report_message('INFO', a:msg)
endfunction " }}}
if empty(ruby_prog)
let ruby_prog = 'not found'
let prog_vers = 'not found'
call add(notes, 'Suggestion: Install the neovim RubyGem using ' .
\ '`gem install neovim`.')
" Reports a successful healthcheck.
function! health#report_ok(msg) abort " {{{
echo s:format_report_message('SUCCESS', a:msg)
endfunction " }}}
" Reports a health warning.
function! health#report_warn(msg, ...) abort " {{{
if a:0 > 0
echo s:format_report_message('WARNING', a:msg, a:1)
else
silent let prog_vers = systemlist(ruby_prog . ' --version')[0]
if v:shell_error
let prog_vers = 'outdated'
call add(notes, 'Suggestion: Install the latest neovim RubyGem using ' .
\ '`gem install neovim`.')
elseif s:version_cmp(prog_vers, "0.2.0") == -1
let prog_vers .= ' (outdated)'
call add(notes, 'Suggestion: Install the latest neovim RubyGem using ' .
\ '`gem install neovim`.')
endif
echo s:format_report_message('WARNING', a:msg)
endif
endfunction " }}}
echo ' Ruby Version: ' . ruby_vers
echo ' Host Executable: ' . ruby_prog
echo ' Host Version: ' . prog_vers
call s:echo_notes(notes)
endfunction
function! health#check(bang) abort
redir => report
try
silent call s:diagnose_python(2)
silent echo ''
silent call s:diagnose_python(3)
silent echo ''
silent call s:diagnose_ruby()
silent echo ''
silent call s:diagnose_manifest()
silent echo ''
finally
redir END
endtry
if a:bang
new
setlocal bufhidden=wipe
call setline(1, split(report, "\n"))
setlocal nomodified
" Reports a failed healthcheck.
function! health#report_error(msg, ...) abort " {{{
if a:0 > 0
echo s:format_report_message('ERROR', a:msg, a:1)
else
echo report
echo "\nTip: Use "
echohl Identifier
echon ":CheckHealth!"
echohl None
echon " to open this in a new buffer."
echo s:format_report_message('ERROR', a:msg)
endif
endfunction " }}}
function! s:filepath_to_function(name) abort
return substitute(substitute(substitute(a:name, ".*autoload/", "", ""),
\ "\\.vim", "#check", ""), "/", "#", "g")
endfunction
function! s:discover_health_checks() abort
let healthchecks = globpath(&runtimepath, 'autoload/health/*.vim', 1, 1)
let healthchecks = map(healthchecks, '<SID>filepath_to_function(v:val)')
return healthchecks
endfunction
" Translates a list of plugin names to healthcheck function names.
function! s:to_fn_names(plugin_names) abort
let healthchecks = []
for p in a:plugin_names
call add(healthchecks, 'health#'.p.'#check')
endfor
return healthchecks
endfunction
" Extracts 'foo' from 'health#foo#check'.
function! s:to_plugin_name(fn_name) abort
return substitute(a:fn_name,
\ '\v.*health\#(.+)\#check.*', '\1', '')
endfunction

View File

@ -0,0 +1,439 @@
let s:bad_responses = [
\ 'unable to parse python response',
\ 'unable to parse',
\ 'unable to get pypi response',
\ 'unable to get neovim executable',
\ 'unable to find neovim version'
\ ]
function! s:is_bad_response(s) abort
return index(s:bad_responses, a:s) >= 0
endfunction
function! s:trim(s) abort
return substitute(a:s, '^\_s*\|\_s*$', '', 'g')
endfunction
" Simple version comparison.
function! s:version_cmp(a, b) abort
let a = split(a:a, '\.')
let b = split(a:b, '\.')
for i in range(len(a))
if a[i] > b[i]
return 1
elseif a[i] < b[i]
return -1
endif
endfor
return 0
endfunction
" Fetch the contents of a URL.
function! s:download(url) abort
let content = ''
if executable('curl')
let content = system(['curl', '-sL', "'", a:url, "'"])
endif
if empty(content) && executable('python')
let script = "
\try:\n
\ from urllib.request import urlopen\n
\except ImportError:\n
\ from urllib2 import urlopen\n
\\n
\try:\n
\ response = urlopen('".a:url."')\n
\ print(response.read().decode('utf8'))\n
\except Exception:\n
\ pass\n
\"
let content = system(['python', '-c', "'", script, "'", '2>/dev/null'])
endif
return content
endfunction
" Get the latest Neovim Python client version from PyPI. Result is cached.
function! s:latest_pypi_version() abort
if exists('s:pypi_version')
return s:pypi_version
endif
let s:pypi_version = 'unable to get pypi response'
let pypi_info = s:download('https://pypi.python.org/pypi/neovim/json')
if !empty(pypi_info)
let pypi_data = json_decode(pypi_info)
let s:pypi_version = get(get(pypi_data, 'info', {}), 'version', 'unable to parse')
return s:pypi_version
endif
endfunction
" Get version information using the specified interpreter. The interpreter is
" used directly in case breaking changes were introduced since the last time
" Neovim's Python client was updated.
"
" Returns [
" python executable version,
" current nvim version,
" current pypi nvim status,
" installed version status
" ]
function! s:version_info(python) abort
let pypi_version = s:latest_pypi_version()
let python_version = s:trim(system([
\ a:python,
\ '-c',
\ 'import sys; print(".".join(str(x) for x in sys.version_info[:3]))',
\ ]))
if empty(python_version)
let python_version = 'unable to parse python response'
endif
let nvim_path = s:trim(system([
\ a:python,
\ '-c',
\ 'import neovim; print(neovim.__file__)',
\ '2>/dev/null']))
let nvim_path = s:trim(system([
\ 'python3',
\ '-c',
\ 'import neovim; print(neovim.__file__)'
\ ]))
" \ '2>/dev/null']))
if empty(nvim_path)
return [python_version, 'unable to find neovim executable', pypi_version, 'unable to get neovim executable']
endif
let nvim_version = 'unable to find neovim version'
let base = fnamemodify(nvim_path, ':h')
for meta in glob(base.'-*/METADATA', 1, 1) + glob(base.'-*/PKG-INFO', 1, 1)
for meta_line in readfile(meta)
if meta_line =~# '^Version:'
let nvim_version = matchstr(meta_line, '^Version: \zs\S\+')
endif
endfor
endfor
let version_status = 'unknown'
if !s:is_bad_response(nvim_version) && !s:is_bad_response(pypi_version)
if s:version_cmp(nvim_version, pypi_version) == -1
let version_status = 'outdated'
else
let version_status = 'up to date'
endif
endif
return [python_version, nvim_version, pypi_version, version_status]
endfunction
" Check the Python interpreter's usability.
function! s:check_bin(bin) abort
if !filereadable(a:bin)
call health#report_error(printf('"%s" was not found.', a:bin))
return 0
elseif executable(a:bin) != 1
call health#report_error(printf('"%s" is not executable.', a:bin))
return 0
endif
return 1
endfunction
" Load the remote plugin manifest file and check for unregistered plugins
function! s:check_manifest() abort
call health#report_start('Remote Plugins')
let existing_rplugins = {}
for item in remote#host#PluginsForHost('python')
let existing_rplugins[item.path] = 'python'
endfor
for item in remote#host#PluginsForHost('python3')
let existing_rplugins[item.path] = 'python3'
endfor
let require_update = 0
for path in map(split(&runtimepath, ','), 'resolve(v:val)')
let python_glob = glob(path.'/rplugin/python*', 1, 1)
if empty(python_glob)
continue
endif
let python_dir = python_glob[0]
let python_version = fnamemodify(python_dir, ':t')
for script in glob(python_dir.'/*.py', 1, 1)
\ + glob(python_dir.'/*/__init__.py', 1, 1)
let contents = join(readfile(script))
if contents =~# '\<\%(from\|import\)\s\+neovim\>'
if script =~# '/__init__\.py$'
let script = fnamemodify(script, ':h')
endif
if !has_key(existing_rplugins, script)
let msg = printf('"%s" is not registered.', fnamemodify(path, ':t'))
if python_version ==# 'pythonx'
if !has('python2') && !has('python3')
let msg .= ' (python2 and python3 not available)'
endif
elseif !has(python_version)
let msg .= printf(' (%s not available)', python_version)
else
let require_update = 1
endif
call health#report_warn(msg)
endif
break
endif
endfor
endfor
if require_update
call health#report_warn('Out of date', ['Run `:UpdateRemotePlugins`'])
else
call health#report_ok('Up to date')
endif
endfunction
function! s:check_python(version) abort
call health#report_start('Python ' . a:version . ' provider')
let python_bin_name = 'python'.(a:version == 2 ? '2' : '3')
let pyenv = resolve(exepath('pyenv'))
let pyenv_root = exists('$PYENV_ROOT') ? resolve($PYENV_ROOT) : 'n'
let venv = exists('$VIRTUAL_ENV') ? resolve($VIRTUAL_ENV) : ''
let host_prog_var = python_bin_name.'_host_prog'
let host_skip_var = python_bin_name.'_host_skip_check'
let python_bin = ''
let python_multiple = []
if exists('g:'.host_prog_var)
call health#report_info(printf('Using: g:%s = "%s"', host_prog_var, get(g:, host_prog_var)))
endif
let [python_bin_name, pythonx_errs] = provider#pythonx#Detect(a:version)
if empty(python_bin_name)
call health#report_warn('No Python interpreter was found with the neovim '
\ . 'module. Using the first available for diagnostics.')
if !empty(pythonx_errs)
call health#report_warn(pythonx_errs)
endif
let old_skip = get(g:, host_skip_var, 0)
let g:[host_skip_var] = 1
let [python_bin_name, pythonx_errs] = provider#pythonx#Detect(a:version)
let g:[host_skip_var] = old_skip
endif
if !empty(python_bin_name)
if exists('g:'.host_prog_var)
let python_bin = exepath(python_bin_name)
endif
let python_bin_name = fnamemodify(python_bin_name, ':t')
endif
if !empty(pythonx_errs)
call health#report_error('Python provider error', pythonx_errs)
endif
if !empty(python_bin_name) && empty(python_bin) && empty(pythonx_errs)
if !exists('g:'.host_prog_var)
call health#report_info(printf('`g:%s` is not set. Searching for '
\ . '%s in the environment.', host_prog_var, python_bin_name))
endif
if !empty(pyenv)
if empty(pyenv_root)
call health#report_warn(
\ 'pyenv was found, but $PYENV_ROOT is not set.',
\ ['Did you follow the final install instructions?']
\ )
else
call health#report_ok(printf('pyenv found: "%s"', pyenv))
endif
let python_bin = s:trim(system(
\ printf('"%s" which %s 2>/dev/null', pyenv, python_bin_name)))
if empty(python_bin)
call health#report_warn(printf('pyenv couldn''t find %s.', python_bin_name))
endif
endif
if empty(python_bin)
let python_bin = exepath(python_bin_name)
if exists('$PATH')
for path in split($PATH, ':')
let path_bin = path.'/'.python_bin_name
if path_bin != python_bin && index(python_multiple, path_bin) == -1
\ && executable(path_bin)
call add(python_multiple, path_bin)
endif
endfor
if len(python_multiple)
" This is worth noting since the user may install something
" that changes $PATH, like homebrew.
call health#report_info(printf('There are multiple %s executables found. '
\ . 'Set "g:%s" to avoid surprises.', python_bin_name, host_prog_var))
endif
if python_bin =~# '\<shims\>'
call health#report_warn(printf('"%s" appears to be a pyenv shim.', python_bin), [
\ 'The "pyenv" executable is not in $PATH,',
\ 'Your pyenv installation is broken. You should set '
\ . '"g:'.host_prog_var.'" to avoid surprises.',
\ ])
endif
endif
endif
endif
if !empty(python_bin)
if empty(venv) && !empty(pyenv) && !exists('g:'.host_prog_var)
\ && !empty(pyenv_root) && resolve(python_bin) !~# '^'.pyenv_root.'/'
call health#report_warn('pyenv is not set up optimally.', [
\ printf('Suggestion: Create a virtualenv specifically '
\ . 'for Neovim using pyenv and use "g:%s". This will avoid '
\ . 'the need to install Neovim''s Python client in each '
\ . 'version/virtualenv.', host_prog_var)
\ ])
elseif !empty(venv) && exists('g:'.host_prog_var)
if !empty(pyenv_root)
let venv_root = pyenv_root
else
let venv_root = fnamemodify(venv, ':h')
endif
if resolve(python_bin) !~# '^'.venv_root.'/'
call health#report_warn('Your virtualenv is not set up optimally.', [
\ printf('Suggestion: Create a virtualenv specifically '
\ . 'for Neovim and use "g:%s". This will avoid '
\ . 'the need to install Neovim''s Python client in each '
\ . 'virtualenv.', host_prog_var)
\ ])
endif
endif
endif
if empty(python_bin) && !empty(python_bin_name)
" An error message should have already printed.
call health#report_error(printf('"%s" was not found.', python_bin_name))
elseif !empty(python_bin) && !s:check_bin(python_bin)
let python_bin = ''
endif
" Check if $VIRTUAL_ENV is active
let virtualenv_inactive = 0
if exists('$VIRTUAL_ENV')
if !empty(pyenv)
let pyenv_prefix = resolve(s:trim(system([pyenv, 'prefix'])))
if $VIRTUAL_ENV != pyenv_prefix
let virtualenv_inactive = 1
endif
elseif !empty(python_bin_name) && exepath(python_bin_name) !~# '^'.$VIRTUAL_ENV.'/'
let virtualenv_inactive = 1
endif
endif
if virtualenv_inactive
let suggestions = [
\ 'If you are using Zsh, see: http://vi.stackexchange.com/a/7654/5229',
\ ]
call health#report_warn(
\ '$VIRTUAL_ENV exists but appears to be inactive. '
\ . 'This could lead to unexpected results.',
\ suggestions)
endif
" Diagnostic output
call health#report_info('Executable: ' . (empty(python_bin) ? 'Not found' : python_bin))
if len(python_multiple)
for path_bin in python_multiple
call health#report_info('Other python executable: ' . path_bin)
endfor
endif
if !empty(python_bin)
let [pyversion, current, latest, status] = s:version_info(python_bin)
if a:version != str2nr(pyversion)
call health#report_warn('Got an unexpected version of Python.' .
\ ' This could lead to confusing error messages.')
endif
if a:version == 3 && str2float(pyversion) < 3.3
call health#report_warn('Python 3.3+ is recommended.')
endif
call health#report_info('Python'.a:version.' version: ' . pyversion)
call health#report_info(printf('%s-neovim Version: %s', python_bin_name, current))
if s:is_bad_response(current)
let suggestions = [
\ 'Error found was: ' . current,
\ 'Use the command `$ pip' . a:version . ' install neovim`',
\ ]
call health#report_error(
\ 'Neovim Python client is not installed.',
\ suggestions)
endif
if s:is_bad_response(latest)
call health#report_warn('Unable to fetch latest Neovim Python client version.')
endif
if s:is_bad_response(status)
call health#report_warn('Latest Neovim Python client versions: ('.latest.')')
else
call health#report_ok('Latest Neovim Python client is installed: ('.status.')')
endif
endif
endfunction
function! s:check_ruby() abort
call health#report_start('Ruby provider')
let min_version = "0.2.4"
let ruby_version = systemlist('ruby -v')[0]
let ruby_prog = provider#ruby#Detect()
let suggestions =
\ ['Install or upgrade the neovim RubyGem using `gem install neovim`.']
if empty(ruby_prog)
let ruby_prog = 'not found'
let prog_vers = 'not found'
call health#report_error('Missing Neovim RubyGem', suggestions)
else
silent let prog_vers = systemlist(ruby_prog . ' --version')[0]
if v:shell_error
let prog_vers = 'outdated'
call health#report_warn('Neovim RubyGem is not up-to-date', suggestions)
elseif s:version_cmp(prog_vers, min_version) == -1
let prog_vers .= ' (outdated)'
call health#report_warn('Neovim RubyGem is not up-to-date', suggestions)
else
call health#report_ok('Found Neovim RubyGem')
endif
endif
call health#report_info('Ruby Version: ' . ruby_version)
call health#report_info('Host Executable: ' . ruby_prog)
call health#report_info('Host Version: ' . prog_vers)
endfunction
function! health#nvim#check() abort
call s:check_manifest()
call s:check_python(2)
call s:check_python(3)
call s:check_ruby()
endfunction

View File

@ -178,7 +178,9 @@ endfunction
function! remote#host#LoadRemotePluginsEvent(event, pattern) abort
autocmd! nvim-rplugin
call remote#host#LoadRemotePlugins()
execute 'silent doautocmd <nomodeline>' a:event a:pattern
if exists('#'.a:event.'#'.a:pattern) " Avoid 'No matching autocommands'.
execute 'silent doautocmd <nomodeline>' a:event a:pattern
endif
endfunction

127
runtime/doc/pi_health.txt Normal file
View File

@ -0,0 +1,127 @@
*pi_health.txt* Healthcheck framework
Author: TJ DeVries <devries.timothyj@gmail.com>
==============================================================================
1. Introduction |health.vim-intro|
2. Commands and functions |health.vim-manual|
3. Create a healthcheck |health.vim-dev|
==============================================================================
Introduction *healthcheck* *health.vim-intro*
Troubleshooting user configuration problems is a time-consuming task that
developers want to minimize. health.vim provides a simple framework for plugin
authors to hook into, and for users to invoke, to check and report the user's
configuration and environment. Type this command to try it: >
:CheckHealth
<
For example, some users have broken or unusual Python setups, which breaks the
|:python| command. |:CheckHealth| detects several common Python configuration
problems and reports them. If the Neovim Python module is not installed, it
shows a warning: >
You have not installed the Neovim Python module
You might want to try `pip install Neovim`
<
Plugin authors are encouraged to add healthchecks, see |health.vim-dev|.
==============================================================================
Commands and functions *health.vim-manual*
Commands
------------------------------------------------------------------------------
*:CheckHealth*
:CheckHealth Run all healthchecks and show the output in a new
tabpage. These healthchecks are included by default:
- python2
- python3
- ruby
- remote plugin
:CheckHealth {plugins}
Run healthchecks for one or more plugins. E.g. to run
only the standard Nvim healthcheck: >
:CheckHealth nvim
< To run the healthchecks for the "foo" and "bar" plugins
(assuming these plugins are on your 'runtimepath' and
they have implemented health#foo#check() and
health#bar#check(), respectively): >
:CheckHealth foo bar
<
Functions
------------------------------------------------------------------------------
health.vim functions are for creating new healthchecks. They mostly just do
some layout and formatting, to give users a consistent presentation.
health#report_start({name}) *health#report_start*
Starts a new report. Most plugins should call this only once, but if
you want different sections to appear in your report, call this once
per section.
health#report_info({msg}) *health#report_info*
Displays an informational message.
health#report_ok({msg}) *health#report_ok*
Displays a "success" message.
health#report_warn({msg}, [{suggestions}]) *health#report_warn*
Displays a warning. {suggestions} is an optional List of suggestions.
health#report_error({msg}, [{suggestions}]) *health#report_error*
Displays an error. {suggestions} is an optional List of suggestions.
health#{plugin}#check() *health.user_checker*
This is the form of a healthcheck definition. Call the above functions
from this function, then |:CheckHealth| does the rest. Example: >
function! health#my_plug#check() abort
silent call s:check_environment_vars()
silent call s:check_python_configuration()
endfunction
<
The function will be found and called automatically when the user
invokes |:CheckHealth|.
All output will be captured from the healthcheck. Use the
health#report_* functions so that your healthcheck has a format
consistent with the standard healthchecks.
==============================================================================
Create a healthcheck *health.vim-dev*
Healthchecks are functions that check the health of the system. Neovim has
built-in checkers, found in $VIMRUNTIME/autoload/health/.
To add a new checker for your own plugin, simply define a
health#{plugin}#check() function in autoload/health/{plugin}.vim.
|:CheckHealth| automatically finds and invokes such functions.
If your plugin is named "jslint", then its healthcheck function must be >
health#jslint#check()
<
defined in this file on 'runtimepath': >
autoload/health/jslint.vim
<
Here's a sample to get started: >
function! health#jslint#check() abort
call health#report_start('sanity checks')
" perform arbitrary checks
" ...
if looks_good
call health#report_ok('found required dependencies')
else
call health#report_error('cannot find jslint',
\ ['npm install --save jslint'])
endif
endfunction
<
==============================================================================
vim:tw=78:ts=8:ft=help:fdm=marker

View File

@ -79,14 +79,6 @@ TROUBLESHOOTING *python-trouble*
If you have trouble with a plugin that uses the `neovim` Python client, use
the |:CheckHealth| command to diagnose your setup.
*:CheckHealth*
:CheckHealth[!] Check your setup for common problems that may be keeping a
plugin from functioning correctly. Include the output of
this command in bug reports to help reduce the amount of
time it takes to address your issue. With "!" the output
will be placed in a new buffer which can make it easier to
save to a file or copy to the clipboard.
==============================================================================
Ruby integration *provider-ruby*

View File

@ -107,6 +107,7 @@ Events:
|TabClosed|
|TermOpen|
|TermClose|
|TextYankPost|
Highlight groups:
|hl-EndOfBuffer|

View File

@ -1 +1 @@
command! -bang CheckHealth call health#check(<bang>0)
command! -nargs=* CheckHealth call health#check([<f-args>])

View File

@ -0,0 +1,3 @@
function! health#broken#check()
throw 'caused an error'
endfunction

View File

@ -0,0 +1,6 @@
function! health#success1#check()
call health#report_start("report 1")
call health#report_ok("everything is fine")
call health#report_start("report 2")
call health#report_ok("nothing to see here")
endfunction

View File

@ -0,0 +1,4 @@
function! health#success2#check()
call health#report_start("another 1")
call health#report_ok("ok")
endfunction

View File

@ -0,0 +1,81 @@
local helpers = require('test.functional.helpers')(after_each)
local plugin_helpers = require('test.functional.plugin.helpers')
describe('health.vim', function()
before_each(function()
plugin_helpers.reset()
-- Provides functions:
-- health#broken#check()
-- health#success1#check()
-- health#success2#check()
helpers.execute("set runtimepath+=test/functional/fixtures")
end)
it("reports", function()
helpers.source([[
let g:health_report = execute([
\ "call health#report_start('Check Bar')",
\ "call health#report_ok('Bar status')",
\ "call health#report_ok('Other Bar status')",
\ "call health#report_warn('Zub')",
\ "call health#report_start('Baz')",
\ "call health#report_warn('Zim', ['suggestion 1', 'suggestion 2'])"
\ ])
]])
local result = helpers.eval("g:health_report")
helpers.eq(helpers.dedent([[
## Check Bar
- SUCCESS: Bar status
- SUCCESS: Other Bar status
- WARNING: Zub
## Baz
- WARNING: Zim
- SUGGESTIONS:
- suggestion 1
- suggestion 2]]),
result)
end)
describe(":CheckHealth", function()
it("concatenates multiple reports", function()
helpers.execute("CheckHealth success1 success2")
helpers.expect([[
health#success1#check
================================================================================
## report 1
- SUCCESS: everything is fine
## report 2
- SUCCESS: nothing to see here
health#success2#check
================================================================================
## another 1
- SUCCESS: ok]])
end)
it("gracefully handles broken healthcheck", function()
helpers.execute("CheckHealth broken")
helpers.expect([[
health#broken#check
================================================================================
- ERROR: Failed to run healthcheck for "broken" plugin. Exception:
caused an error]])
end)
it("gracefully handles invalid healthcheck", function()
helpers.execute("CheckHealth non_existent_healthcheck")
helpers.expect([[
health#non_existent_healthcheck#check
================================================================================
- ERROR: No healthcheck found for "non_existent_healthcheck" plugin.]])
end)
end)
end)