aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt2
-rw-r--r--cmake/RunTests.cmake6
-rw-r--r--runtime/autoload/health.vim542
-rw-r--r--runtime/autoload/health/nvim.vim439
-rw-r--r--runtime/autoload/provider/pythonx.vim21
-rw-r--r--runtime/autoload/provider/ruby.vim18
-rw-r--r--runtime/autoload/remote/host.vim58
-rw-r--r--runtime/doc/eval.txt48
-rw-r--r--runtime/doc/msgpack_rpc.txt34
-rw-r--r--runtime/doc/pi_health.txt127
-rw-r--r--runtime/doc/provider.txt8
-rw-r--r--runtime/doc/remote_plugin.txt26
-rw-r--r--runtime/doc/vim_diff.txt1
-rw-r--r--runtime/ftplugin/man.vim2
-rw-r--r--runtime/plugin/health.vim2
-rwxr-xr-xscripts/release.sh2
-rw-r--r--src/nvim/eval.c118
-rw-r--r--src/nvim/event/process.c12
-rw-r--r--src/nvim/event/rstream.c17
-rw-r--r--src/nvim/event/socket.c4
-rw-r--r--src/nvim/event/stream.c9
-rw-r--r--src/nvim/event/stream.h3
-rw-r--r--src/nvim/event/wstream.c17
-rw-r--r--src/nvim/ex_cmds.c541
-rw-r--r--src/nvim/globals.h3
-rw-r--r--src/nvim/msgpack_rpc/channel.c92
-rw-r--r--src/nvim/msgpack_rpc/channel.h1
-rw-r--r--src/nvim/os/input.c6
-rw-r--r--src/nvim/os/shell.c8
-rw-r--r--src/nvim/path.c9
-rw-r--r--src/nvim/terminal.c3
-rw-r--r--src/nvim/testdir/test_alot.vim1
-rw-r--r--src/nvim/testdir/test_regexp_utf8.vim41
-rw-r--r--src/nvim/tui/input.c12
-rw-r--r--src/nvim/version.c3
-rw-r--r--test/functional/api/rpc_fixture.lua38
-rw-r--r--test/functional/api/server_requests_spec.lua46
-rw-r--r--test/functional/core/job_spec.lua8
-rw-r--r--test/functional/fixtures/autoload/health/broken.vim3
-rw-r--r--test/functional/fixtures/autoload/health/success1.vim6
-rw-r--r--test/functional/fixtures/autoload/health/success2.vim4
-rw-r--r--test/functional/helpers.lua1
-rw-r--r--test/functional/plugin/health_spec.lua81
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,
- &regmatch) == FAIL) {
- if (do_error)
+ if (search_regcomp(pat, RE_SUBST, which_pat, SEARCH_HIS, &regmatch) == 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(&regmatch, 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(&regmatch, 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(&regmatch,
- 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)