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 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 ---@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 + if r.code ~= 0 and not silent then local cmd_str = table.concat(cmd, ' ') - man_error(string.format('command timed out: %s', cmd_str)) + man_error(string.format("command error '%s': %s", cmd_str, r.stderr)) end - if exit_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))) - 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: +---
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. +--- +--- @param cmd (string[]) Command to execute +--- @param opts (SystemOpts|nil) Options: +--- - cwd: (string) Set the current working directory for the sub-process. +--- - env: table 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 +--- @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 +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 +--- @param clear_env? boolean +--- @return string[]? +local function setup_env(env, clear_env) + if clear_env then + return env + end + + --- @type table + 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 @@ -14,32 +14,6 @@ local function is_dir(filename) return stat and stat.type == 'directory' or false end ----@private ---- Merges current process env with the given env and returns the result as ---- a list of "k=v" strings. ---- ----
---- 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", }
---- 
----@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`. --- @@ -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)