diff options
43 files changed, 1533 insertions, 890 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 3877699af9..cb044d521c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -59,7 +59,7 @@ set_property(CACHE CMAKE_BUILD_TYPE PROPERTY # version string, else it is combined with the result of `git describe`. set(NVIM_VERSION_MAJOR 0) set(NVIM_VERSION_MINOR 1) -set(NVIM_VERSION_PATCH 5) +set(NVIM_VERSION_PATCH 6) set(NVIM_VERSION_PRERELEASE "-dev") # for package maintainers file(TO_CMAKE_PATH ${CMAKE_CURRENT_LIST_DIR}/.git FORCED_GIT_DIR) diff --git a/cmake/RunTests.cmake b/cmake/RunTests.cmake index 9fa91ffb5d..a045f9f982 100644 --- a/cmake/RunTests.cmake +++ b/cmake/RunTests.cmake @@ -2,6 +2,9 @@ get_filename_component(BUSTED_DIR ${BUSTED_PRG} PATH) set(ENV{PATH} "${BUSTED_DIR}:$ENV{PATH}") set(ENV{VIMRUNTIME} ${WORKING_DIR}/runtime) +set(ENV{NVIM_RPLUGIN_MANIFEST} ${WORKING_DIR}/Xtest_rplugin_manifest) +set(ENV{XDG_CONFIG_HOME} ${WORKING_DIR}/Xtest_xdg/config) +set(ENV{XDG_DATA_HOME} ${WORKING_DIR}/Xtest_xdg/share) if(NVIM_PRG) set(ENV{NVIM_PROG} "${NVIM_PRG}") @@ -34,6 +37,9 @@ execute_process( RESULT_VARIABLE res ${EXTRA_ARGS}) +file(REMOVE ${WORKING_DIR}/Xtest_rplugin_manifest) +file(REMOVE_RECURSE ${WORKING_DIR}/Xtest_xdg) + if(NOT res EQUAL 0) message(STATUS "Output to stderr:\n${err}") message(FATAL_ERROR "Running ${TEST_TYPE} tests failed with error: ${res}.") diff --git a/runtime/autoload/health.vim b/runtime/autoload/health.vim index 0a698e6492..6ba9405e0a 100644 --- a/runtime/autoload/health.vim +++ b/runtime/autoload/health.vim @@ -1,468 +1,150 @@ -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 +function! s:enhance_syntax() abort + syntax keyword healthError ERROR + highlight link healthError Error - 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 + syntax keyword healthWarning WARNING + highlight link healthWarning WarningMsg + syntax keyword healthInfo INFO + highlight link healthInfo ModeMsg -" 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 + syntax keyword healthSuccess SUCCESS + highlight link healthSuccess Function - 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 + 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 = '' -" 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 healthchecks = empty(a:plugin_names) + \ ? s:discover_health_checks() + \ : s:to_fn_names(a:plugin_names) - 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 + if empty(healthchecks) + let report = "ERROR: No healthchecks found." + else + 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 - 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] + tabnew + setlocal bufhidden=wipe + set filetype=markdown + call s:enhance_syntax() + call setline(1, split(report, "\n")) + setlocal nomodified 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 +" Starts a new report. +function! health#report_start(name) abort + echo "\n## " . a:name 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 +" 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 - - 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 + 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 +" 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 = [] -" 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 + " 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 - - 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 + " 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 - 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 + return output +endfunction " }}} - echo ' Python Version:' pyversion - echo printf(' %s-neovim Version: %s', python_bin_name, current) +" 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 current == 'not found' - call add(notes, 'Error: Neovim Python client is not installed.') - endif +" Reports a successful healthcheck. +function! health#report_ok(msg) abort " {{{ + echo s:format_report_message('SUCCESS', a:msg) +endfunction " }}} - 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 +" 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 + echo s:format_report_message('WARNING', a:msg) endif +endfunction " }}} - 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`.') +" 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 - 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('ERROR', a:msg) endif +endfunction " }}} - echo ' Ruby Version: ' . ruby_vers - echo ' Host Executable: ' . ruby_prog - echo ' Host Version: ' . prog_vers - - call s:echo_notes(notes) +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 -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 +" 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 - if a:bang - new - setlocal bufhidden=wipe - call setline(1, split(report, "\n")) - setlocal nomodified - else - echo report - echo "\nTip: Use " - echohl Identifier - echon ":CheckHealth!" - echohl None - echon " to open this in a new buffer." - endif +" Extracts 'foo' from 'health#foo#check'. +function! s:to_plugin_name(fn_name) abort + return substitute(a:fn_name, + \ '\v.*health\#(.+)\#check.*', '\1', '') endfunction diff --git a/runtime/autoload/health/nvim.vim b/runtime/autoload/health/nvim.vim new file mode 100644 index 0000000000..7865634313 --- /dev/null +++ b/runtime/autoload/health/nvim.vim @@ -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 diff --git a/runtime/autoload/provider/pythonx.vim b/runtime/autoload/provider/pythonx.vim index 0ebf00112f..6d6b38978c 100644 --- a/runtime/autoload/provider/pythonx.vim +++ b/runtime/autoload/provider/pythonx.vim @@ -5,11 +5,24 @@ endif let s:loaded_pythonx_provider = 1 +let s:stderr = {} +let s:job_opts = {'rpc': v:true} + +" TODO(bfredl): this logic is common and should be builtin +function! s:job_opts.on_stderr(chan_id, data, event) + let stderr = get(s:stderr, a:chan_id, ['']) + let last = remove(stderr, -1) + let a:data[0] = last.a:data[0] + call extend(stderr, a:data) + let s:stderr[a:chan_id] = stderr +endfunction + function! provider#pythonx#Require(host) abort let ver = (a:host.orig_name ==# 'python') ? 2 : 3 " Python host arguments - let args = ['-c', 'import sys; sys.path.remove(""); import neovim; neovim.start_host()'] + let prog = (ver == '2' ? provider#python#Prog() : provider#python3#Prog()) + let args = [prog, '-c', 'import sys; sys.path.remove(""); import neovim; neovim.start_host()'] " Collect registered Python plugins into args let python_plugins = remote#host#PluginsForHost(a:host.name) @@ -18,14 +31,16 @@ function! provider#pythonx#Require(host) abort endfor try - let channel_id = rpcstart((ver ==# '2' ? - \ provider#python#Prog() : provider#python3#Prog()), args) + let channel_id = jobstart(args, s:job_opts) if rpcrequest(channel_id, 'poll') ==# 'ok' return channel_id endif catch echomsg v:throwpoint echomsg v:exception + for row in get(s:stderr, channel_id, []) + echomsg row + endfor endtry throw remote#host#LoadErrorForHost(a:host.orig_name, \ '$NVIM_PYTHON_LOG_FILE') diff --git a/runtime/autoload/provider/ruby.vim b/runtime/autoload/provider/ruby.vim index e9130b98c1..c8ede20a75 100644 --- a/runtime/autoload/provider/ruby.vim +++ b/runtime/autoload/provider/ruby.vim @@ -4,6 +4,17 @@ if exists('g:loaded_ruby_provider') endif let g:loaded_ruby_provider = 1 +let s:stderr = {} +let s:job_opts = {'rpc': v:true} + +function! s:job_opts.on_stderr(chan_id, data, event) + let stderr = get(s:stderr, a:chan_id, ['']) + let last = remove(stderr, -1) + let a:data[0] = last.a:data[0] + call extend(stderr, a:data) + let s:stderr[a:chan_id] = stderr +endfunction + function! provider#ruby#Detect() abort return exepath('neovim-ruby-host') endfunction @@ -13,7 +24,7 @@ function! provider#ruby#Prog() endfunction function! provider#ruby#Require(host) abort - let args = [] + let args = [provider#ruby#Prog()] let ruby_plugins = remote#host#PluginsForHost(a:host.name) for plugin in ruby_plugins @@ -21,13 +32,16 @@ function! provider#ruby#Require(host) abort endfor try - let channel_id = rpcstart(provider#ruby#Prog(), args) + let channel_id = jobstart(args, s:job_opts) if rpcrequest(channel_id, 'poll') ==# 'ok' return channel_id endif catch echomsg v:throwpoint echomsg v:exception + for row in get(s:stderr, channel_id, []) + echomsg row + endfor endtry throw remote#host#LoadErrorForHost(a:host.orig_name, '$NVIM_RUBY_LOG_FILE') endfunction diff --git a/runtime/autoload/remote/host.vim b/runtime/autoload/remote/host.vim index eb5e87d7e1..110f80297a 100644 --- a/runtime/autoload/remote/host.vim +++ b/runtime/autoload/remote/host.vim @@ -118,7 +118,32 @@ function! remote#host#RegisterPlugin(host, path, specs) abort endfunction -function! s:GetManifest() abort +" Get the path to the rplugin manifest file. +function! s:GetManifestPath() abort + let manifest_base = '' + + if exists('$NVIM_RPLUGIN_MANIFEST') + return fnamemodify($NVIM_RPLUGIN_MANIFEST, ':p') + endif + + let dest = has('win32') ? '$LOCALAPPDATA' : '$XDG_DATA_HOME' + if !exists(dest) + let dest = has('win32') ? '~/AppData/Local' : '~/.local/share' + endif + + let dest = fnamemodify(expand(dest), ':p') + if !empty(dest) && !filereadable(dest) + let dest .= ('/' ==# dest[-1:] ? '' : '/') . 'nvim' + call mkdir(dest, 'p', 0700) + let manifest_base = dest + endif + + return manifest_base.'/rplugin.vim' +endfunction + + +" Old manifest file based on known script locations. +function! s:GetOldManifestPath() abort let prefix = exists('$MYVIMRC') \ ? $MYVIMRC \ : matchstr(get(split(execute('scriptnames'), '\n'), 0, ''), '\f\+$') @@ -127,9 +152,25 @@ function! s:GetManifest() abort endfunction +function! s:GetManifest() abort + let manifest = s:GetManifestPath() + + if !filereadable(manifest) + " Check if an old manifest file exists and move it to the new location. + let old_manifest = s:GetOldManifestPath() + if filereadable(old_manifest) + call rename(old_manifest, manifest) + endif + endif + + return manifest +endfunction + + function! remote#host#LoadRemotePlugins() abort - if filereadable(s:GetManifest()) - exe 'source '.s:GetManifest() + let manifest = s:GetManifest() + if filereadable(manifest) + execute 'source' fnameescape(manifest) endif endfunction @@ -137,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 @@ -202,7 +245,7 @@ function! remote#host#UpdateRemotePlugins() abort endif endfor call writefile(commands, s:GetManifest()) - echomsg printf('remote/host: generated the manifest file in "%s"', + echomsg printf('remote/host: generated rplugin manifest: %s', \ s:GetManifest()) endfunction @@ -220,9 +263,8 @@ function! remote#host#LoadErrorForHost(host, log) abort \ 'You can try to see what happened '. \ 'by starting Neovim with the environment variable '. \ a:log . ' set to a file and opening the generated '. - \ 'log file. Also, the host stderr will be available '. - \ 'in Neovim log, so it may contain useful information. '. - \ 'See also ~/.nvimlog.' + \ 'log file. Also, the host stderr is available '. + \ 'in messages.' endfunction diff --git a/runtime/doc/eval.txt b/runtime/doc/eval.txt index 3fa5474a7e..03d8f84aa6 100644 --- a/runtime/doc/eval.txt +++ b/runtime/doc/eval.txt @@ -2043,7 +2043,6 @@ rpcnotify({channel}, {event}[, {args}...]) Sends an |RPC| notification to {channel} rpcrequest({channel}, {method}[, {args}...]) Sends an |RPC| request to {channel} -rpcstart({prog}[, {argv}]) Spawns {prog} and opens an |RPC| channel rpcstop({channel}) Closes an |RPC| {channel} screenattr({row}, {col}) Number attribute at screen position screenchar({row}, {col}) Number character at screen position @@ -4395,8 +4394,10 @@ items({dict}) *items()* order. jobclose({job}[, {stream}]) {Nvim} *jobclose()* - Close {job}'s {stream}, which can be one "stdin", "stdout" or - "stderr". If {stream} is omitted, all streams are closed. + Close {job}'s {stream}, which can be one of "stdin", "stdout", + "stderr" or "rpc" (closes the rpc channel for a job started + with the "rpc" option.) If {stream} is omitted, all streams + are closed. jobpid({job}) {Nvim} *jobpid()* Return the pid (process id) of {job}. @@ -4418,6 +4419,10 @@ jobsend({job}, {data}) {Nvim} *jobsend()* :call jobsend(j, ["abc", "123\n456", ""]) < will send "abc<NL>123<NUL>456<NL>". + If the job was started with the rpc option this function + cannot be used, instead use |rpcnotify()| and |rpcrequest()| + to communicate with the job. + jobstart({cmd}[, {opts}]) {Nvim} *jobstart()* Spawns {cmd} as a job. If {cmd} is a |List| it is run directly. If {cmd} is a |String| it is processed like this: > @@ -4433,9 +4438,14 @@ jobstart({cmd}[, {opts}]) {Nvim} *jobstart()* on_exit : exit event handler (function name or |Funcref|) cwd : Working directory of the job; defaults to |current-directory|. + rpc : If set, |msgpack-rpc| will be used to communicate + with the job over stdin and stdout. "on_stdout" is + then ignored, but "on_stderr" can still be used. pty : If set, the job will be connected to a new pseudo - terminal, and the job streams are connected to - the master file descriptor. + terminal, and the job streams are connected to + the master file descriptor. "on_stderr" is ignored + as all output will be received on stdout. + width : (pty only) Width of the terminal screen height : (pty only) Height of the terminal screen TERM : (pty only) $TERM environment variable @@ -4447,10 +4457,12 @@ jobstart({cmd}[, {opts}]) {Nvim} *jobstart()* {opts} is passed as |self| to the callback; the caller may pass arbitrary data by setting other keys. Returns: - - job ID on success, used by |jobsend()| and |jobstop()| + - The job ID on success, which is used by |jobsend()| (or + |rpcnotify()| and |rpcrequest()| if "rpc" option was used) + and |jobstop()| - 0 on invalid arguments or if the job table is full - -1 if {cmd}[0] is not executable. - See |job-control| for more information. + See |job-control| and |msgpack-rpc| for more information. jobstop({job}) {Nvim} *jobstop()* Stop a job created with |jobstart()| by sending a `SIGTERM` @@ -5649,19 +5661,19 @@ rpcrequest({channel}, {method}[, {args}...]) {Nvim} *rpcrequest()* :let result = rpcrequest(rpc_chan, "func", 1, 2, 3) rpcstart({prog}[, {argv}]) {Nvim} *rpcstart()* - Spawns {prog} as a job (optionally passing the list {argv}), - and opens an |RPC| channel with the spawned process's - stdin/stdout. Returns: - - channel id on success, which is used by |rpcrequest()|, - |rpcnotify()| and |rpcstop()| - - 0 on failure - Example: > - :let rpc_chan = rpcstart('prog', ['arg1', 'arg2']) + Deprecated. Replace > + :let id = rpcstart('prog', ['arg1', 'arg2']) +< with > + :let id = jobstart(['prog', 'arg1', 'arg2'], {'rpc': v:true}) rpcstop({channel}) {Nvim} *rpcstop()* - Closes an |RPC| {channel}, possibly created via - |rpcstart()|. Also closes channels created by connections to - |v:servername|. + Closes an |RPC| {channel}. If the channel is a job + started with |jobstart()| the job is killed. + It is better to use |jobstop()| in this case, or use + |jobclose|(id, "rpc") to only close the channel without + killing the job. + Closes the socket connection if the channel was opened by + connecting to |v:servername|. screenattr(row, col) *screenattr()* Like screenchar(), but return the attribute. This is a rather diff --git a/runtime/doc/msgpack_rpc.txt b/runtime/doc/msgpack_rpc.txt index cfd9084cfc..18c0ff8a58 100644 --- a/runtime/doc/msgpack_rpc.txt +++ b/runtime/doc/msgpack_rpc.txt @@ -11,7 +11,6 @@ RPC API for Nvim *RPC* *rpc* *msgpack-rpc* 3. Connecting |rpc-connecting| 4. Clients |rpc-api-client| 5. Types |rpc-types| -6. Vimscript functions |rpc-vim-functions| ============================================================================== 1. Introduction *rpc-intro* @@ -66,12 +65,16 @@ To get a formatted dump of the API using python (requires the `pyyaml` and ============================================================================== 3. Connecting *rpc-connecting* -There are several ways to open a msgpack-rpc stream to an Nvim server: +There are several ways to open a msgpack-rpc channel to an Nvim instance: 1. Through stdin/stdout when `nvim` is started with `--embed`. This is how applications can embed Nvim. - 2. Through stdin/stdout of some other process spawned by |rpcstart()|. + 2. Through stdin/stdout of some other process spawned by |jobstart()|. + Set the "rpc" key to |v:true| in the options dict to use the job's stdin + and stdout as a single msgpack channel that is processed directly by + Nvim. Then it is not possible to process raw data to or from the + process's stdin and stdout. stderr can still be used, though. 3. Through the socket automatically created with each instance. The socket location is stored in |v:servername|. @@ -110,11 +113,12 @@ functions can be called interactively: >>> nvim = attach('socket', path='[address]') >>> nvim.command('echo "hello world!"') < -You can also embed an Nvim instance via |rpcstart()| +You can also embed an Nvim instance via |jobstart()|, and communicate using +|rpcrequest()| and |rpcnotify()|: > - let vim = rpcstart('nvim', ['--embed']) + let vim = jobstart(['nvim', '--embed'], {'rpc': v:true}) echo rpcrequest(vim, 'vim_eval', '"Hello " . "world!"') - call rpcstop(vim) + call jobstop(vim) < ============================================================================== 4. Implementing API clients *rpc-api-client* *api-client* @@ -234,22 +238,4 @@ the type codes, because a client may be built against one Nvim version but connect to another with different type codes. ============================================================================== -6. Vimscript functions *rpc-vim-functions* - -RPC functions are available in Vimscript: - - 1. |rpcstart()|: Similarly to |jobstart()|, this will spawn a co-process - with its standard handles connected to Nvim. The difference is that it's - not possible to process raw data to or from the process's stdin, stdout, - or stderr. This is because the job's stdin and stdout are used as - a single msgpack channel that is processed directly by Nvim. - 2. |rpcstop()|: Same as |jobstop()|, but operates on handles returned by - |rpcstart()|. - 3. |rpcrequest()|: Sends a msgpack-rpc request to the process. - 4. |rpcnotify()|: Sends a msgpack-rpc notification to the process. - -|rpcrequest()| and |rpcnotify()| can also be used with channels connected to -a nvim server. |v:servername| - -============================================================================== vim:tw=78:ts=8:noet:ft=help:norl: diff --git a/runtime/doc/pi_health.txt b/runtime/doc/pi_health.txt new file mode 100644 index 0000000000..69833103d1 --- /dev/null +++ b/runtime/doc/pi_health.txt @@ -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 diff --git a/runtime/doc/provider.txt b/runtime/doc/provider.txt index 7380fb9346..63dbb00896 100644 --- a/runtime/doc/provider.txt +++ b/runtime/doc/provider.txt @@ -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* diff --git a/runtime/doc/remote_plugin.txt b/runtime/doc/remote_plugin.txt index d906096a86..dddc021d68 100644 --- a/runtime/doc/remote_plugin.txt +++ b/runtime/doc/remote_plugin.txt @@ -93,22 +93,22 @@ approach with |rpcnotify()|, meaning return values or exceptions raised in the handler function are ignored. To test the above plugin, it must be saved in "rplugin/python" in a -'runtimepath' directory (~/.config/nvim/rplugin/python/limit.py for example). -Then, the remote plugin manifest must be generated with -`:UpdateRemotePlugins`. +'runtimepath' directory (~/.config/nvim/rplugin/python/limit.py for example). +Then, the remote plugin manifest must be generated with +|:UpdateRemotePlugins|. ============================================================================== 4. Remote plugin manifest *remote-plugin-manifest* + *:UpdateRemotePlugins* Just installing remote plugins to "rplugin/{host}" isn't enough for them to be -automatically loaded when required. You must execute `:UpdateRemotePlugins` +automatically loaded when required. You must execute |:UpdateRemotePlugins| every time a remote plugin is installed, updated, or deleted. -`:UpdateRemotePlugins` generates the remote plugin manifest, a special +|:UpdateRemotePlugins| generates the remote plugin manifest, a special Vimscript file containing declarations for all Vimscript entities (commands/autocommands/functions) defined by all remote plugins, with each -entity associated with the host and plugin path. The manifest is a generated -extension to the user's vimrc (it even has the vimrc filename prepended). +entity associated with the host and plugin path. Manifest declarations are just calls to the `remote#host#RegisterPlugin` function, which takes care of bootstrapping the host as soon as the declared @@ -125,10 +125,20 @@ the example, say the Java plugin is a semantic completion engine for Java code. If it defines the autocommand "BufEnter *.java", then the Java host is spawned only when Nvim loads a buffer matching "*.java". -If the explicit call to `:UpdateRemotePlugins` seems incovenient, try to see it +If the explicit call to |:UpdateRemotePlugins| seems incovenient, try to see it like this: It's a way to provide IDE capabilities in Nvim while still keeping it fast and lightweight for general use. It's also analogous to the |:helptags| command. + *$NVIM_RPLUGIN_MANIFEST* +Unless $NVIM_RPLUGIN_MANIFEST is set the manifest will be written to a file +named `rplugin.vim` at: + + Unix ~ + $XDG_DATA_HOME/nvim/ or ~/.local/share/nvim/ + + Windows ~ + $LOCALAPPDATA/nvim/ or ~/AppData/Local/nvim/ + ============================================================================== vim:tw=78:ts=8:noet:ft=help:norl: diff --git a/runtime/doc/vim_diff.txt b/runtime/doc/vim_diff.txt index 2b1c66d0c1..8ed8a7f64c 100644 --- a/runtime/doc/vim_diff.txt +++ b/runtime/doc/vim_diff.txt @@ -107,6 +107,7 @@ Events: |TabClosed| |TermOpen| |TermClose| + |TextYankPost| Highlight groups: |hl-EndOfBuffer| diff --git a/runtime/ftplugin/man.vim b/runtime/ftplugin/man.vim index 79aecb123d..6a9ad27956 100644 --- a/runtime/ftplugin/man.vim +++ b/runtime/ftplugin/man.vim @@ -1,7 +1,7 @@ " Maintainer: Anmol Sethi <anmol@aubble.com> " Previous Maintainer: SungHyun Nam <goweol@gmail.com> -if exists('b:did_ftplugin') +if exists('b:did_ftplugin') || &filetype !=# 'man' finish endif let b:did_ftplugin = 1 diff --git a/runtime/plugin/health.vim b/runtime/plugin/health.vim index db094a03a4..3c8e509acd 100644 --- a/runtime/plugin/health.vim +++ b/runtime/plugin/health.vim @@ -1 +1 @@ -command! -bang CheckHealth call health#check(<bang>0) +command! -nargs=* CheckHealth call health#check([<f-args>]) diff --git a/scripts/release.sh b/scripts/release.sh index 67738ccc96..41a246c041 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/usr/bin/env bash # Performs steps to tag a release. # diff --git a/src/nvim/eval.c b/src/nvim/eval.c index d936c9572a..dce24230b0 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -408,6 +408,7 @@ typedef struct { Terminal *term; bool stopped; bool exited; + bool rpc; int refcount; ufunc_T *on_stdout, *on_stderr, *on_exit; dict_T *self; @@ -448,8 +449,7 @@ typedef struct { #define FNE_INCL_BR 1 /* find_name_end(): include [] in name */ #define FNE_CHECK_START 2 /* find_name_end(): check name starts with valid character */ -static uint64_t current_job_id = 1; -static PMap(uint64_t) *jobs = NULL; +static PMap(uint64_t) *jobs = NULL; static uint64_t last_timer_id = 0; static PMap(uint64_t) *timers = NULL; @@ -11724,16 +11724,35 @@ static void f_jobclose(typval_T *argvars, typval_T *rettv) if (argvars[1].v_type == VAR_STRING) { char *stream = (char *)argvars[1].vval.v_string; if (!strcmp(stream, "stdin")) { - process_close_in(proc); + if (data->rpc) { + EMSG(_("Invalid stream on rpc job, use jobclose(id, 'rpc')")); + } else { + process_close_in(proc); + } } else if (!strcmp(stream, "stdout")) { - process_close_out(proc); + if (data->rpc) { + EMSG(_("Invalid stream on rpc job, use jobclose(id, 'rpc')")); + } else { + process_close_out(proc); + } } else if (!strcmp(stream, "stderr")) { process_close_err(proc); + } else if (!strcmp(stream, "rpc")) { + if (data->rpc) { + channel_close(data->id); + } else { + EMSG(_("Invalid job stream: Not an rpc job")); + } } else { EMSG2(_("Invalid job stream \"%s\""), stream); } } else { - process_close_streams(proc); + if (data->rpc) { + channel_close(data->id); + process_close_err(proc); + } else { + process_close_streams(proc); + } } } @@ -11790,6 +11809,11 @@ static void f_jobsend(typval_T *argvars, typval_T *rettv) return; } + if (data->rpc) { + EMSG(_("Can't send raw data to rpc channel")); + return; + } + ssize_t input_len; char *input = (char *) save_tv_as_string(&argvars[1], &input_len, false); if (!input) { @@ -11911,12 +11935,23 @@ static void f_jobstart(typval_T *argvars, typval_T *rettv) return; } + dict_T *job_opts = NULL; + bool detach = false, rpc = false, pty = false; ufunc_T *on_stdout = NULL, *on_stderr = NULL, *on_exit = NULL; char *cwd = NULL; if (argvars[1].v_type == VAR_DICT) { job_opts = argvars[1].vval.v_dict; + detach = get_dict_number(job_opts, (uint8_t *)"detach") != 0; + rpc = get_dict_number(job_opts, (uint8_t *)"rpc") != 0; + pty = get_dict_number(job_opts, (uint8_t *)"pty") != 0; + if (pty && rpc) { + EMSG2(_(e_invarg2), "job cannot have both 'pty' and 'rpc' options set"); + shell_free_argv(argv); + return; + } + char *new_cwd = (char *)get_dict_string(job_opts, (char_u *)"cwd", false); if (new_cwd && strlen(new_cwd) > 0) { cwd = new_cwd; @@ -11934,10 +11969,8 @@ static void f_jobstart(typval_T *argvars, typval_T *rettv) } } - bool pty = job_opts && get_dict_number(job_opts, (uint8_t *)"pty") != 0; - bool detach = job_opts && get_dict_number(job_opts, (uint8_t *)"detach") != 0; TerminalJobData *data = common_job_init(argv, on_stdout, on_stderr, on_exit, - job_opts, pty, detach, cwd); + job_opts, pty, rpc, detach, cwd); Process *proc = (Process *)&data->proc; if (pty) { @@ -11955,7 +11988,7 @@ static void f_jobstart(typval_T *argvars, typval_T *rettv) } } - if (!on_stdout) { + if (!rpc && !on_stdout) { proc->out = NULL; } if (!on_stderr) { @@ -14105,7 +14138,7 @@ end: api_free_object(result); } -// "rpcstart()" function +// "rpcstart()" function (DEPRECATED) static void f_rpcstart(typval_T *argvars, typval_T *rettv) { rettv->v_type = VAR_NUMBER; @@ -14158,32 +14191,27 @@ static void f_rpcstart(typval_T *argvars, typval_T *rettv) // The last item of argv must be NULL argv[i] = NULL; - uint64_t channel_id = channel_from_process(argv); - if (!channel_id) { - EMSG(_(e_api_spawn_failed)); - } - - rettv->vval.v_number = (varnumber_T)channel_id; + TerminalJobData *data = common_job_init(argv, NULL, NULL, NULL, + NULL, false, true, false, NULL); + common_job_start(data, rettv); } // "rpcstop()" function static void f_rpcstop(typval_T *argvars, typval_T *rettv) { - rettv->v_type = VAR_NUMBER; - rettv->vval.v_number = 0; - - if (check_restricted() || check_secure()) { - return; - } - if (argvars[0].v_type != VAR_NUMBER) { // Wrong argument types EMSG(_(e_invarg)); return; } - rettv->vval.v_number = channel_close(argvars[0].vval.v_number); + // if called with a job, stop it, else closes the channel + if (pmap_get(uint64_t)(jobs, argvars[0].vval.v_number)) { + f_jobstop(argvars, rettv); + } else { + rettv->vval.v_number = channel_close(argvars[0].vval.v_number); + } } /* @@ -16677,7 +16705,7 @@ static void f_termopen(typval_T *argvars, typval_T *rettv) } TerminalJobData *data = common_job_init(argv, on_stdout, on_stderr, on_exit, - job_opts, true, false, cwd); + job_opts, true, false, false, cwd); data->proc.pty.width = curwin->w_width; data->proc.pty.height = curwin->w_height; data->proc.pty.term_name = xstrdup("xterm-256color"); @@ -22101,6 +22129,7 @@ static inline TerminalJobData *common_job_init(char **argv, ufunc_T *on_exit, dict_T *self, bool pty, + bool rpc, bool detach, char *cwd) { @@ -22111,6 +22140,7 @@ static inline TerminalJobData *common_job_init(char **argv, data->on_exit = on_exit; data->self = self; data->events = queue_new_child(main_loop.events); + data->rpc = rpc; if (pty) { data->proc.pty = pty_process_init(&main_loop, data); } else { @@ -22130,7 +22160,9 @@ static inline TerminalJobData *common_job_init(char **argv, return data; } -/// Return true/false on success/failure. +/// common code for getting job callbacks for jobstart, termopen and rpcstart +/// +/// @return true/false on success/failure. static inline bool common_job_callbacks(dict_T *vopts, ufunc_T **on_stdout, ufunc_T **on_stderr, ufunc_T **on_exit) { @@ -22174,15 +22206,22 @@ static inline bool common_job_start(TerminalJobData *data, typval_T *rettv) } xfree(cmd); - data->id = current_job_id++; - wstream_init(proc->in, 0); - if (proc->out) { - rstream_init(proc->out, 0); - rstream_start(proc->out, on_job_stdout); + data->id = next_chan_id++; + + if (data->rpc) { + // the rpc channel takes over the in and out streams + channel_from_process(proc, data->id); + } else { + wstream_init(proc->in, 0); + if (proc->out) { + rstream_init(proc->out, 0); + rstream_start(proc->out, on_job_stdout, data); + } } + if (proc->err) { rstream_init(proc->err, 0); - rstream_start(proc->err, on_job_stderr); + rstream_start(proc->err, on_job_stderr, data); } pmap_put(uint64_t)(jobs, data->id, data); rettv->vval.v_number = data->id; @@ -22302,12 +22341,18 @@ static void on_process_exit(Process *proc, int status, void *d) snprintf(msg, sizeof msg, "\r\n[Process exited %d]", proc->status); terminal_close(data->term, msg); } + if (data->rpc) { + channel_process_exit(data->id, status); + } if (data->status_ptr) { *data->status_ptr = status; } process_job_event(data, data->on_exit, "exit", NULL, 0, status); + + pmap_del(uint64_t)(jobs, data->id); + term_job_data_decref(data); } static void term_write(char *buf, size_t size, void *d) @@ -22355,7 +22400,7 @@ static void term_job_data_decref(TerminalJobData *data) static void on_job_event(JobEvent *ev) { if (!ev->callback) { - goto end; + return; } typval_T argv[3]; @@ -22391,13 +22436,6 @@ static void on_job_event(JobEvent *ev) call_user_func(ev->callback, argc, argv, &rettv, curwin->w_cursor.lnum, curwin->w_cursor.lnum, ev->data->self); clear_tv(&rettv); - -end: - if (!ev->received) { - // exit event, safe to free job data now - pmap_del(uint64_t)(jobs, ev->data->id); - term_job_data_decref(ev->data); - } } static TerminalJobData *find_job(uint64_t id) diff --git a/src/nvim/event/process.c b/src/nvim/event/process.c index 317e40e43a..f507e3d71d 100644 --- a/src/nvim/event/process.c +++ b/src/nvim/event/process.c @@ -25,7 +25,7 @@ #define CLOSE_PROC_STREAM(proc, stream) \ do { \ if (proc->stream && !proc->stream->closed) { \ - stream_close(proc->stream, NULL); \ + stream_close(proc->stream, NULL, NULL); \ } \ } while (0) @@ -78,10 +78,8 @@ bool process_spawn(Process *proc) FUNC_ATTR_NONNULL_ALL return false; } - void *data = proc->data; - if (proc->in) { - stream_init(NULL, proc->in, -1, (uv_stream_t *)&proc->in->uv.pipe, data); + stream_init(NULL, proc->in, -1, (uv_stream_t *)&proc->in->uv.pipe); proc->in->events = proc->events; proc->in->internal_data = proc; proc->in->internal_close_cb = on_process_stream_close; @@ -89,7 +87,7 @@ bool process_spawn(Process *proc) FUNC_ATTR_NONNULL_ALL } if (proc->out) { - stream_init(NULL, proc->out, -1, (uv_stream_t *)&proc->out->uv.pipe, data); + stream_init(NULL, proc->out, -1, (uv_stream_t *)&proc->out->uv.pipe); proc->out->events = proc->events; proc->out->internal_data = proc; proc->out->internal_close_cb = on_process_stream_close; @@ -97,7 +95,7 @@ bool process_spawn(Process *proc) FUNC_ATTR_NONNULL_ALL } if (proc->err) { - stream_init(NULL, proc->err, -1, (uv_stream_t *)&proc->err->uv.pipe, data); + stream_init(NULL, proc->err, -1, (uv_stream_t *)&proc->err->uv.pipe); proc->err->events = proc->events; proc->err->internal_data = proc; proc->err->internal_close_cb = on_process_stream_close; @@ -373,7 +371,7 @@ static void flush_stream(Process *proc, Stream *stream) if (stream->read_cb) { // Stream callback could miss EOF handling if a child keeps the stream // open. - stream->read_cb(stream, stream->buffer, 0, stream->data, true); + stream->read_cb(stream, stream->buffer, 0, stream->cb_data, true); } break; } diff --git a/src/nvim/event/rstream.c b/src/nvim/event/rstream.c index a520143064..5126dfd84e 100644 --- a/src/nvim/event/rstream.c +++ b/src/nvim/event/rstream.c @@ -17,21 +17,19 @@ # include "event/rstream.c.generated.h" #endif -void rstream_init_fd(Loop *loop, Stream *stream, int fd, size_t bufsize, - void *data) +void rstream_init_fd(Loop *loop, Stream *stream, int fd, size_t bufsize) FUNC_ATTR_NONNULL_ARG(1) FUNC_ATTR_NONNULL_ARG(2) { - stream_init(loop, stream, fd, NULL, data); + stream_init(loop, stream, fd, NULL); rstream_init(stream, bufsize); } -void rstream_init_stream(Stream *stream, uv_stream_t *uvstream, size_t bufsize, - void *data) +void rstream_init_stream(Stream *stream, uv_stream_t *uvstream, size_t bufsize) FUNC_ATTR_NONNULL_ARG(1) FUNC_ATTR_NONNULL_ARG(2) { - stream_init(NULL, stream, -1, uvstream, data); + stream_init(NULL, stream, -1, uvstream); rstream_init(stream, bufsize); } @@ -48,10 +46,11 @@ void rstream_init(Stream *stream, size_t bufsize) /// Starts watching for events from a `Stream` instance. /// /// @param stream The `Stream` instance -void rstream_start(Stream *stream, stream_read_cb cb) +void rstream_start(Stream *stream, stream_read_cb cb, void *data) FUNC_ATTR_NONNULL_ARG(1) { stream->read_cb = cb; + stream->cb_data = data; if (stream->uvstream) { uv_read_start(stream->uvstream, alloc_cb, read_cb); } else { @@ -81,7 +80,7 @@ static void on_rbuffer_nonfull(RBuffer *buf, void *data) { Stream *stream = data; assert(stream->read_cb); - rstream_start(stream, stream->read_cb); + rstream_start(stream, stream->read_cb, stream->cb_data); } // Callbacks used by libuv @@ -179,7 +178,7 @@ static void read_event(void **argv) if (stream->read_cb) { size_t count = (uintptr_t)argv[1]; bool eof = (uintptr_t)argv[2]; - stream->read_cb(stream, stream->buffer, count, stream->data, eof); + stream->read_cb(stream, stream->buffer, count, stream->cb_data, eof); } stream->pending_reqs--; if (stream->closed && !stream->pending_reqs) { diff --git a/src/nvim/event/socket.c b/src/nvim/event/socket.c index cdaf40849b..8f9327f3d4 100644 --- a/src/nvim/event/socket.c +++ b/src/nvim/event/socket.c @@ -113,7 +113,7 @@ int socket_watcher_start(SocketWatcher *watcher, int backlog, socket_cb cb) return 0; } -int socket_watcher_accept(SocketWatcher *watcher, Stream *stream, void *data) +int socket_watcher_accept(SocketWatcher *watcher, Stream *stream) FUNC_ATTR_NONNULL_ARG(1) FUNC_ATTR_NONNULL_ARG(2) { uv_stream_t *client; @@ -133,7 +133,7 @@ int socket_watcher_accept(SocketWatcher *watcher, Stream *stream, void *data) return result; } - stream_init(NULL, stream, -1, client, data); + stream_init(NULL, stream, -1, client); return 0; } diff --git a/src/nvim/event/stream.c b/src/nvim/event/stream.c index 33404158cf..26083c20f4 100644 --- a/src/nvim/event/stream.c +++ b/src/nvim/event/stream.c @@ -30,8 +30,7 @@ int stream_set_blocking(int fd, bool blocking) return retval; } -void stream_init(Loop *loop, Stream *stream, int fd, uv_stream_t *uvstream, - void *data) +void stream_init(Loop *loop, Stream *stream, int fd, uv_stream_t *uvstream) FUNC_ATTR_NONNULL_ARG(2) { stream->uvstream = uvstream; @@ -58,7 +57,6 @@ void stream_init(Loop *loop, Stream *stream, int fd, uv_stream_t *uvstream, stream->uvstream->data = stream; } - stream->data = data; stream->internal_data = NULL; stream->fpos = 0; stream->curmem = 0; @@ -74,12 +72,13 @@ void stream_init(Loop *loop, Stream *stream, int fd, uv_stream_t *uvstream, stream->num_bytes = 0; } -void stream_close(Stream *stream, stream_close_cb on_stream_close) +void stream_close(Stream *stream, stream_close_cb on_stream_close, void *data) FUNC_ATTR_NONNULL_ARG(1) { assert(!stream->closed); stream->closed = true; stream->close_cb = on_stream_close; + stream->close_cb_data = data; if (!stream->pending_reqs) { stream_close_handle(stream); @@ -103,7 +102,7 @@ static void close_cb(uv_handle_t *handle) rbuffer_free(stream->buffer); } if (stream->close_cb) { - stream->close_cb(stream, stream->data); + stream->close_cb(stream, stream->close_cb_data); } if (stream->internal_close_cb) { stream->internal_close_cb(stream, stream->internal_data); diff --git a/src/nvim/event/stream.h b/src/nvim/event/stream.h index ad4e24775b..a176fac1c0 100644 --- a/src/nvim/event/stream.h +++ b/src/nvim/event/stream.h @@ -44,13 +44,14 @@ struct stream { uv_file fd; stream_read_cb read_cb; stream_write_cb write_cb; + void *cb_data; stream_close_cb close_cb, internal_close_cb; + void *close_cb_data, *internal_data; size_t fpos; size_t curmem; size_t maxmem; size_t pending_reqs; size_t num_bytes; - void *data, *internal_data; bool closed; Queue *events; }; diff --git a/src/nvim/event/wstream.c b/src/nvim/event/wstream.c index 8028e35e6b..fc7aad8eb9 100644 --- a/src/nvim/event/wstream.c +++ b/src/nvim/event/wstream.c @@ -22,19 +22,17 @@ typedef struct { # include "event/wstream.c.generated.h" #endif -void wstream_init_fd(Loop *loop, Stream *stream, int fd, size_t maxmem, - void *data) +void wstream_init_fd(Loop *loop, Stream *stream, int fd, size_t maxmem) FUNC_ATTR_NONNULL_ARG(1) FUNC_ATTR_NONNULL_ARG(2) { - stream_init(loop, stream, fd, NULL, data); + stream_init(loop, stream, fd, NULL); wstream_init(stream, maxmem); } -void wstream_init_stream(Stream *stream, uv_stream_t *uvstream, size_t maxmem, - void *data) +void wstream_init_stream(Stream *stream, uv_stream_t *uvstream, size_t maxmem) FUNC_ATTR_NONNULL_ARG(1) FUNC_ATTR_NONNULL_ARG(2) { - stream_init(NULL, stream, -1, uvstream, data); + stream_init(NULL, stream, -1, uvstream); wstream_init(stream, maxmem); } @@ -54,10 +52,11 @@ void wstream_init(Stream *stream, size_t maxmem) /// /// @param stream The `Stream` instance /// @param cb The callback -void wstream_set_write_cb(Stream *stream, stream_write_cb cb) - FUNC_ATTR_NONNULL_ALL +void wstream_set_write_cb(Stream *stream, stream_write_cb cb, void *data) + FUNC_ATTR_NONNULL_ARG(1, 2) { stream->write_cb = cb; + stream->cb_data = data; } /// Queues data for writing to the backing file descriptor of a `Stream` @@ -138,7 +137,7 @@ static void write_cb(uv_write_t *req, int status) wstream_release_wbuffer(data->buffer); if (data->stream->write_cb) { - data->stream->write_cb(data->stream, data->stream->data, status); + data->stream->write_cb(data->stream, data->stream->cb_data, status); } data->stream->pending_reqs--; diff --git a/src/nvim/ex_cmds.c b/src/nvim/ex_cmds.c index 5de9ac0523..b7691997d7 100644 --- a/src/nvim/ex_cmds.c +++ b/src/nvim/ex_cmds.c @@ -64,6 +64,24 @@ */ typedef struct sign sign_T; +/// Case matching style to use for :substitute +typedef enum { + kSubHonorOptions = 0, ///< Honor the user's 'ignorecase'/'smartcase' options + kSubIgnoreCase, ///< Ignore case of the search + kSubMatchCase, ///< Match case of the search +} SubIgnoreType; + +/// Flags kept between calls to :substitute. +typedef struct { + bool do_all; ///< do multiple substitutions per line + bool do_ask; ///< ask for confirmation + bool do_count; ///< count only + bool do_error; ///< if false, ignore errors + bool do_print; ///< print last line with subs + bool do_list; ///< list last line with subs + bool do_number; ///< list last line with line nr + SubIgnoreType do_ic; ///< ignore case flag +} subflags_T; #ifdef INCLUDE_GENERATED_DECLARATIONS # include "ex_cmds.c.generated.h" @@ -2901,6 +2919,159 @@ void sub_set_replacement(SubReplacementString sub) old_sub = sub; } +/// Recognize ":%s/\n//" and turn it into a join command, which is much +/// more efficient. +/// +/// @param[in] eap Ex arguments +/// @param[in] pat Search pattern +/// @param[in] sub Replacement string +/// @param[in] cmd Command from :s_flags +/// +/// @returns true if :substitute can be replaced with a join command +static bool sub_joining_lines(exarg_T *eap, char_u *pat, + char_u *sub, char_u *cmd) + FUNC_ATTR_NONNULL_ARG(1, 3, 4) +{ + // TODO(vim): find a generic solution to make line-joining operations more + // efficient, avoid allocating a string that grows in size. + if (pat != NULL + && strcmp((const char *)pat, "\\n") == 0 + && *sub == NUL + && (*cmd == NUL || (cmd[1] == NUL + && (*cmd == 'g' + || *cmd == 'l' + || *cmd == 'p' + || *cmd == '#')))) { + curwin->w_cursor.lnum = eap->line1; + if (*cmd == 'l') { + eap->flags = EXFLAG_LIST; + } else if (*cmd == '#') { + eap->flags = EXFLAG_NR; + } else if (*cmd == 'p') { + eap->flags = EXFLAG_PRINT; + } + + // The number of lines joined is the number of lines in the range + linenr_T joined_lines_count = eap->line2 - eap->line1 + 1 + // plus one extra line if not at the end of file. + + (eap->line2 < curbuf->b_ml.ml_line_count ? 1 : 0); + if (joined_lines_count > 1) { + do_join(joined_lines_count, FALSE, TRUE, FALSE, true); + sub_nsubs = joined_lines_count - 1; + sub_nlines = 1; + do_sub_msg(false); + ex_may_print(eap); + } + + if (!cmdmod.keeppatterns) { + save_re_pat(RE_SUBST, pat, p_magic); + } + add_to_history(HIST_SEARCH, pat, TRUE, NUL); + + return true; + } + + return false; +} + +/// Allocate memory to store the replacement text for :substitute. +/// +/// Slightly more memory that is strictly necessary is allocated to reduce the +/// frequency of memory (re)allocation. +/// +/// @param[in,out] new_start pointer to the memory for the replacement text +/// @param[in] needed_len amount of memory needed +/// +/// @returns pointer to the end of the allocated memory +static char_u *sub_grow_buf(char_u **new_start, int needed_len) + FUNC_ATTR_NONNULL_ARG(1) FUNC_ATTR_NONNULL_RET +{ + int new_start_len = 0; + char_u *new_end; + if (*new_start == NULL) { + // Get some space for a temporary buffer to do the + // substitution into (and some extra space to avoid + // too many calls to xmalloc()/free()). + new_start_len = needed_len + 50; + *new_start = xmalloc(new_start_len); + **new_start = NUL; + new_end = *new_start; + } else { + // Check if the temporary buffer is long enough to do the + // substitution into. If not, make it larger (with a bit + // extra to avoid too many calls to xmalloc()/free()). + size_t len = STRLEN(*new_start); + needed_len += len; + if (needed_len > new_start_len) { + new_start_len = needed_len + 50; + *new_start = xrealloc(*new_start, new_start_len); + } + new_end = *new_start + len; + } + + return new_end; +} + +/// Parse cmd string for :substitute's {flags} and update subflags accordingly +/// +/// @param[in] cmd command string +/// @param[in,out] subflags current flags defined for the :substitute command +/// @param[in,out] which_pat pattern type from which to get default search +/// +/// @returns pointer to the end of the flags, which may be the end of the string +static char_u *sub_parse_flags(char_u *cmd, subflags_T *subflags, + int *which_pat) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_NONNULL_RET +{ + // Find trailing options. When '&' is used, keep old options. + if (*cmd == '&') { + cmd++; + } else { + subflags->do_all = p_gd; + subflags->do_ask = false; + subflags->do_error = true; + subflags->do_print = false; + subflags->do_count = false; + subflags->do_number = false; + subflags->do_ic = kSubHonorOptions; + } + while (*cmd) { + // Note that 'g' and 'c' are always inverted. + // 'r' is never inverted. + if (*cmd == 'g') { + subflags->do_all = !subflags->do_all; + } else if (*cmd == 'c') { + subflags->do_ask = !subflags->do_ask; + } else if (*cmd == 'n') { + subflags->do_count = true; + } else if (*cmd == 'e') { + subflags->do_error = !subflags->do_error; + } else if (*cmd == 'r') { // use last used regexp + *which_pat = RE_LAST; + } else if (*cmd == 'p') { + subflags->do_print = true; + } else if (*cmd == '#') { + subflags->do_print = true; + subflags->do_number = true; + } else if (*cmd == 'l') { + subflags->do_print = true; + subflags->do_list = true; + } else if (*cmd == 'i') { // ignore case + subflags->do_ic = kSubIgnoreCase; + } else if (*cmd == 'I') { // don't ignore case + subflags->do_ic = kSubMatchCase; + } else { + break; + } + cmd++; + } + if (subflags->do_count) { + subflags->do_ask = false; + } + + return cmd; +} + /* do_sub() * * Perform a substitution from line eap->line1 to line eap->line2 using the @@ -2912,41 +3083,35 @@ void sub_set_replacement(SubReplacementString sub) */ void do_sub(exarg_T *eap) { - linenr_T lnum; long i = 0; regmmatch_T regmatch; - static int do_all = FALSE; /* do multiple substitutions per line */ - static int do_ask = FALSE; /* ask for confirmation */ - static bool do_count = false; /* count only */ - static int do_error = TRUE; /* if false, ignore errors */ - static int do_print = FALSE; /* print last line with subs. */ - static int do_list = FALSE; /* list last line with subs. */ - static int do_number = FALSE; /* list last line with line nr*/ - static int do_ic = 0; /* ignore case flag */ - int save_do_all; // remember user specified 'g' flag - int save_do_ask; // remember user specified 'c' flag - char_u *pat = NULL, *sub = NULL; /* init for GCC */ + static subflags_T subflags = { + .do_all = false, + .do_ask = false, + .do_count = false, + .do_error = true, + .do_print = false, + .do_list = false, + .do_number = false, + .do_ic = kSubHonorOptions + }; + char_u *pat = NULL, *sub = NULL; // init for GCC int delimiter; int sublen; - int got_quit = FALSE; - int got_match = FALSE; - int temp; + int got_quit = false; + int got_match = false; int which_pat; - char_u *cmd; - int save_State; - linenr_T first_line = 0; /* first changed line */ - linenr_T last_line= 0; /* below last changed line AFTER the - * change */ + char_u *cmd = eap->arg; + linenr_T first_line = 0; // first changed line + linenr_T last_line= 0; // below last changed line AFTER the + // change linenr_T old_line_count = curbuf->b_ml.ml_line_count; - linenr_T line2; - long nmatch; /* number of lines in match */ - char_u *sub_firstline; /* allocated copy of first sub line */ - int endcolumn = FALSE; /* cursor in last column when done */ + char_u *sub_firstline; // allocated copy of first sub line + bool endcolumn = false; // cursor in last column when done pos_T old_cursor = curwin->w_cursor; int start_nsubs; int save_ma = 0; - cmd = eap->arg; if (!global_busy) { sub_nsubs = 0; sub_nlines = 0; @@ -3028,104 +3193,20 @@ void do_sub(exarg_T *eap) endcolumn = (curwin->w_curswant == MAXCOL); } - // Recognize ":%s/\n//" and turn it into a join command, which is much - // more efficient. - // TODO: find a generic solution to make line-joining operations more - // efficient, avoid allocating a string that grows in size. - if (pat != NULL - && strcmp((const char *)pat, "\\n") == 0 - && *sub == NUL - && (*cmd == NUL || (cmd[1] == NUL - && (*cmd == 'g' - || *cmd == 'l' - || *cmd == 'p' - || *cmd == '#')))) { - curwin->w_cursor.lnum = eap->line1; - if (*cmd == 'l') { - eap->flags = EXFLAG_LIST; - } else if (*cmd == '#') { - eap->flags = EXFLAG_NR; - } else if (*cmd == 'p') { - eap->flags = EXFLAG_PRINT; - } - - // The number of lines joined is the number of lines in the range - linenr_T joined_lines_count = eap->line2 - eap->line1 + 1 - // plus one extra line if not at the end of file. - + (eap->line2 < curbuf->b_ml.ml_line_count ? 1 : 0); - if (joined_lines_count > 1) { - do_join(joined_lines_count, FALSE, TRUE, FALSE, true); - sub_nsubs = joined_lines_count - 1; - sub_nlines = 1; - do_sub_msg(false); - ex_may_print(eap); - } - - if (!cmdmod.keeppatterns) { - save_re_pat(RE_SUBST, pat, p_magic); - } - add_to_history(HIST_SEARCH, pat, TRUE, NUL); - + if (sub_joining_lines(eap, pat, sub, cmd)) { return; } - /* - * Find trailing options. When '&' is used, keep old options. - */ - if (*cmd == '&') { - ++cmd; - } else { - // default is global on - do_all = p_gd ? TRUE : FALSE; - - do_ask = FALSE; - do_error = TRUE; - do_print = FALSE; - do_count = false; - do_number = FALSE; - do_ic = 0; - } - while (*cmd) { - // Note that 'g' and 'c' are always inverted. - // 'r' is never inverted. - if (*cmd == 'g') - do_all = !do_all; - else if (*cmd == 'c') - do_ask = !do_ask; - else if (*cmd == 'n') - do_count = true; - else if (*cmd == 'e') - do_error = !do_error; - else if (*cmd == 'r') /* use last used regexp */ - which_pat = RE_LAST; - else if (*cmd == 'p') - do_print = TRUE; - else if (*cmd == '#') { - do_print = TRUE; - do_number = TRUE; - } else if (*cmd == 'l') { - do_print = TRUE; - do_list = TRUE; - } else if (*cmd == 'i') /* ignore case */ - do_ic = 'i'; - else if (*cmd == 'I') /* don't ignore case */ - do_ic = 'I'; - else - break; - ++cmd; - } - if (do_count) { - do_ask = FALSE; - } + cmd = sub_parse_flags(cmd, &subflags, &which_pat); - save_do_all = do_all; - save_do_ask = do_ask; + bool save_do_all = subflags.do_all; // remember user specified 'g' flag + bool save_do_ask = subflags.do_ask; // remember user specified 'c' flag // check for a trailing count cmd = skipwhite(cmd); if (ascii_isdigit(*cmd)) { i = getdigits_long(&cmd); - if (i <= 0 && !eap->skip && do_error) { + if (i <= 0 && !eap->skip && subflags.do_error) { EMSG(_(e_zerocount)); return; } @@ -3150,24 +3231,25 @@ void do_sub(exarg_T *eap) if (eap->skip) /* not executing commands, only parsing */ return; - if (!do_count && !MODIFIABLE(curbuf)) { - /* Substitution is not allowed in non-'modifiable' buffer */ + if (!subflags.do_count && !MODIFIABLE(curbuf)) { + // Substitution is not allowed in non-'modifiable' buffer EMSG(_(e_modifiable)); return; } - if (search_regcomp(pat, RE_SUBST, which_pat, SEARCH_HIS, - ®match) == FAIL) { - if (do_error) + if (search_regcomp(pat, RE_SUBST, which_pat, SEARCH_HIS, ®match) == FAIL) { + if (subflags.do_error) { EMSG(_(e_invcmd)); + } return; } - /* the 'i' or 'I' flag overrules 'ignorecase' and 'smartcase' */ - if (do_ic == 'i') - regmatch.rmm_ic = TRUE; - else if (do_ic == 'I') - regmatch.rmm_ic = FALSE; + // the 'i' or 'I' flag overrules 'ignorecase' and 'smartcase' + if (subflags.do_ic == kSubIgnoreCase) { + regmatch.rmm_ic = true; + } else if (subflags.do_ic == kSubMatchCase) { + regmatch.rmm_ic = false; + } sub_firstline = NULL; @@ -3179,29 +3261,25 @@ void do_sub(exarg_T *eap) if (!(sub[0] == '\\' && sub[1] == '=')) sub = regtilde(sub, p_magic); - /* - * Check for a match on each line. - */ - line2 = eap->line2; - for (lnum = eap->line1; lnum <= line2 && !(got_quit - || aborting() - ); ++lnum) { - nmatch = vim_regexec_multi(®match, curwin, curbuf, lnum, - (colnr_T)0, NULL); + // Check for a match on each line. + linenr_T line2 = eap->line2; + for (linenr_T lnum = eap->line1; + lnum <= line2 && !(got_quit || aborting()); + lnum++) { + long nmatch = vim_regexec_multi(®match, curwin, curbuf, lnum, + (colnr_T)0, NULL); if (nmatch) { colnr_T copycol; colnr_T matchcol; colnr_T prev_matchcol = MAXCOL; char_u *new_end, *new_start = NULL; - unsigned new_start_len = 0; char_u *p1; int did_sub = FALSE; int lastone; - int len, copy_len, needed_len; - long nmatch_tl = 0; /* nr of lines matched below lnum */ - int do_again; /* do it again after joining lines */ - int skip_match = FALSE; - linenr_T sub_firstlnum; /* nr of first sub line */ + long nmatch_tl = 0; // nr of lines matched below lnum + int do_again; // do it again after joining lines + int skip_match = false; + linenr_T sub_firstlnum; // nr of first sub line /* * The new text is build up step by step, to avoid too much @@ -3241,8 +3319,7 @@ void do_sub(exarg_T *eap) * accordingly. * * The new text is built up in new_start[]. It has some extra - * room to avoid using xmalloc()/free() too often. new_start_len is - * the length of the allocated memory at new_start. + * room to avoid using xmalloc()/free() too often. * * Make a copy of the old line, so it won't be taken away when * updating the screen or handling a multi-line match. The "old_" @@ -3261,9 +3338,9 @@ void do_sub(exarg_T *eap) /* * Loop until nothing more to replace in this line. * 1. Handle match with empty string. - * 2. If do_ask is set, ask for confirmation. + * 2. If subflags.do_ask is set, ask for confirmation. * 3. substitute the string. - * 4. if do_all is set, find next match + * 4. if subflags.do_all is set, find next match * 5. break if there isn't another match in this line */ for (;; ) { @@ -3314,15 +3391,13 @@ void do_sub(exarg_T *eap) matchcol = regmatch.endpos[0].col; prev_matchcol = matchcol; - /* - * 2. If do_count is set only increase the counter. - * If do_ask is set, ask for confirmation. - */ - if (do_count) { - /* For a multi-line match, put matchcol at the NUL at - * the end of the line and set nmatch to one, so that - * we continue looking for a match on the next line. - * Avoids that ":s/\nB\@=//gc" get stuck. */ + // 2. If subflags.do_count is set only increase the counter. + // If do_ask is set, ask for confirmation. + if (subflags.do_count) { + // For a multi-line match, put matchcol at the NUL at + // the end of the line and set nmatch to one, so that + // we continue looking for a match on the next line. + // Avoids that ":s/\nB\@=//gc" get stuck. if (nmatch > 1) { matchcol = (colnr_T)STRLEN(sub_firstline); nmatch = 1; @@ -3336,12 +3411,12 @@ void do_sub(exarg_T *eap) goto skip; } - if (do_ask) { + if (subflags.do_ask) { int typed = 0; /* change State to CONFIRM, so that the mouse works * properly */ - save_State = State; + int save_State = State; State = CONFIRM; setmouse(); /* disable mouse in xterm */ curwin->w_cursor.col = regmatch.startpos[0].col; @@ -3354,17 +3429,17 @@ void do_sub(exarg_T *eap) /* * Loop until 'y', 'n', 'q', CTRL-E or CTRL-Y typed. */ - while (do_ask) { + while (subflags.do_ask) { if (exmode_active) { char_u *resp; colnr_T sc, ec; - print_line_no_prefix(lnum, do_number, do_list); + print_line_no_prefix(lnum, subflags.do_number, subflags.do_list); getvcol(curwin, &curwin->w_cursor, &sc, NULL, NULL); curwin->w_cursor.col = regmatch.endpos[0].col - 1; getvcol(curwin, &curwin->w_cursor, NULL, NULL, &ec); - if (do_number || curwin->w_p_nu) { + if (subflags.do_number || curwin->w_p_nu) { int numw = number_width(curwin) + 1; sc += numw; ec += numw; @@ -3388,7 +3463,7 @@ void do_sub(exarg_T *eap) curwin->w_p_fen = FALSE; /* Invert the matched string. * Remove the inversion afterwards. */ - temp = RedrawingDisabled; + int temp = RedrawingDisabled; RedrawingDisabled = 0; if (new_start != NULL) { @@ -3468,13 +3543,13 @@ void do_sub(exarg_T *eap) if (typed == 'y') break; if (typed == 'l') { - /* last: replace and then stop */ - do_all = FALSE; + // last: replace and then stop + subflags.do_all = false; line2 = lnum; break; } if (typed == 'a') { - do_ask = FALSE; + subflags.do_ask = false; break; } if (typed == Ctrl_E) @@ -3510,19 +3585,26 @@ void do_sub(exarg_T *eap) /* * 3. substitute the string. */ - if (do_count) { - /* prevent accidentally changing the buffer by a function */ + if (subflags.do_count) { + // prevent accidentally changing the buffer by a function save_ma = curbuf->b_p_ma; - curbuf->b_p_ma = FALSE; + curbuf->b_p_ma = false; sandbox++; } - /* get length of substitution part */ + // Save flags for recursion. They can change for e.g. + // :s/^/\=execute("s#^##gn") + subflags_T subflags_save = subflags; + // get length of substitution part sublen = vim_regsub_multi(®match, - sub_firstlnum - regmatch.startpos[0].lnum, - sub, sub_firstline, FALSE, p_magic, TRUE); - if (do_count) { + sub_firstlnum - regmatch.startpos[0].lnum, + sub, sub_firstline, false, p_magic, true); + // Don't keep flags set by a recursive call + subflags = subflags_save; + if (subflags.do_count) { curbuf->b_p_ma = save_ma; - sandbox--; + if (sandbox > 0) { + sandbox--; + } goto skip; } @@ -3545,33 +3627,10 @@ void do_sub(exarg_T *eap) p1 = ml_get(sub_firstlnum + nmatch - 1); nmatch_tl += nmatch - 1; } - copy_len = regmatch.startpos[0].col - copycol; - needed_len = copy_len + ((unsigned)STRLEN(p1) - - regmatch.endpos[0].col) + sublen + 1; - if (new_start == NULL) { - /* - * Get some space for a temporary buffer to do the - * substitution into (and some extra space to avoid - * too many calls to xmalloc()/free()). - */ - new_start_len = needed_len + 50; - new_start = xmalloc(new_start_len); - *new_start = NUL; - new_end = new_start; - } else { - /* - * Check if the temporary buffer is long enough to do the - * substitution into. If not, make it larger (with a bit - * extra to avoid too many calls to xmalloc()/free()). - */ - len = (unsigned)STRLEN(new_start); - needed_len += len; - if (needed_len > (int)new_start_len) { - new_start_len = needed_len + 50; - new_start = xrealloc(new_start, new_start_len); - } - new_end = new_start + len; - } + size_t copy_len = regmatch.startpos[0].col - copycol; + new_end = sub_grow_buf(&new_start, + copy_len + (STRLEN(p1) - regmatch.endpos[0].col) + + sublen + 1); /* * copy the text up to the part that matched @@ -3595,11 +3654,12 @@ void do_sub(exarg_T *eap) sub_firstlnum += nmatch - 1; xfree(sub_firstline); sub_firstline = vim_strsave(ml_get(sub_firstlnum)); - /* When going beyond the last line, stop substituting. */ - if (sub_firstlnum <= line2) - do_again = TRUE; - else - do_all = FALSE; + // When going beyond the last line, stop substituting. + if (sub_firstlnum <= line2) { + do_again = true; + } else { + subflags.do_all = false; + } } /* Remember next character to be copied. */ @@ -3630,11 +3690,12 @@ void do_sub(exarg_T *eap) ml_append(lnum - 1, new_start, (colnr_T)(p1 - new_start + 1), FALSE); mark_adjust(lnum + 1, (linenr_T)MAXLNUM, 1L, 0L); - if (do_ask) + if (subflags.do_ask) { appended_lines(lnum - 1, 1L); - else { - if (first_line == 0) + } else { + if (first_line == 0) { first_line = lnum; + } last_line = lnum + 1; } /* All line numbers increase. */ @@ -3651,12 +3712,10 @@ void do_sub(exarg_T *eap) p1 += (*mb_ptr2len)(p1) - 1; } - /* - * 4. If do_all is set, find next match. - * Prevent endless loop with patterns that match empty - * strings, e.g. :s/$/pat/g or :s/[a-z]* /(&)/g. - * But ":s/\n/#/" is OK. - */ + // 4. If subflags.do_all is set, find next match. + // Prevent endless loop with patterns that match empty + // strings, e.g. :s/$/pat/g or :s/[a-z]* /(&)/g. + // But ":s/\n/#/" is OK. skip: /* We already know that we did the last subst when we are at * the end of the line, except that a pattern like @@ -3667,7 +3726,7 @@ skip: || got_int || got_quit || lnum > line2 - || !(do_all || do_again) + || !(subflags.do_all || do_again) || (sub_firstline[matchcol] == NUL && nmatch <= 1 && !re_multiline(regmatch.regprog))); nmatch = -1; @@ -3717,21 +3776,23 @@ skip: for (i = 0; i < nmatch_tl; ++i) ml_delete(lnum, (int)FALSE); mark_adjust(lnum, lnum + nmatch_tl - 1, - (long)MAXLNUM, -nmatch_tl); - if (do_ask) + (long)MAXLNUM, -nmatch_tl); + if (subflags.do_ask) { deleted_lines(lnum, nmatch_tl); - --lnum; - line2 -= nmatch_tl; /* nr of lines decreases */ + } + lnum--; + line2 -= nmatch_tl; // nr of lines decreases nmatch_tl = 0; } /* When asking, undo is saved each time, must also set * changed flag each time. */ - if (do_ask) + if (subflags.do_ask) { changed_bytes(lnum, 0); - else { - if (first_line == 0) + } else { + if (first_line == 0) { first_line = lnum; + } last_line = lnum + 1; } @@ -3784,9 +3845,10 @@ skip: xfree(sub_firstline); /* may have to free allocated copy of the line */ - /* ":s/pat//n" doesn't move the cursor */ - if (do_count) + // ":s/pat//n" doesn't move the cursor + if (subflags.do_count) { curwin->w_cursor = old_cursor; + } if (sub_nsubs > start_nsubs) { /* Set the '[ and '] marks. */ @@ -3795,28 +3857,37 @@ skip: curbuf->b_op_start.col = curbuf->b_op_end.col = 0; if (!global_busy) { - if (!do_ask) { /* when interactive leave cursor on the match */ - if (endcolumn) + // when interactive leave cursor on the match + if (!subflags.do_ask) { + if (endcolumn) { coladvance((colnr_T)MAXCOL); - else + } else { beginline(BL_WHITE | BL_FIX); + } } - if (!do_sub_msg(do_count) && do_ask) + if (!do_sub_msg(subflags.do_count) && subflags.do_ask) { MSG(""); - } else - global_need_beginline = TRUE; - if (do_print) - print_line(curwin->w_cursor.lnum, do_number, do_list); + } + } else { + global_need_beginline = true; + } + if (subflags.do_print) { + print_line(curwin->w_cursor.lnum, subflags.do_number, subflags.do_list); + } } else if (!global_busy) { - if (got_int) /* interrupted */ + if (got_int) { + // interrupted EMSG(_(e_interr)); - else if (got_match) /* did find something but nothing substituted */ + } else if (got_match) { + // did find something but nothing substituted MSG(""); - else if (do_error) /* nothing found */ + } else if (subflags.do_error) { + // nothing found EMSG2(_(e_patnotf2), get_search_pat()); + } } - if (do_ask && hasAnyFolding(curwin)) { + if (subflags.do_ask && hasAnyFolding(curwin)) { // Cursor position may require updating changed_window_setting(); } @@ -3824,9 +3895,9 @@ skip: vim_regfree(regmatch.regprog); // Restore the flag values, they can be used for ":&&". - do_all = save_do_all; - do_ask = save_do_ask; -} + subflags.do_all = save_do_all; + subflags.do_ask = save_do_ask; +} // NOLINT(readability/fn_size) /* * Give message for number of substitutions. diff --git a/src/nvim/globals.h b/src/nvim/globals.h index ddfb4f62e1..57d12d396e 100644 --- a/src/nvim/globals.h +++ b/src/nvim/globals.h @@ -1245,6 +1245,9 @@ EXTERN char *ignoredp; // If a msgpack-rpc channel should be started over stdin/stdout EXTERN bool embedded_mode INIT(= false); +/// next free id for a job or rpc channel +EXTERN uint64_t next_chan_id INIT(= 1); + /// Used to track the status of external functions. /// Currently only used for iconv(). typedef enum { diff --git a/src/nvim/msgpack_rpc/channel.c b/src/nvim/msgpack_rpc/channel.c index 5b249ee1c7..8b5f212d66 100644 --- a/src/nvim/msgpack_rpc/channel.c +++ b/src/nvim/msgpack_rpc/channel.c @@ -19,6 +19,7 @@ #include "nvim/main.h" #include "nvim/ascii.h" #include "nvim/memory.h" +#include "nvim/eval.h" #include "nvim/os_unix.h" #include "nvim/message.h" #include "nvim/map.h" @@ -55,12 +56,7 @@ typedef struct { msgpack_unpacker *unpacker; union { Stream stream; - struct { - LibuvProcess uvproc; - Stream in; - Stream out; - Stream err; - } process; + Process *proc; struct { Stream in; Stream out; @@ -79,7 +75,6 @@ typedef struct { uint64_t request_id; } RequestEvent; -static uint64_t next_id = 1; static PMap(uint64_t) *channels = NULL; static PMap(cstr_t) *event_strings = NULL; static msgpack_sbuffer out_buffer; @@ -112,33 +107,20 @@ void channel_teardown(void) } /// Creates an API channel by starting a process and connecting to its -/// stdin/stdout. stderr is forwarded to the editor error stream. +/// stdin/stdout. stderr is handled by the job infrastructure. /// /// @param argv The argument vector for the process. [consumed] /// @return The channel id (> 0), on success. /// 0, on error. -uint64_t channel_from_process(char **argv) -{ - Channel *channel = register_channel(kChannelTypeProc); - channel->data.process.uvproc = libuv_process_init(&main_loop, channel); - Process *proc = &channel->data.process.uvproc.process; - proc->argv = argv; - proc->in = &channel->data.process.in; - proc->out = &channel->data.process.out; - proc->err = &channel->data.process.err; - proc->cb = process_exit; - if (!process_spawn(proc)) { - loop_poll_events(&main_loop, 0); - decref(channel); - return 0; - } - +uint64_t channel_from_process(Process *proc, uint64_t id) +{ + Channel *channel = register_channel(kChannelTypeProc, id, proc->events); incref(channel); // process channels are only closed by the exit_cb + channel->data.proc = proc; + wstream_init(proc->in, 0); rstream_init(proc->out, 0); - rstream_start(proc->out, parse_msgpack); - rstream_init(proc->err, 0); - rstream_start(proc->err, forward_stderr); + rstream_start(proc->out, parse_msgpack, channel); return channel->id; } @@ -148,14 +130,14 @@ uint64_t channel_from_process(char **argv) /// @param watcher The SocketWatcher ready to accept the connection void channel_from_connection(SocketWatcher *watcher) { - Channel *channel = register_channel(kChannelTypeSocket); - socket_watcher_accept(watcher, &channel->data.stream, channel); + Channel *channel = register_channel(kChannelTypeSocket, 0, NULL); + socket_watcher_accept(watcher, &channel->data.stream); incref(channel); // close channel only after the stream is closed channel->data.stream.internal_close_cb = close_cb; channel->data.stream.internal_data = channel; wstream_init(&channel->data.stream, 0); rstream_init(&channel->data.stream, CHANNEL_BUFFER_SIZE); - rstream_start(&channel->data.stream, parse_msgpack); + rstream_start(&channel->data.stream, parse_msgpack, channel); } /// Sends event/arguments to channel @@ -314,30 +296,21 @@ bool channel_close(uint64_t id) /// Neovim void channel_from_stdio(void) { - Channel *channel = register_channel(kChannelTypeStdio); + Channel *channel = register_channel(kChannelTypeStdio, 0, NULL); incref(channel); // stdio channels are only closed on exit // read stream - rstream_init_fd(&main_loop, &channel->data.std.in, 0, CHANNEL_BUFFER_SIZE, - channel); - rstream_start(&channel->data.std.in, parse_msgpack); + rstream_init_fd(&main_loop, &channel->data.std.in, 0, CHANNEL_BUFFER_SIZE); + rstream_start(&channel->data.std.in, parse_msgpack, channel); // write stream - wstream_init_fd(&main_loop, &channel->data.std.out, 1, 0, NULL); + wstream_init_fd(&main_loop, &channel->data.std.out, 1, 0); } -static void forward_stderr(Stream *stream, RBuffer *rbuf, size_t count, - void *data, bool eof) +void channel_process_exit(uint64_t id, int status) { - while (rbuffer_size(rbuf)) { - char buf[256]; - size_t read = rbuffer_read(rbuf, buf, sizeof(buf) - 1); - buf[read] = NUL; - ELOG("Channel %" PRIu64 " stderr: %s", ((Channel *)data)->id, buf); - } -} + Channel *channel = pmap_get(uint64_t)(channels, id); -static void process_exit(Process *proc, int status, void *data) -{ - decref(data); + channel->closed = true; + decref(channel); } static void parse_msgpack(Stream *stream, RBuffer *rbuf, size_t c, void *data, @@ -512,7 +485,7 @@ static bool channel_write(Channel *channel, WBuffer *buffer) success = wstream_write(&channel->data.stream, buffer); break; case kChannelTypeProc: - success = wstream_write(&channel->data.process.in, buffer); + success = wstream_write(channel->data.proc->in, buffer); break; case kChannelTypeStdio: success = wstream_write(&channel->data.std.out, buffer); @@ -637,16 +610,17 @@ static void close_channel(Channel *channel) switch (channel->type) { case kChannelTypeSocket: - stream_close(&channel->data.stream, NULL); + stream_close(&channel->data.stream, NULL, NULL); break; case kChannelTypeProc: - if (!channel->data.process.uvproc.process.closed) { - process_stop(&channel->data.process.uvproc.process); - } + // Only close the rpc channel part, + // there could be an error message on the stderr stream + process_close_in(channel->data.proc); + process_close_out(channel->data.proc); break; case kChannelTypeStdio: - stream_close(&channel->data.std.in, NULL); - stream_close(&channel->data.std.out, NULL); + stream_close(&channel->data.std.in, NULL, NULL); + stream_close(&channel->data.std.out, NULL, NULL); queue_put(main_loop.fast_events, exit_event, 1, channel); return; default: @@ -680,7 +654,9 @@ static void free_channel(Channel *channel) pmap_free(cstr_t)(channel->subscribed_events); kv_destroy(channel->call_stack); kv_destroy(channel->delayed_notifications); - queue_free(channel->events); + if (channel->type != kChannelTypeProc) { + queue_free(channel->events); + } xfree(channel); } @@ -689,15 +665,15 @@ static void close_cb(Stream *stream, void *data) decref(data); } -static Channel *register_channel(ChannelType type) +static Channel *register_channel(ChannelType type, uint64_t id, Queue *events) { Channel *rv = xmalloc(sizeof(Channel)); - rv->events = queue_new_child(main_loop.events); + rv->events = events ? events : queue_new_child(main_loop.events); rv->type = type; rv->refcount = 1; rv->closed = false; rv->unpacker = msgpack_unpacker_new(MSGPACK_UNPACKER_INIT_BUFFER_SIZE); - rv->id = next_id++; + rv->id = id > 0 ? id : next_chan_id++; rv->pending_requests = 0; rv->subscribed_events = pmap_new(cstr_t)(); rv->next_request_id = 1; diff --git a/src/nvim/msgpack_rpc/channel.h b/src/nvim/msgpack_rpc/channel.h index 104547a7b8..0d92976d02 100644 --- a/src/nvim/msgpack_rpc/channel.h +++ b/src/nvim/msgpack_rpc/channel.h @@ -6,6 +6,7 @@ #include "nvim/api/private/defs.h" #include "nvim/event/socket.h" +#include "nvim/event/process.h" #include "nvim/vim.h" #define METHOD_MAXLEN 512 diff --git a/src/nvim/os/input.c b/src/nvim/os/input.c index a4e01b18cd..c0c73364c0 100644 --- a/src/nvim/os/input.c +++ b/src/nvim/os/input.c @@ -60,8 +60,8 @@ void input_start(int fd) } global_fd = fd; - rstream_init_fd(&main_loop, &read_stream, fd, READ_BUFFER_SIZE, NULL); - rstream_start(&read_stream, read_cb); + rstream_init_fd(&main_loop, &read_stream, fd, READ_BUFFER_SIZE); + rstream_start(&read_stream, read_cb, NULL); } void input_stop(void) @@ -71,7 +71,7 @@ void input_stop(void) } rstream_stop(&read_stream); - stream_close(&read_stream, NULL); + stream_close(&read_stream, NULL, NULL); } static void cursorhold_event(void **argv) diff --git a/src/nvim/os/shell.c b/src/nvim/os/shell.c index 64c673930a..ba52b9f661 100644 --- a/src/nvim/os/shell.c +++ b/src/nvim/os/shell.c @@ -236,10 +236,10 @@ static int do_os_system(char **argv, } proc->out->events = NULL; rstream_init(proc->out, 0); - rstream_start(proc->out, data_cb); + rstream_start(proc->out, data_cb, &buf); proc->err->events = NULL; rstream_init(proc->err, 0); - rstream_start(proc->err, data_cb); + rstream_start(proc->err, data_cb, &buf); // write the input, if any if (input) { @@ -251,7 +251,7 @@ static int do_os_system(char **argv, return -1; } // close the input stream after everything is written - wstream_set_write_cb(&in, shell_write_cb); + wstream_set_write_cb(&in, shell_write_cb, NULL); } // invoke busy_start here so event_poll_until wont change the busy state for @@ -546,5 +546,5 @@ static size_t write_output(char *output, size_t remaining, bool to_buffer, static void shell_write_cb(Stream *stream, void *data, int status) { - stream_close(stream, NULL); + stream_close(stream, NULL, NULL); } diff --git a/src/nvim/path.c b/src/nvim/path.c index c20dffa9b1..a2617973b1 100644 --- a/src/nvim/path.c +++ b/src/nvim/path.c @@ -2186,9 +2186,16 @@ static int path_get_absolute_path(const char_u *fname, char_u *buf, /// Check if the given file is absolute. /// -/// This just checks if the file name starts with '/' or '~'. /// @return `TRUE` if "fname" is absolute. int path_is_absolute_path(const char_u *fname) { +#ifdef WIN32 + // A name like "d:/foo" and "//server/share" is absolute + return ((isalpha(fname[0]) && fname[1] == ':' + && vim_ispathsep_nocolon(fname[2])) + || (vim_ispathsep_nocolon(fname[0]) && fname[0] == fname[1])); +#else + // UNIX: This just checks if the file name starts with '/' or '~'. return *fname == '/' || *fname == '~'; +#endif } diff --git a/src/nvim/terminal.c b/src/nvim/terminal.c index 6f50c03be9..90636326a5 100644 --- a/src/nvim/terminal.c +++ b/src/nvim/terminal.c @@ -1081,7 +1081,8 @@ static void redraw(bool restore_cursor) restore_cursor = true; } - int save_row, save_col; + int save_row = 0; + int save_col = 0; if (restore_cursor) { // save the current row/col to restore after updating screen when not // focused diff --git a/src/nvim/testdir/test_alot.vim b/src/nvim/testdir/test_alot.vim index 30b8a9ceb8..daf6f026ba 100644 --- a/src/nvim/testdir/test_alot.vim +++ b/src/nvim/testdir/test_alot.vim @@ -6,4 +6,5 @@ source test_cursor_func.vim source test_cmdline.vim source test_menu.vim source test_popup.vim +source test_regexp_utf8.vim source test_unlet.vim diff --git a/src/nvim/testdir/test_regexp_utf8.vim b/src/nvim/testdir/test_regexp_utf8.vim new file mode 100644 index 0000000000..38f9ed41d5 --- /dev/null +++ b/src/nvim/testdir/test_regexp_utf8.vim @@ -0,0 +1,41 @@ +" Tests for regexp in utf8 encoding +scriptencoding utf-8 + +func s:equivalence_test() + let str = "AÀÁÂÃÄÅĀĂĄǍǞǠẢ BḂḆ CÇĆĈĊČ DĎĐḊḎḐ EÈÉÊËĒĔĖĘĚẺẼ FḞ GĜĞĠĢǤǦǴḠ HĤĦḢḦḨ IÌÍÎÏĨĪĬĮİǏỈ JĴ KĶǨḰḴ LĹĻĽĿŁḺ MḾṀ NÑŃŅŇṄṈ OÒÓÔÕÖØŌŎŐƠǑǪǬỎ PṔṖ Q RŔŖŘṘṞ SŚŜŞŠṠ TŢŤŦṪṮ UÙÚÛÜŨŪŬŮŰŲƯǓỦ VṼ WŴẀẂẄẆ XẊẌ YÝŶŸẎỲỶỸ ZŹŻŽƵẐẔ aàáâãäåāăąǎǟǡả bḃḇ cçćĉċč dďđḋḏḑ eèéêëēĕėęěẻẽ fḟ gĝğġģǥǧǵḡ hĥħḣḧḩẖ iìíîïĩīĭįǐỉ jĵǰ kķǩḱḵ lĺļľŀłḻ mḿṁ nñńņňʼnṅṉ oòóôõöøōŏőơǒǫǭỏ pṕṗ q rŕŗřṙṟ sśŝşšṡ tţťŧṫṯẗ uùúûüũūŭůűųưǔủ vṽ wŵẁẃẅẇẘ xẋẍ yýÿŷẏẙỳỷỹ zźżžƶẑẕ" + let groups = split(str) + for group1 in groups + for c in split(group1, '\zs') + " next statement confirms that equivalence class matches every + " character in group + call assert_match('^[[=' . c . '=]]*$', group1) + for group2 in groups + if group2 != group1 + " next statement converts that equivalence class doesn't match + " character in any other group + call assert_equal(-1, match(group2, '[[=' . c . '=]]')) + endif + endfor + endfor + endfor +endfunc + +func Test_equivalence_re1() + set re=1 + call s:equivalence_test() + set re=0 +endfunc + +func Test_equivalence_re2() + set re=2 + call s:equivalence_test() + set re=0 +endfunc + +func Test_recursive_substitute() + new + s/^/\=execute("s#^##gn") + " check we are now not in the sandbox + call setwinvar(1, 'myvar', 1) + bwipe! +endfunc diff --git a/src/nvim/tui/input.c b/src/nvim/tui/input.c index be256f3ebc..3ef4d34c9a 100644 --- a/src/nvim/tui/input.c +++ b/src/nvim/tui/input.c @@ -38,7 +38,7 @@ void term_input_init(TermInput *input, Loop *loop) int curflags = termkey_get_canonflags(input->tk); termkey_set_canonflags(input->tk, curflags | TERMKEY_CANON_DELBS); // setup input handle - rstream_init_fd(loop, &input->read_stream, input->in_fd, 0xfff, input); + rstream_init_fd(loop, &input->read_stream, input->in_fd, 0xfff); // initialize a timer handle for handling ESC with libtermkey time_watcher_init(loop, &input->timer_handle, input); } @@ -49,13 +49,13 @@ void term_input_destroy(TermInput *input) uv_mutex_destroy(&input->key_buffer_mutex); uv_cond_destroy(&input->key_buffer_cond); time_watcher_close(&input->timer_handle, NULL); - stream_close(&input->read_stream, NULL); + stream_close(&input->read_stream, NULL, NULL); termkey_destroy(input->tk); } void term_input_start(TermInput *input) { - rstream_start(&input->read_stream, read_cb); + rstream_start(&input->read_stream, read_cb, input); } void term_input_stop(TermInput *input) @@ -340,7 +340,7 @@ static void read_cb(Stream *stream, RBuffer *buf, size_t c, void *data, // // ls *.md | xargs nvim input->in_fd = 2; - stream_close(&input->read_stream, NULL); + stream_close(&input->read_stream, NULL, NULL); queue_put(input->loop->fast_events, restart_reading, 1, input); } else { loop_schedule(&main_loop, event_create(1, input_done_event, 0)); @@ -391,6 +391,6 @@ static void read_cb(Stream *stream, RBuffer *buf, size_t c, void *data, static void restart_reading(void **argv) { TermInput *input = argv[0]; - rstream_init_fd(input->loop, &input->read_stream, input->in_fd, 0xfff, input); - rstream_start(&input->read_stream, read_cb); + rstream_init_fd(input->loop, &input->read_stream, input->in_fd, 0xfff); + rstream_start(&input->read_stream, read_cb, input); } diff --git a/src/nvim/version.c b/src/nvim/version.c index 1cb8d128aa..978bc2e228 100644 --- a/src/nvim/version.c +++ b/src/nvim/version.c @@ -75,6 +75,7 @@ static char *features[] = { // clang-format off static int included_patches[] = { + 2219, // 2200, // 2199, // 2198, @@ -577,7 +578,7 @@ static int included_patches[] = { 1703, // 1702, // 1701, - // 1700, + 1700, // 1699, // 1698 NA // 1697, diff --git a/test/functional/api/rpc_fixture.lua b/test/functional/api/rpc_fixture.lua new file mode 100644 index 0000000000..423864740f --- /dev/null +++ b/test/functional/api/rpc_fixture.lua @@ -0,0 +1,38 @@ +local deps_prefix = './.deps/usr' +if os.getenv('DEPS_PREFIX') then + deps_prefix = os.getenv('DEPS_PREFIX') +end + +package.path = deps_prefix .. '/share/lua/5.1/?.lua;' .. + deps_prefix .. '/share/lua/5.1/?/init.lua;' .. + package.path + +package.cpath = deps_prefix .. '/lib/lua/5.1/?.so;' .. + package.cpath + +local mpack = require('mpack') +local StdioStream = require('nvim.stdio_stream') +local Session = require('nvim.session') + +local stdio_stream = StdioStream.open() +local session = Session.new(stdio_stream) + +local function on_request(method, args) + if method == 'poll' then + return 'ok' + elseif method == 'write_stderr' then + io.stderr:write(args[1]) + return "done!" + elseif method == "exit" then + session:stop() + return mpack.NIL + end +end + +local function on_notification(event, args) + if event == 'ping' and #args == 0 then + session:notify("vim_eval", "rpcnotify(g:channel, 'pong')") + end +end + +session:run(on_request, on_notification) diff --git a/test/functional/api/server_requests_spec.lua b/test/functional/api/server_requests_spec.lua index eb63834cb0..b76c3b9cd6 100644 --- a/test/functional/api/server_requests_spec.lua +++ b/test/functional/api/server_requests_spec.lua @@ -4,7 +4,9 @@ local helpers = require('test.functional.helpers')(after_each) local clear, nvim, eval = helpers.clear, helpers.nvim, helpers.eval local eq, neq, run, stop = helpers.eq, helpers.neq, helpers.run, helpers.stop -local nvim_prog = helpers.nvim_prog +local nvim_prog, command, funcs = helpers.nvim_prog, helpers.command, helpers.funcs +local source, next_message = helpers.source, helpers.next_message +local meths = helpers.meths describe('server -> client', function() @@ -144,11 +146,11 @@ describe('server -> client', function() end before_each(function() - nvim('command', "let vim = rpcstart('"..nvim_prog.."', ['-u', 'NONE', '-i', 'NONE', '--cmd', 'set noswapfile', '--embed'])") + command("let vim = rpcstart('"..nvim_prog.."', ['-u', 'NONE', '-i', 'NONE', '--cmd', 'set noswapfile', '--embed'])") neq(0, eval('vim')) end) - after_each(function() nvim('command', 'call rpcstop(vim)') end) + after_each(function() command('call rpcstop(vim)') end) it('can send/recieve notifications and make requests', function() nvim('command', "call rpcnotify(vim, 'vim_set_current_line', 'SOME TEXT')") @@ -181,4 +183,42 @@ describe('server -> client', function() eq(true, string.match(err, ': (.*)') == 'Failed to evaluate expression') end) end) + + describe('when using jobstart', function() + local jobid + before_each(function() + local channel = nvim('get_api_info')[1] + nvim('set_var', 'channel', channel) + source([[ + function! s:OnEvent(id, data, event) + call rpcnotify(g:channel, a:event, 0, a:data) + endfunction + let g:job_opts = { + \ 'on_stderr': function('s:OnEvent'), + \ 'on_exit': function('s:OnEvent'), + \ 'user': 0, + \ 'rpc': v:true + \ } + ]]) + local lua_prog = arg[-1] + meths.set_var("args", {lua_prog, 'test/functional/api/rpc_fixture.lua'}) + jobid = eval("jobstart(g:args, g:job_opts)") + neq(0, 'jobid') + end) + + after_each(function() + funcs.jobstop(jobid) + end) + + it('rpc and text stderr can be combined', function() + eq("ok",funcs.rpcrequest(jobid, "poll")) + funcs.rpcnotify(jobid, "ping") + eq({'notification', 'pong', {}}, next_message()) + eq("done!",funcs.rpcrequest(jobid, "write_stderr", "fluff\n")) + eq({'notification', 'stderr', {0, {'fluff', ''}}}, next_message()) + funcs.rpcrequest(jobid, "exit") + eq({'notification', 'exit', {0, 0}}, next_message()) + end) + end) + end) diff --git a/test/functional/core/job_spec.lua b/test/functional/core/job_spec.lua index 1d11374e4d..921bf1655e 100644 --- a/test/functional/core/job_spec.lua +++ b/test/functional/core/job_spec.lua @@ -5,6 +5,7 @@ local clear, eq, eval, execute, feed, insert, neq, next_msg, nvim, helpers.insert, helpers.neq, helpers.next_message, helpers.nvim, helpers.nvim_dir, helpers.ok, helpers.source, helpers.write_file, helpers.mkdir, helpers.rmdir +local command = helpers.command local Screen = require('test.functional.ui.screen') @@ -429,6 +430,13 @@ describe('jobs', function() eq({'notification', 'j', {0, {jobid, 'exit'}}}, next_msg()) end) + it('cannot have both rpc and pty options', function() + command("let g:job_opts.pty = v:true") + command("let g:job_opts.rpc = v:true") + local _, err = pcall(command, "let j = jobstart(['cat', '-'], g:job_opts)") + ok(string.find(err, "E475: Invalid argument: job cannot have both 'pty' and 'rpc' options set") ~= nil) + end) + describe('running tty-test program', function() local function next_chunk() local rv diff --git a/test/functional/fixtures/autoload/health/broken.vim b/test/functional/fixtures/autoload/health/broken.vim new file mode 100644 index 0000000000..a2a595b96f --- /dev/null +++ b/test/functional/fixtures/autoload/health/broken.vim @@ -0,0 +1,3 @@ +function! health#broken#check() + throw 'caused an error' +endfunction diff --git a/test/functional/fixtures/autoload/health/success1.vim b/test/functional/fixtures/autoload/health/success1.vim new file mode 100644 index 0000000000..a360347455 --- /dev/null +++ b/test/functional/fixtures/autoload/health/success1.vim @@ -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 diff --git a/test/functional/fixtures/autoload/health/success2.vim b/test/functional/fixtures/autoload/health/success2.vim new file mode 100644 index 0000000000..b742b4879d --- /dev/null +++ b/test/functional/fixtures/autoload/health/success2.vim @@ -0,0 +1,4 @@ +function! health#success2#check() + call health#report_start("another 1") + call health#report_ok("ok") +endfunction diff --git a/test/functional/helpers.lua b/test/functional/helpers.lua index 6f43ec817c..2d54d23254 100644 --- a/test/functional/helpers.lua +++ b/test/functional/helpers.lua @@ -241,6 +241,7 @@ local function clear(...) 'ASAN_OPTIONS', 'LD_LIBRARY_PATH', 'PATH', 'NVIM_LOG_FILE', + 'NVIM_RPLUGIN_MANIFEST', }) do env_tbl[k] = os.getenv(k) end diff --git a/test/functional/plugin/health_spec.lua b/test/functional/plugin/health_spec.lua new file mode 100644 index 0000000000..a9665cd751 --- /dev/null +++ b/test/functional/plugin/health_spec.lua @@ -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) |