aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLewis Russell <lewis6991@gmail.com>2023-06-07 13:52:23 +0100
committerGitHub <noreply@github.com>2023-06-07 13:52:23 +0100
commitc0952e62fd0ee16a3275bb69e0de04c836b39015 (patch)
treeebecfe9f07b4e5d5a306b83a886372da269d38f9
parent4ecc71f6fc7377403ed91ae5bc32992a5d08f678 (diff)
downloadrneovim-c0952e62fd0ee16a3275bb69e0de04c836b39015.tar.gz
rneovim-c0952e62fd0ee16a3275bb69e0de04c836b39015.tar.bz2
rneovim-c0952e62fd0ee16a3275bb69e0de04c836b39015.zip
feat(lua): add `vim.system()`
feat(lua): add vim.system() Problem: Handling system commands in Lua is tedious and error-prone: - vim.fn.jobstart() is vimscript and comes with all limitations attached to typval. - vim.loop.spawn is too low level Solution: Add vim.system(). Partly inspired by Python's subprocess module Does not expose any libuv objects.
-rw-r--r--runtime/doc/lua.txt71
-rw-r--r--runtime/doc/news.txt2
-rw-r--r--runtime/lua/man.lua82
-rw-r--r--runtime/lua/vim/_editor.lua99
-rw-r--r--runtime/lua/vim/_system.lua342
-rw-r--r--runtime/lua/vim/lsp/rpc.lua124
-rw-r--r--scripts/lua2dox.lua1
-rw-r--r--test/functional/lua/system_spec.lua57
8 files changed, 611 insertions, 167 deletions
diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt
index 38289dc5d0..5e0a1edc11 100644
--- a/runtime/doc/lua.txt
+++ b/runtime/doc/lua.txt
@@ -1566,6 +1566,77 @@ schedule_wrap({cb}) *vim.schedule_wrap()*
• |vim.schedule()|
• |vim.in_fast_event()|
+system({cmd}, {opts}, {on_exit}) *vim.system()*
+ Run a system command
+
+ Examples: >lua
+
+ local on_exit = function(obj)
+ print(obj.code)
+ print(obj.signal)
+ print(obj.stdout)
+ print(obj.stderr)
+ end
+
+ -- Run asynchronously
+ vim.system({'echo', 'hello'}, { text = true }, on_exit)
+
+ -- Run synchronously
+ local obj = vim.system({'echo', 'hello'}, { text = true }):wait()
+ -- { code = 0, signal = 0, stdout = 'hello', stderr = '' }
+<
+
+ See |uv.spawn()| for more details.
+
+ Parameters: ~
+ • {cmd} (string[]) Command to execute
+ • {opts} (SystemOpts|nil) Options:
+ • cwd: (string) Set the current working directory for the
+ sub-process.
+ • env: table<string,string> Set environment variables for
+ the new process. Inherits the current environment with
+ `NVIM` set to |v:servername|.
+ • clear_env: (boolean) `env` defines the job environment
+ exactly, instead of merging current environment.
+ • stdin: (string|string[]|boolean) If `true`, then a pipe
+ to stdin is opened and can be written to via the
+ `write()` method to SystemObj. If string or string[] then
+ will be written to stdin and closed. Defaults to `false`.
+ • stdout: (boolean|function) Handle output from stdout.
+ When passed as a function must have the signature
+ `fun(err: string, data: string)`. Defaults to `true`
+ • stderr: (boolean|function) Handle output from stdout.
+ When passed as a function must have the signature
+ `fun(err: string, data: string)`. Defaults to `true`.
+ • text: (boolean) Handle stdout and stderr as text.
+ Replaces `\r\n` with `\n`.
+ • timeout: (integer)
+ • detach: (boolean) If true, spawn the child process in a
+ detached state - this will make it a process group
+ leader, and will effectively enable the child to keep
+ running after the parent exits. Note that the child
+ process will still keep the parent's event loop alive
+ unless the parent process calls |uv.unref()| on the
+ child's process handle.
+ • {on_exit} (function|nil) Called when subprocess exits. When provided,
+ the command runs asynchronously. Receives SystemCompleted
+ object, see return of SystemObj:wait().
+
+ Return: ~
+ SystemObj Object with the fields:
+ • pid (integer) Process ID
+ • wait (fun(timeout: integer|nil): SystemCompleted)
+ • SystemCompleted is an object with the fields:
+ • code: (integer)
+ • signal: (integer)
+ • stdout: (string), nil if stdout argument is passed
+ • stderr: (string), nil if stderr argument is passed
+
+ • kill (fun(signal: integer))
+ • write (fun(data: string|nil)) Requires `stdin=true`. Pass `nil` to
+ close the stream.
+ • is_closing (fun(): boolean)
+
==============================================================================
Lua module: inspector *lua-inspector*
diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt
index af5263bcf5..4afb3429f4 100644
--- a/runtime/doc/news.txt
+++ b/runtime/doc/news.txt
@@ -76,6 +76,8 @@ The following new APIs or features were added.
is resized horizontally). Note: Lines that are not visible and kept in
|'scrollback'| are not reflown.
+• |vim.system()| for running system commands.
+
==============================================================================
CHANGED FEATURES *news-changed*
diff --git a/runtime/lua/man.lua b/runtime/lua/man.lua
index 09265b1999..08aff20a03 100644
--- a/runtime/lua/man.lua
+++ b/runtime/lua/man.lua
@@ -14,79 +14,19 @@ local function man_error(msg)
end
-- Run a system command and timeout after 30 seconds.
----@param cmd_ string[]
+---@param cmd string[]
---@param silent boolean?
----@param env string[]
+---@param env? table<string,string|number>
---@return string
-local function system(cmd_, silent, env)
- local stdout_data = {} ---@type string[]
- local stderr_data = {} ---@type string[]
- local stdout = assert(vim.uv.new_pipe(false))
- local stderr = assert(vim.uv.new_pipe(false))
+local function system(cmd, silent, env)
+ local r = vim.system(cmd, { env = env, timeout = 10000 }):wait()
- local done = false
- local exit_code ---@type integer?
-
- -- We use the `env` command here rather than the env option to vim.uv.spawn since spawn will
- -- completely overwrite the environment when we just want to modify the existing one.
- --
- -- Overwriting mainly causes problems NixOS which relies heavily on a non-standard environment.
- local cmd = cmd_
- if env then
- cmd = { 'env' }
- vim.list_extend(cmd, env)
- vim.list_extend(cmd, cmd_)
- end
-
- local handle
- handle = vim.uv.spawn(cmd[1], {
- args = vim.list_slice(cmd, 2),
- stdio = { nil, stdout, stderr },
- }, function(code)
- exit_code = code
- stdout:close()
- stderr:close()
- handle:close()
- done = true
- end)
-
- if handle then
- stdout:read_start(function(_, data)
- stdout_data[#stdout_data + 1] = data
- end)
- stderr:read_start(function(_, data)
- stderr_data[#stderr_data + 1] = data
- end)
- else
- stdout:close()
- stderr:close()
- if not silent then
- local cmd_str = table.concat(cmd, ' ')
- man_error(string.format('command error: %s', cmd_str))
- end
- return ''
- end
-
- vim.wait(30000, function()
- return done
- end)
-
- if not done then
- if handle then
- handle:close()
- stdout:close()
- stderr:close()
- end
- local cmd_str = table.concat(cmd, ' ')
- man_error(string.format('command timed out: %s', cmd_str))
- end
-
- if exit_code ~= 0 and not silent then
+ if r.code ~= 0 and not silent then
local cmd_str = table.concat(cmd, ' ')
- man_error(string.format("command error '%s': %s", cmd_str, table.concat(stderr_data)))
+ man_error(string.format("command error '%s': %s", cmd_str, r.stderr))
end
- return table.concat(stdout_data)
+ return assert(r.stdout)
end
---@param line string
@@ -312,7 +252,7 @@ local function get_path(sect, name, silent)
end
local lines = system(cmd, silent)
- local results = vim.split(lines or {}, '\n', { trimempty = true })
+ local results = vim.split(lines, '\n', { trimempty = true })
if #results == 0 then
return
@@ -505,9 +445,9 @@ local function get_page(path, silent)
-- http://comments.gmane.org/gmane.editors.vim.devel/29085
-- Set MAN_KEEP_FORMATTING so Debian man doesn't discard backspaces.
return system(cmd, silent, {
- 'MANPAGER=cat',
- 'MANWIDTH=' .. manwidth,
- 'MAN_KEEP_FORMATTING=1',
+ MANPAGER = 'cat',
+ MANWIDTH = manwidth,
+ MAN_KEEP_FORMATTING = 1,
})
end
diff --git a/runtime/lua/vim/_editor.lua b/runtime/lua/vim/_editor.lua
index 7b946a55e4..d46b0fbf32 100644
--- a/runtime/lua/vim/_editor.lua
+++ b/runtime/lua/vim/_editor.lua
@@ -42,10 +42,6 @@ for k, v in pairs({
vim._submodules[k] = v
end
--- Remove at Nvim 1.0
----@deprecated
-vim.loop = vim.uv
-
-- There are things which have special rules in vim._init_packages
-- for legacy reasons (uri) or for performance (_inspector).
-- most new things should go into a submodule namespace ( vim.foobar.do_thing() )
@@ -69,13 +65,73 @@ vim.log = {
},
}
--- Internal-only until comments in #8107 are addressed.
--- Returns:
--- {errcode}, {output}
-function vim._system(cmd)
- local out = vim.fn.system(cmd)
- local err = vim.v.shell_error
- return err, out
+-- TODO(lewis6991): document that the signature is system({cmd}, [{opts},] {on_exit})
+--- Run a system command
+---
+--- Examples:
+--- <pre>lua
+---
+--- local on_exit = function(obj)
+--- print(obj.code)
+--- print(obj.signal)
+--- print(obj.stdout)
+--- print(obj.stderr)
+--- end
+---
+--- -- Run asynchronously
+--- vim.system({'echo', 'hello'}, { text = true }, on_exit)
+---
+--- -- Run synchronously
+--- local obj = vim.system({'echo', 'hello'}, { text = true }):wait()
+--- -- { code = 0, signal = 0, stdout = 'hello', stderr = '' }
+---
+--- </pre>
+---
+--- See |uv.spawn()| for more details.
+---
+--- @param cmd (string[]) Command to execute
+--- @param opts (SystemOpts|nil) Options:
+--- - cwd: (string) Set the current working directory for the sub-process.
+--- - env: table<string,string> Set environment variables for the new process. Inherits the
+--- current environment with `NVIM` set to |v:servername|.
+--- - clear_env: (boolean) `env` defines the job environment exactly, instead of merging current
+--- environment.
+--- - stdin: (string|string[]|boolean) If `true`, then a pipe to stdin is opened and can be written
+--- to via the `write()` method to SystemObj. If string or string[] then will be written to stdin
+--- and closed. Defaults to `false`.
+--- - stdout: (boolean|function)
+--- Handle output from stdout. When passed as a function must have the signature `fun(err: string, data: string)`.
+--- Defaults to `true`
+--- - stderr: (boolean|function)
+--- Handle output from stdout. When passed as a function must have the signature `fun(err: string, data: string)`.
+--- Defaults to `true`.
+--- - text: (boolean) Handle stdout and stderr as text. Replaces `\r\n` with `\n`.
+--- - timeout: (integer)
+--- - detach: (boolean) If true, spawn the child process in a detached state - this will make it
+--- a process group leader, and will effectively enable the child to keep running after the
+--- parent exits. Note that the child process will still keep the parent's event loop alive
+--- unless the parent process calls |uv.unref()| on the child's process handle.
+---
+--- @param on_exit (function|nil) Called when subprocess exits. When provided, the command runs
+--- asynchronously. Receives SystemCompleted object, see return of SystemObj:wait().
+---
+--- @return SystemObj Object with the fields:
+--- - pid (integer) Process ID
+--- - wait (fun(timeout: integer|nil): SystemCompleted)
+--- - SystemCompleted is an object with the fields:
+--- - code: (integer)
+--- - signal: (integer)
+--- - stdout: (string), nil if stdout argument is passed
+--- - stderr: (string), nil if stderr argument is passed
+--- - kill (fun(signal: integer))
+--- - write (fun(data: string|nil)) Requires `stdin=true`. Pass `nil` to close the stream.
+--- - is_closing (fun(): boolean)
+function vim.system(cmd, opts, on_exit)
+ if type(opts) == 'function' then
+ on_exit = opts
+ opts = nil
+ end
+ return require('vim._system').run(cmd, opts, on_exit)
end
-- Gets process info from the `ps` command.
@@ -85,13 +141,14 @@ function vim._os_proc_info(pid)
error('invalid pid')
end
local cmd = { 'ps', '-p', pid, '-o', 'comm=' }
- local err, name = vim._system(cmd)
- if 1 == err and vim.trim(name) == '' then
+ local r = vim.system(cmd):wait()
+ local name = assert(r.stdout)
+ if r.code == 1 and vim.trim(name) == '' then
return {} -- Process not found.
- elseif 0 ~= err then
+ elseif r.code ~= 0 then
error('command failed: ' .. vim.fn.string(cmd))
end
- local _, ppid = vim._system({ 'ps', '-p', pid, '-o', 'ppid=' })
+ local ppid = assert(vim.system({ 'ps', '-p', pid, '-o', 'ppid=' }):wait().stdout)
-- Remove trailing whitespace.
name = vim.trim(name):gsub('^.*/', '')
ppid = tonumber(ppid) or -1
@@ -109,14 +166,14 @@ function vim._os_proc_children(ppid)
error('invalid ppid')
end
local cmd = { 'pgrep', '-P', ppid }
- local err, rv = vim._system(cmd)
- if 1 == err and vim.trim(rv) == '' then
+ local r = vim.system(cmd):wait()
+ if r.code == 1 and vim.trim(r.stdout) == '' then
return {} -- Process not found.
- elseif 0 ~= err then
+ elseif r.code ~= 0 then
error('command failed: ' .. vim.fn.string(cmd))
end
local children = {}
- for s in rv:gmatch('%S+') do
+ for s in r.stdout:gmatch('%S+') do
local i = tonumber(s)
if i ~= nil then
table.insert(children, i)
@@ -1006,4 +1063,8 @@ end
require('vim._meta')
+-- Remove at Nvim 1.0
+---@deprecated
+vim.loop = vim.uv
+
return vim
diff --git a/runtime/lua/vim/_system.lua b/runtime/lua/vim/_system.lua
new file mode 100644
index 0000000000..eadf801a31
--- /dev/null
+++ b/runtime/lua/vim/_system.lua
@@ -0,0 +1,342 @@
+local uv = vim.uv
+
+--- @class SystemOpts
+--- @field cmd string[]
+--- @field stdin string|string[]|true
+--- @field stdout fun(err:string, data: string)|false
+--- @field stderr fun(err:string, data: string)|false
+--- @field cwd? string
+--- @field env? table<string,string|number>
+--- @field clear_env? boolean
+--- @field text boolean?
+--- @field timeout? integer Timeout in ms
+--- @field detach? boolean
+
+--- @class SystemCompleted
+--- @field code integer
+--- @field signal integer
+--- @field stdout? string
+--- @field stderr? string
+
+--- @class SystemState
+--- @field handle uv_process_t
+--- @field timer uv_timer_t
+--- @field pid integer
+--- @field timeout? integer
+--- @field done boolean
+--- @field stdin uv_stream_t?
+--- @field stdout uv_stream_t?
+--- @field stderr uv_stream_t?
+--- @field cmd string[]
+--- @field result? SystemCompleted
+
+---@private
+---@param state SystemState
+local function close_handles(state)
+ for _, handle in pairs({ state.handle, state.stdin, state.stdout, state.stderr }) do
+ if not handle:is_closing() then
+ handle:close()
+ end
+ end
+end
+
+--- @param cmd string[]
+--- @return SystemCompleted
+local function timeout_result(cmd)
+ local cmd_str = table.concat(cmd, ' ')
+ local err = string.format("Command timed out: '%s'", cmd_str)
+ return { code = 0, signal = 2, stdout = '', stderr = err }
+end
+
+--- @class SystemObj
+--- @field pid integer
+--- @field private _state SystemState
+--- @field wait fun(self: SystemObj, timeout?: integer): SystemCompleted
+--- @field kill fun(self: SystemObj, signal: integer)
+--- @field write fun(self: SystemObj, data?: string|string[])
+--- @field is_closing fun(self: SystemObj): boolean?
+local SystemObj = {}
+
+--- @param state SystemState
+--- @return SystemObj
+local function new_systemobj(state)
+ return setmetatable({
+ pid = state.pid,
+ _state = state,
+ }, { __index = SystemObj })
+end
+
+--- @param signal integer
+function SystemObj:kill(signal)
+ local state = self._state
+ state.handle:kill(signal)
+ close_handles(state)
+end
+
+local MAX_TIMEOUT = 2 ^ 31
+
+--- @param timeout? integer
+--- @return SystemCompleted
+function SystemObj:wait(timeout)
+ local state = self._state
+
+ vim.wait(timeout or state.timeout or MAX_TIMEOUT, function()
+ return state.done
+ end)
+
+ if not state.done then
+ self:kill(6) -- 'sigint'
+ state.result = timeout_result(state.cmd)
+ end
+
+ return state.result
+end
+
+--- @param data string[]|string|nil
+function SystemObj:write(data)
+ local stdin = self._state.stdin
+
+ if not stdin then
+ error('stdin has not been opened on this object')
+ end
+
+ if type(data) == 'table' then
+ for _, v in ipairs(data) do
+ stdin:write(v)
+ stdin:write('\n')
+ end
+ elseif type(data) == 'string' then
+ stdin:write(data)
+ elseif data == nil then
+ -- Shutdown the write side of the duplex stream and then close the pipe.
+ -- Note shutdown will wait for all the pending write requests to complete
+ -- TODO(lewis6991): apparently shutdown doesn't behave this way.
+ -- (https://github.com/neovim/neovim/pull/17620#discussion_r820775616)
+ stdin:write('', function()
+ stdin:shutdown(function()
+ if stdin then
+ stdin:close()
+ end
+ end)
+ end)
+ end
+end
+
+--- @return boolean
+function SystemObj:is_closing()
+ local handle = self._state.handle
+ return handle == nil or handle:is_closing()
+end
+
+---@private
+---@param output function|'false'
+---@return uv_stream_t?
+---@return function? Handler
+local function setup_output(output)
+ if output == nil then
+ return assert(uv.new_pipe(false)), nil
+ end
+
+ if type(output) == 'function' then
+ return assert(uv.new_pipe(false)), output
+ end
+
+ assert(output == false)
+ return nil, nil
+end
+
+---@private
+---@param input string|string[]|true|nil
+---@return uv_stream_t?
+---@return string|string[]?
+local function setup_input(input)
+ if not input then
+ return
+ end
+
+ local towrite --- @type string|string[]?
+ if type(input) == 'string' or type(input) == 'table' then
+ towrite = input
+ end
+
+ return assert(uv.new_pipe(false)), towrite
+end
+
+--- @return table<string,string>
+local function base_env()
+ local env = vim.fn.environ()
+ env['NVIM'] = vim.v.servername
+ env['NVIM_LISTEN_ADDRESS'] = nil
+ return env
+end
+
+--- uv.spawn will completely overwrite the environment
+--- when we just want to modify the existing one, so
+--- make sure to prepopulate it with the current env.
+--- @param env? table<string,string|number>
+--- @param clear_env? boolean
+--- @return string[]?
+local function setup_env(env, clear_env)
+ if clear_env then
+ return env
+ end
+
+ --- @type table<string,string|number>
+ env = vim.tbl_extend('force', base_env(), env or {})
+
+ local renv = {} --- @type string[]
+ for k, v in pairs(env) do
+ renv[#renv + 1] = string.format('%s=%s', k, tostring(v))
+ end
+
+ return renv
+end
+
+--- @param stream uv_stream_t
+--- @param text? boolean
+--- @param bucket string[]
+--- @return fun(err: string?, data: string?)
+local function default_handler(stream, text, bucket)
+ return function(err, data)
+ if err then
+ error(err)
+ end
+ if data ~= nil then
+ if text then
+ bucket[#bucket + 1] = data:gsub('\r\n', '\n')
+ else
+ bucket[#bucket + 1] = data
+ end
+ else
+ stream:read_stop()
+ stream:close()
+ end
+ end
+end
+
+local M = {}
+
+--- @param cmd string
+--- @param opts uv.aliases.spawn_options
+--- @param on_exit fun(code: integer, signal: integer)
+--- @param on_error fun()
+--- @return uv_process_t, integer
+local function spawn(cmd, opts, on_exit, on_error)
+ local handle, pid_or_err = uv.spawn(cmd, opts, on_exit)
+ if not handle then
+ on_error()
+ error(pid_or_err)
+ end
+ return handle, pid_or_err --[[@as integer]]
+end
+
+--- Run a system command
+---
+--- @param cmd string[]
+--- @param opts? SystemOpts
+--- @param on_exit? fun(out: SystemCompleted)
+--- @return SystemObj
+function M.run(cmd, opts, on_exit)
+ vim.validate({
+ cmd = { cmd, 'table' },
+ opts = { opts, 'table', true },
+ on_exit = { on_exit, 'function', true },
+ })
+
+ opts = opts or {}
+
+ local stdout, stdout_handler = setup_output(opts.stdout)
+ local stderr, stderr_handler = setup_output(opts.stderr)
+ local stdin, towrite = setup_input(opts.stdin)
+
+ --- @type SystemState
+ local state = {
+ done = false,
+ cmd = cmd,
+ timeout = opts.timeout,
+ stdin = stdin,
+ stdout = stdout,
+ stderr = stderr,
+ }
+
+ -- Define data buckets as tables and concatenate the elements at the end as
+ -- one operation.
+ --- @type string[], string[]
+ local stdout_data, stderr_data
+
+ state.handle, state.pid = spawn(cmd[1], {
+ args = vim.list_slice(cmd, 2),
+ stdio = { stdin, stdout, stderr },
+ cwd = opts.cwd,
+ env = setup_env(opts.env, opts.clear_env),
+ detached = opts.detach,
+ hide = true,
+ }, function(code, signal)
+ close_handles(state)
+ if state.timer then
+ state.timer:stop()
+ state.timer:close()
+ end
+
+ local check = assert(uv.new_check())
+
+ check:start(function()
+ for _, pipe in pairs({ state.stdin, state.stdout, state.stderr }) do
+ if not pipe:is_closing() then
+ return
+ end
+ end
+ check:stop()
+
+ state.done = true
+ state.result = {
+ code = code,
+ signal = signal,
+ stdout = stdout_data and table.concat(stdout_data) or nil,
+ stderr = stderr_data and table.concat(stderr_data) or nil,
+ }
+
+ if on_exit then
+ on_exit(state.result)
+ end
+ end)
+ end, function()
+ close_handles(state)
+ end)
+
+ if stdout then
+ stdout_data = {}
+ stdout:read_start(stdout_handler or default_handler(stdout, opts.text, stdout_data))
+ end
+
+ if stderr then
+ stderr_data = {}
+ stderr:read_start(stderr_handler or default_handler(stderr, opts.text, stderr_data))
+ end
+
+ local obj = new_systemobj(state)
+
+ if towrite then
+ obj:write(towrite)
+ obj:write(nil) -- close the stream
+ end
+
+ if opts.timeout then
+ state.timer = assert(uv.new_timer())
+ state.timer:start(opts.timeout, 0, function()
+ state.timer:stop()
+ state.timer:close()
+ if state.handle and state.handle:is_active() then
+ obj:kill(6) --- 'sigint'
+ state.result = timeout_result(state.cmd)
+ if on_exit then
+ on_exit(state.result)
+ end
+ end
+ end)
+ end
+
+ return obj
+end
+
+return M
diff --git a/runtime/lua/vim/lsp/rpc.lua b/runtime/lua/vim/lsp/rpc.lua
index 5f48effebf..64bc732bdf 100644
--- a/runtime/lua/vim/lsp/rpc.lua
+++ b/runtime/lua/vim/lsp/rpc.lua
@@ -15,32 +15,6 @@ local function is_dir(filename)
end
---@private
---- Merges current process env with the given env and returns the result as
---- a list of "k=v" strings.
----
---- <pre>
---- Example:
----
---- in: { PRODUCTION="false", PATH="/usr/bin/", PORT=123, HOST="0.0.0.0", }
---- out: { "PRODUCTION=false", "PATH=/usr/bin/", "PORT=123", "HOST=0.0.0.0", }
---- </pre>
----@param env (table) table of environment variable assignments
----@returns (table) list of `"k=v"` strings
-local function env_merge(env)
- if env == nil then
- return env
- end
- -- Merge.
- env = vim.tbl_extend('force', vim.fn.environ(), env)
- local final_env = {}
- for k, v in pairs(env) do
- assert(type(k) == 'string', 'env must be a dict')
- table.insert(final_env, k .. '=' .. tostring(v))
- end
- return final_env
-end
-
----@private
--- Embeds the given string into a table and correctly computes `Content-Length`.
---
---@param encoded_message (string)
@@ -658,89 +632,85 @@ end
--- - `is_closing()` returns a boolean indicating if the RPC is closing.
--- - `terminate()` terminates the RPC client.
local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
- local _ = log.info()
- and log.info('Starting RPC client', { cmd = cmd, args = cmd_args, extra = extra_spawn_params })
+ if log.info() then
+ log.info('Starting RPC client', { cmd = cmd, args = cmd_args, extra = extra_spawn_params })
+ end
+
validate({
cmd = { cmd, 's' },
cmd_args = { cmd_args, 't' },
dispatchers = { dispatchers, 't', true },
})
- if extra_spawn_params and extra_spawn_params.cwd then
+ extra_spawn_params = extra_spawn_params or {}
+
+ if extra_spawn_params.cwd then
assert(is_dir(extra_spawn_params.cwd), 'cwd must be a directory')
end
dispatchers = merge_dispatchers(dispatchers)
- local stdin = uv.new_pipe(false)
- local stdout = uv.new_pipe(false)
- local stderr = uv.new_pipe(false)
- local handle, pid
+
+ local sysobj ---@type SystemObj
local client = new_client(dispatchers, {
write = function(msg)
- stdin:write(msg)
+ sysobj:write(msg)
end,
is_closing = function()
- return handle == nil or handle:is_closing()
+ return sysobj == nil or sysobj:is_closing()
end,
terminate = function()
- if handle then
- handle:kill(15)
- end
+ sysobj:kill(15)
end,
})
- ---@private
- --- Callback for |vim.uv.spawn()| Closes all streams and runs the `on_exit` dispatcher.
- ---@param code (integer) Exit code
- ---@param signal (integer) Signal that was used to terminate (if any)
- local function onexit(code, signal)
- stdin:close()
- stdout:close()
- stderr:close()
- handle:close()
- dispatchers.on_exit(code, signal)
+ local handle_body = function(body)
+ client:handle_body(body)
end
- local spawn_params = {
- args = cmd_args,
- stdio = { stdin, stdout, stderr },
- detached = not is_win,
- }
- if extra_spawn_params then
- spawn_params.cwd = extra_spawn_params.cwd
- spawn_params.env = env_merge(extra_spawn_params.env)
- if extra_spawn_params.detached ~= nil then
- spawn_params.detached = extra_spawn_params.detached
+
+ local stdout_handler = create_read_loop(handle_body, nil, function(err)
+ client:on_error(client_errors.READ_ERROR, err)
+ end)
+
+ local stderr_handler = function(_, chunk)
+ if chunk and log.error() then
+ log.error('rpc', cmd, 'stderr', chunk)
end
end
- handle, pid = uv.spawn(cmd, spawn_params, onexit)
- if handle == nil then
- stdin:close()
- stdout:close()
- stderr:close()
+
+ local detached = not is_win
+ if extra_spawn_params.detached ~= nil then
+ detached = extra_spawn_params.detached
+ end
+
+ local cmd1 = { cmd }
+ vim.list_extend(cmd1, cmd_args)
+
+ local ok, sysobj_or_err = pcall(vim.system, cmd1, {
+ stdin = true,
+ stdout = stdout_handler,
+ stderr = stderr_handler,
+ cwd = extra_spawn_params.cwd,
+ env = extra_spawn_params.env,
+ detach = detached,
+ }, function(obj)
+ dispatchers.on_exit(obj.code, obj.signal)
+ end)
+
+ if not ok then
+ local err = sysobj_or_err --[[@as string]]
local msg = string.format('Spawning language server with cmd: `%s` failed', cmd)
- if string.match(pid, 'ENOENT') then
+ if string.match(err, 'ENOENT') then
msg = msg
.. '. The language server is either not installed, missing from PATH, or not executable.'
else
- msg = msg .. string.format(' with error message: %s', pid)
+ msg = msg .. string.format(' with error message: %s', err)
end
vim.notify(msg, vim.log.levels.WARN)
return
end
- stderr:read_start(function(_, chunk)
- if chunk then
- local _ = log.error() and log.error('rpc', cmd, 'stderr', chunk)
- end
- end)
-
- local handle_body = function(body)
- client:handle_body(body)
- end
- stdout:read_start(create_read_loop(handle_body, nil, function(err)
- client:on_error(client_errors.READ_ERROR, err)
- end))
+ sysobj = sysobj_or_err --[[@as SystemObj]]
return public_client(client)
end
diff --git a/scripts/lua2dox.lua b/scripts/lua2dox.lua
index 014934aebe..bb5214f091 100644
--- a/scripts/lua2dox.lua
+++ b/scripts/lua2dox.lua
@@ -340,6 +340,7 @@ function TLua2DoX_filter.filter(this, AppStamp, Filename)
if vim.startswith(line, '---@cast')
or vim.startswith(line, '---@diagnostic')
+ or vim.startswith(line, '---@overload')
or vim.startswith(line, '---@type') then
-- Ignore LSP directives
outStream:writeln('// gg:"' .. line .. '"')
diff --git a/test/functional/lua/system_spec.lua b/test/functional/lua/system_spec.lua
new file mode 100644
index 0000000000..836d3a83b0
--- /dev/null
+++ b/test/functional/lua/system_spec.lua
@@ -0,0 +1,57 @@
+local helpers = require('test.functional.helpers')(after_each)
+local clear = helpers.clear
+local exec_lua = helpers.exec_lua
+local eq = helpers.eq
+
+local function system_sync(cmd, opts)
+ return exec_lua([[
+ return vim.system(...):wait()
+ ]], cmd, opts)
+end
+
+local function system_async(cmd, opts)
+ exec_lua([[
+ local cmd, opts = ...
+ _G.done = false
+ vim.system(cmd, opts, function(obj)
+ _G.done = true
+ _G.ret = obj
+ end)
+ ]], cmd, opts)
+
+ while true do
+ if exec_lua[[return _G.done]] then
+ break
+ end
+ end
+
+ return exec_lua[[return _G.ret]]
+end
+
+describe('vim.system', function()
+ before_each(function()
+ clear()
+ end)
+
+ for name, system in pairs{ sync = system_sync, async = system_async, } do
+ describe('('..name..')', function()
+ it('can run simple commands', function()
+ eq('hello\n', system({'echo', 'hello' }, { text = true }).stdout)
+ end)
+
+ it('handle input', function()
+ eq('hellocat', system({ 'cat' }, { stdin = 'hellocat', text = true }).stdout)
+ end)
+
+ it ('supports timeout', function()
+ eq({
+ code = 0,
+ signal = 2,
+ stdout = '',
+ stderr = "Command timed out: 'sleep 10'"
+ }, system({ 'sleep', '10' }, { timeout = 1 }))
+ end)
+ end)
+ end
+
+end)