CheckHealth

- Use execute() instead of redir
- Fixed logic on suboptimal pyenv/virtualenv checks.
- Move system calls from strings to lists. Fixes #5218
- Add highlighting
- Automatically discover health checkers
- Add tests

Helped-by: Shougo Matsushita <Shougo.Matsu@gmail.com>
Helped-by: Tommy Allen <tommy@esdf.io>

Closes #4932
This commit is contained in:
TJ DeVries
2016-06-16 17:01:47 -04:00
committed by Justin M. Keyes
parent a26d52ea32
commit 2cc523c3af
7 changed files with 837 additions and 462 deletions

View File

@@ -1,468 +1,199 @@
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. 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')
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
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
echo ' Python Version:' pyversion
echo printf(' %s-neovim Version: %s', python_bin_name, current)
if current == 'not found'
call add(notes, 'Error: Neovim Python client is not installed.')
endif
if latest == 'unknown'
call add(notes, 'Warning: Unable to fetch latest Neovim Python client version.')
endif
if status == 'outdated'
echon ' (latest: '.latest.')'
else
echon ' ('.status.')'
endif
endif
call s:echo_notes(notes)
endfunction
function! s:diagnose_ruby() abort
echo 'Checking: Ruby'
let ruby_vers = systemlist('ruby -v')[0]
let ruby_prog = provider#ruby#Detect()
let notes = []
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`.')
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
endif
echo ' Ruby Version: ' . ruby_vers
echo ' Host Executable: ' . ruby_prog
echo ' Host Version: ' . prog_vers
call s:echo_notes(notes)
endfunction
" Dictionary where we keep all of the healtch check functions we've found.
" They will only be run if the value is true
let g:health_checkers = get(g:, 'health_checkers', {})
let s:current_checker = get(s:, 'current_checker', '')
""
" Function to run the health checkers
" It manages the output and any file local settings
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
let l:report = '# Checking health'
if g:health_checkers == {}
call health#add_checker(s:_default_checkers())
endif
for l:checker in items(g:health_checkers)
" Disabled checkers will not run their registered check functions
if l:checker[1]
let s:current_checker = l:checker[0]
let l:report .= "\n\n--------------------------------------------------------------------------------\n"
let l:report .= printf("\n## Checker %s says:\n", s:current_checker)
let l:report .= capture('call ' . l:checker[0] . '()')
endif
endfor
let l:report .= "\n--------------------------------------------------------------------------------\n"
if a:bang
new
setlocal bufhidden=wipe
set syntax=health
set filetype=health
call setline(1, split(report, "\n"))
setlocal nomodified
else
echo report
echo "\nTip: Use "
echohl Identifier
echon ":CheckHealth!"
echon ':CheckHealth!'
echohl None
echon " to open this in a new buffer."
echon ' to open this in a new buffer.'
endif
endfunction
" Report functions {{{
""
" Start a report section.
" It should represent a general area of tests that can be understood
" from the argument {name}
" To start a new report section, use this function again
function! health#report_start(name) abort " {{{
echo ' - Checking: ' . a:name
endfunction " }}}
""
" Format a message for a specific report item
function! s:format_report_message(status, msg, ...) abort " {{{
let l:output = ' - ' . a:status . ': ' . a:msg
" Check optional parameters
if a:0 > 0
" Suggestions go in the first optional parameter can be a string or list
if type(a:1) == type("")
let l:output .= "\n - SUGGESTIONS:"
let l:output .= "\n - " . a:1
elseif type(a:1) == type([])
" Report each suggestion
let l:output .= "\n - SUGGESTIONS:"
for l:suggestion in a:1
let l:output .= "\n - " . l:suggestion
endfor
else
echoerr "A string or list is required as the optional argument for suggestions"
endif
endif
return output
endfunction " }}}
""
" Use {msg} to report information in the current section
function! health#report_info(msg) abort " {{{
echo s:format_report_message('INFO', a:msg)
endfunction " }}}
""
" Use {msg} to represent the check that has passed
function! health#report_ok(msg) abort " {{{
echo s:format_report_message('SUCCESS', a:msg)
endfunction " }}}
""
" Use {msg} to represent a failed health check and optionally a list of suggestions on how to fix it.
function! health#report_warn(msg, ...) abort " {{{
if a:0 > 0 && type(a:1) == type([])
echo s:format_report_message('WARNING', a:msg, a:1)
else
echo s:format_report_message('WARNING', a:msg)
endif
endfunction " }}}
""
" Use {msg} to represent a critically failed health check and optionally a list of suggestions on how to fix it.
function! health#report_error(msg, ...) abort " {{{
if a:0 > 0 && type(a:1) == type([])
echo s:format_report_message('ERROR', a:msg, a:1)
else
echo s:format_report_message('ERROR', a:msg)
endif
endfunction " }}}
" }}}
" Health checker management {{{
""
" Add a single health checker
" It does not modify any values if the checker already exists
function! s:add_single_checker(checker_name) abort " {{{
if has_key(g:health_checkers, a:checker_name)
return
else
let g:health_checkers[a:checker_name] = v:true
endif
endfunction " }}}
""
" Enable a single health checker
" It will modify the values if the checker already exists
function! s:enable_single_checker(checker_name) abort " {{{
let g:health_checkers[a:checker_name] = v:true
endfunction " }}}
""
" Disable a single health checker
" It will modify the values if the checker already exists
function! s:disable_single_checker(checker_name) abort " {{{
let g:health_checkers[a:checker_name] = v:false
endfunction " }}}
""
" Add at least one health checker
" {checker_name} can be specified by either a list of strings or a single string.
" It does not modify any values if the checker already exists
function! health#add_checker(checker_name) abort " {{{
if type(a:checker_name) == type('')
call s:add_single_checker(a:checker_name)
elseif type(a:checker_name) == type([])
for checker in a:checker_name
call s:add_single_checker(checker)
endfor
endif
endfunction " }}}
""
" Enable at least one health checker
" {checker_name} can be specified by either a list of strings or a single string.
function! health#enable_checker(checker_name) abort " {{{
if type(a:checker_name) == type('')
call s:enable_single_checker(a:checker_name)
elseif type(a:checker_name) == type([])
for checker in a:checker_name
call s:enable_single_checker(checker)
endfor
endif
endfunction " }}}
""
" Disable at least one health checker
" {checker_name} can be specified by either a list of strings or a single string.
function! health#disable_checker(checker_name) abort " {{{
if type(a:checker_name) == type('')
call s:disable_single_checker(a:checker_name)
elseif type(a:checker_name) == type([])
for checker in a:checker_name
call s:disable_single_checker(checker)
endfor
endif
endfunction " }}}
function! s:change_file_name_to_health_checker(name) abort " {{{
return substitute(substitute(substitute(a:name, ".*autoload/", "", ""), "\\.vim", "#check", ""), "/", "#", "g")
endfunction " }}}
function! s:_default_checkers() abort " {{{
" Get all of the files that are in autoload/health/ folders with a vim
" suffix
let checker_files = globpath(&runtimepath, 'autoload/health/*.vim', 1, 1)
let temp = checker_files[0]
let checkers_to_source = []
for file_name in checker_files
call add(checkers_to_source, s:change_file_name_to_health_checker(file_name))
endfor
return checkers_to_source
endfunction " }}}
" }}}

View File

@@ -0,0 +1,426 @@
" Script variables
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'
\ ]
""
" Check if the string is a bad response
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. The 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
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 = []
call health#report_start('Python ' . a:version . ' Configuration')
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.')
" TODO: Not sure what to do about these errors, or if this is the right
" type.
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('Provier python has reported errors:', pythonx_errs)
endif
if !empty(python_bin_name) && empty(python_bin) && empty(pythonx_errs)
if !exists('g:'.host_prog_var)
call health#report_warn(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 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! health#nvim#check() abort
silent call s:check_python(2)
silent echo ''
silent call s:check_python(3)
silent echo ''
silent call s:check_manifest()
silent echo ''
endfunction

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

@@ -0,0 +1,146 @@
*pi_health.txt* Check the status of your Neovim system
Author: TJ DeVries <devries.timothyj@gmail.com>
==============================================================================
1. Contents *health.vim-contents*
1. Contents : |health.vim-contents|
2. Health.vim introduction : |health.vim-intro|
3. Health.vim manual : |health.vim-manual|
3.1 Health.vim commands : |health.vim-commands|
4. Making a new checker : |health.vim-checkers|
==============================================================================
2. Health.vim introduction *health.vim-intro*
Debugging common issues is a time consuming task that many developers would
like to eliminate, and where elimination is impossible, minimize. Many common
questions and difficulties could be answered by a simple check of an
environment variable or a setting that the user has made. However, even with
FAQs and other manuals, it can be difficult to suggest the path a user should
take without knowing some information about their system.
Health.vim aims to solve this problem in two ways for both core and plugin
maintainers.
The way this is done is to provide an interface that users will know to check
first before posting question in the issue tracker, dev channels, etc. This
is similar to how |:help| functions currently. The user experiencing
difficulty can run |:CheckHealth| to view the status of one's system.
The aim of |:CheckHealth| is two-fold.
The first aim is to provide maintainers with an overview of the user's working
environment. This skips large amounts of time where the maintainer must
instruct the user on which steps to take to get debug information, and allows
the maintainer to extend existing health scripts as more helpful debug
information is found.
The second aim is to provide maintainers a way of automating the answering of
frequently encountered question. A common occurrence with Neovim is that the
user has not installed the necessary Python modules to interact with Python
remote plugins. A simple check of whether the Neovim remote plugin is
installed can lead to a suggestion of >
You have not installed the Neovim Python module
You might want to try `$ pip install Neovim`
<
With these possibilities, it allows the maintainer of a plugin to spend more
time on active development, rather than trying to spend time on debugging
common issues many times.
==============================================================================
3. Health.vim manual *health.vim-manual*
3.1 Commands
------------
:CheckHealth[!] *:CheckHealth*
Run all health checkers found in g:health_checkers
It will 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.
3.2 Functions *health.functions*
-------------
3.2.1 Report Functions *health.report_functions*
----------------------
The |health.report_functions| are used by the plugin maintainer to remove the
hassle of formatting multiple different levels of output. Not only does it
remove the hassle of formatting, but it also provides users with a consistent
interface for viewing the health information about the system.
These functions are also expected to have the capability to produce output in
multiple different formats. For example, if parsing of the results were to be
done by a remote plugin, the results could be output in a valid JSON format
and then the remote plugin could parse the results easily.
health#report_start({name}) *health.funcs.report_start*
Start a report section. It should represent a general area of tests
that can be understood from the argument {name} To start a new report
section, use this function again
health#report_info({msg}) *health.funcs.report_info*
Use {msg} to report information in the current section
health#report_ok({msg}) *health.funcs.report_ok*
Use {msg} to represent the check that has passed
health#report_warn({msg}, ...) *health.funcs.report_warn*
Use {msg} to represent a failed health check and optionally a list of
suggestions on how to fix it.
health#report_error({msg}, ...) *health.funcs.report_error*
Use {msg} to represent a critically failed health check and optionally
a list of suggestions on how to fix it.
3.3 User Functions *health.user_functions*
------------------
health#{my_plug}#check() *health.user_checker*
A user defined function to run all of the checks that are required for
either debugging or suggestion making. An example might be something
like: >
function! health#my_plug#check() abort
silent call s:check_environment_vars()
silent call s:check_python_configuration()
endfunction
<
This function will be found, sourced, and automatically called when
the user invokes |:CheckHealth|.
All output will be captured from the health checker. It is recommended
that the plugin maintainer uses the calls described in
|health.report_functions|. The benefits these functions provide are
described in the same section.
==============================================================================
4. Making a new checker *health.vim-checkers*
Health checkers are the scripts that check the health of the system. Neovim
has built in checkers, which can be found in `runtime/autoload/health/`. To
add a checker for a plugin, add a `health` folder in the `autoload` directory
of your plugin. It is then suggested that the name of your script be
`{plug_name}.vim`. For example, the health checker for `my_plug` might be
placed in: >
$PLUGIN_BASE/autoload/health/my_plug.vim
>
Inside this script, a function must be specified to run. This function is
described in |health.user_checker|.
==============================================================================
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

@@ -1 +1,3 @@
" call health#add_checker('health#nvim#check')
command! -bang CheckHealth call health#check(<bang>0)

20
runtime/syntax/health.vim Normal file
View File

@@ -0,0 +1,20 @@
if exists("b:current_syntax")
finish
endif
syntax keyword healthError ERROR
highlight link healthError Error
syntax keyword healthWarning WARNING
highlight link healthWarning Todo
syntax keyword healthInfo INFO
highlight link healthInfo Identifier
syntax keyword healthSuccess SUCCESS
highlight link healthSuccess Function
syntax keyword healthSuggestion SUGGESTION
highlight link healthSuggestion String
let b:current_syntax = "health"

View File

@@ -0,0 +1,58 @@
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()
end)
it('should echo the results when using the basic functions', function()
helpers.execute("call health#report_start('Foo')")
local report = helpers.redir_exec([[call health#report_start('Check Bar')]])
.. helpers.redir_exec([[call health#report_ok('Bar status')]])
.. helpers.redir_exec([[call health#report_ok('Other Bar status')]])
.. helpers.redir_exec([[call health#report_warn('Zub')]])
.. helpers.redir_exec([[call health#report_start('Baz')]])
.. helpers.redir_exec([[call health#report_warn('Zim', ['suggestion 1', 'suggestion 2'])]])
local expected_contents = {
'Checking: Check Bar',
'SUCCESS: Bar status',
'WARNING: Zub',
'SUGGESTIONS:',
'- suggestion 1',
'- suggestion 2'
}
for _, content in ipairs(expected_contents) do
assert(string.find(report, content))
end
end)
describe('CheckHealth', function()
-- Run the health check and store important results
-- Run it here because it may take awhile to complete, depending on the system
helpers.execute([[CheckHealth!]])
local report = helpers.curbuf_contents()
local health_checkers = helpers.redir_exec("echo g:health_checkers")
it('should find the default checker upon execution', function()
assert(string.find(health_checkers, "'health#nvim#check': v:true"))
end)
it('should alert the user that health#nvim#check is running', function()
assert(string.find(report, '# Checking health'))
assert(string.find(report, 'Checker health#nvim#check says:'))
assert(string.find(report, 'Checking:'))
end)
end)
it('should allow users to disable checkers', function()
helpers.execute("call health#disable_checker('health#nvim#check')")
helpers.execute("CheckHealth!")
local health_checkers = helpers.redir_exec("echo g:health_checkers")
assert(string.find(health_checkers, "'health#nvim#check': v:false"))
end)
end)