diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt index 053b47a1c8..f82718f1b3 100644 --- a/runtime/doc/lua.txt +++ b/runtime/doc/lua.txt @@ -1754,6 +1754,101 @@ vim.str_utfindex({s}, {encoding}, {index}, {strict_indexing}) Return: ~ (`integer`) + +============================================================================== +Lua module: vim.system *lua-vim-system* + +*vim.SystemCompleted* + + Fields: ~ + • {code} (`integer`) + • {signal} (`integer`) + • {stdout}? (`string`) `nil` if stdout is disabled or has a custom + handler. + • {stderr}? (`string`) `nil` if stderr is disabled or has a custom + handler. + +*vim.SystemObj* + + Fields: ~ + • {cmd} (`string[]`) Command name and args + • {pid} (`integer`) Process ID + • {kill} (`fun(self: vim.SystemObj, signal: integer|string)`) See + |SystemObj:kill()|. + • {wait} (`fun(self: vim.SystemObj, timeout: integer?): vim.SystemCompleted`) + See |SystemObj:wait()|. + • {write} (`fun(self: vim.SystemObj, data: string[]|string?)`) See + |SystemObj:write()|. + • {is_closing} (`fun(self: vim.SystemObj): boolean`) See + |SystemObj:is_closing()|. + + +SystemObj:is_closing() *SystemObj:is_closing()* + Checks if the process handle is closing or already closed. + + This method returns `true` if the underlying process handle is either + `nil` or is in the process of closing. It is useful for determining + whether it is safe to perform operations on the process handle. + + Return: ~ + (`boolean`) + +SystemObj:kill({signal}) *SystemObj:kill()* + Sends a signal to the process. + + The signal can be specified as an integer or as a string. + + Example: >lua + local obj = vim.system({'sleep', '10'}) + obj:kill('TERM') -- sends SIGTERM to the process +< + + Parameters: ~ + • {signal} (`integer|string`) Signal to send to the process. + +SystemObj:wait({timeout}) *SystemObj:wait()* + Waits for the process to complete or until the specified timeout elapses. + + This method blocks execution until the associated process has exited or + the optional `timeout` (in milliseconds) has been reached. If the process + does not exit before the timeout, it is forcefully terminated with SIGKILL + (signal 9), and the exit code is set to 124. + + If no `timeout` is provided, the method will wait indefinitely (or use the + timeout specified in the options when the process was started). + + Example: >lua + local obj = vim.system({'echo', 'hello'}, { text = true }) + local result = obj:wait(1000) -- waits up to 1000ms + print(result.code, result.signal, result.stdout, result.stderr) +< + + Parameters: ~ + • {timeout} (`integer?`) + + Return: ~ + (`vim.SystemCompleted`) See |vim.SystemCompleted|. + +SystemObj:write({data}) *SystemObj:write()* + Writes data to the stdin of the process or closes stdin. + + If `data` is a list of strings, each string is written followed by a + newline. + + If `data` is a string, it is written as-is. + + If `data` is `nil`, the write side of the stream is shut down and the pipe + is closed. + + Example: >lua + local obj = vim.system({'cat'}, { stdin = true }) + obj:write({'hello', 'world'}) -- writes 'hello\nworld\n' to stdin + obj:write(nil) -- closes stdin +< + + Parameters: ~ + • {data} (`string[]|string?`) + vim.system({cmd}, {opts}, {on_exit}) *vim.system()* Runs a system command or throws an error if {cmd} cannot be run. @@ -1778,32 +1873,30 @@ vim.system({cmd}, {opts}, {on_exit}) *vim.system()* Parameters: ~ • {cmd} (`string[]`) Command to execute - • {opts} (`vim.SystemOpts?`) 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. Note: if - `env` is `nil`, the current environment is used but - without `NVIM` set. - • stdin: (string|string[]|boolean) If `true`, then a pipe + • {opts} (`table?`) A table with the following fields: + • {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. Note: if `env` is `nil`, the current + environment is used but without `NVIM` set. + • {stdin}? (`string|string[]|true`) 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 stderr. - 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) Run the command with a time limit. - Upon timeout the process is sent the TERM signal (15) and - the exit code is set to 124. - • detach: (boolean) If true, spawn the child process in a + `write()` method to SystemObj. If `string` or `string[]` + then will be written to stdin and closed. + • {stdout}? (`fun(err:string?, data: string?)|boolean`, + default: `true`) Handle output from stdout. + • {stderr}? (`fun(err:string?, data: string?)|boolean`, + default: `true`) Handle output from stderr. + • {text}? (`boolean`) Handle stdout and stderr as text. + Normalizes line endings by replacing `\r\n` with `\n`. + • {timeout}? (`integer`) Run the command with a time limit + in ms. Upon timeout the process is sent the TERM signal + (15) and the exit code is set to 124. + • {detach}? (`boolean`) 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 @@ -1811,29 +1904,14 @@ vim.system({cmd}, {opts}, {on_exit}) *vim.system()* unless the parent process calls |uv.unref()| on the child's process handle. • {on_exit} (`fun(out: vim.SystemCompleted)?`) Called when subprocess - exits. When provided, the command runs asynchronously. - Receives SystemCompleted object, see return of - SystemObj:wait(). + exits. When provided, the command runs asynchronously. See + return of SystemObj:wait(). + + Overloads: ~ + • `fun(cmd: string, on_exit: fun(out: vim.SystemCompleted)): vim.SystemObj` Return: ~ - (`vim.SystemObj`) Object with the fields: - • cmd (string[]) Command name and args - • pid (integer) Process ID - • wait (fun(timeout: integer|nil): SystemCompleted) Wait for the - process to complete, including any open handles for background - processes (e.g., `bash -c 'sleep 10 &'`). To avoid waiting for - handles, set stdout=false and stderr=false. Upon timeout the process - is sent the KILL signal (9) and the exit code is set to 124. Cannot - be called in |api-fast|. - • 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|string)) - • write (fun(data: string|nil)) Requires `stdin=true`. Pass `nil` to - close the stream. - • is_closing (fun(): boolean) + (`vim.SystemObj`) See |vim.SystemObj|. ============================================================================== @@ -2827,7 +2905,8 @@ vim.ui.open({path}, {opt}) *vim.ui.open()* • {cmd}? (`string[]`) Command used to open the path or URL. Return (multiple): ~ - (`vim.SystemObj?`) Command object, or nil if not found. + (`vim.SystemObj?`) Command object, or nil if not found. See + |vim.SystemObj|. (`string?`) Error message on failure, or nil on success. See also: ~ diff --git a/runtime/lua/vim/_editor.lua b/runtime/lua/vim/_editor.lua index 3cacff7b42..d8bd2d96d2 100644 --- a/runtime/lua/vim/_editor.lua +++ b/runtime/lua/vim/_editor.lua @@ -76,81 +76,6 @@ local utfs = { ['utf-32'] = true, } --- TODO(lewis6991): document that the signature is system({cmd}, [{opts},] {on_exit}) ---- Runs a system command or throws an error if {cmd} cannot be run. ---- ---- Examples: ---- ---- ```lua ---- local on_exit = function(obj) ---- print(obj.code) ---- print(obj.signal) ---- print(obj.stdout) ---- print(obj.stderr) ---- end ---- ---- -- Runs asynchronously: ---- vim.system({'echo', 'hello'}, { text = true }, on_exit) ---- ---- -- Runs synchronously: ---- local obj = vim.system({'echo', 'hello'}, { text = true }):wait() ---- -- { code = 0, signal = 0, stdout = 'hello\n', stderr = '' } ---- ---- ``` ---- ---- See |uv.spawn()| for more details. Note: unlike |uv.spawn()|, vim.system ---- throws an error if {cmd} cannot be run. ---- ---- @param cmd (string[]) Command to execute ---- @param opts vim.SystemOpts? 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. Note: if `env` is `nil`, the current environment is used but without `NVIM` set. ---- - 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 stderr. 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) Run the command with a time limit. Upon timeout the process is sent the ---- TERM signal (15) and the exit code is set to 124. ---- - 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? fun(out: vim.SystemCompleted) Called when subprocess exits. When provided, the command runs ---- asynchronously. Receives SystemCompleted object, see return of SystemObj:wait(). ---- ---- @return vim.SystemObj Object with the fields: ---- - cmd (string[]) Command name and args ---- - pid (integer) Process ID ---- - wait (fun(timeout: integer|nil): SystemCompleted) Wait for the process to complete, ---- including any open handles for background processes (e.g., `bash -c 'sleep 10 &'`). ---- To avoid waiting for handles, set stdout=false and stderr=false. Upon timeout the process is ---- sent the KILL signal (9) and the exit code is set to 124. Cannot be called in |api-fast|. ---- - 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|string)) ---- - 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. -- Used by nvim_get_proc() as a fallback. function vim._os_proc_info(pid) diff --git a/runtime/lua/vim/_init_packages.lua b/runtime/lua/vim/_init_packages.lua index 21e97c65fe..71263e66c3 100644 --- a/runtime/lua/vim/_init_packages.lua +++ b/runtime/lua/vim/_init_packages.lua @@ -96,4 +96,5 @@ end -- only on main thread: functions for interacting with editor state if vim.api and not vim.is_thread() then require('vim._editor') + require('vim._system') end diff --git a/runtime/lua/vim/_system.lua b/runtime/lua/vim/_system.lua index ced341fe28..6e311823f0 100644 --- a/runtime/lua/vim/_system.lua +++ b/runtime/lua/vim/_system.lua @@ -1,23 +1,51 @@ local uv = vim.uv --- @class vim.SystemOpts ---- @field stdin? string|string[]|true ---- @field stdout? fun(err:string?, data: string?)|false ---- @field stderr? fun(err:string?, data: string?)|false +--- @inlinedoc +--- +--- Set the current working directory for the sub-process. --- @field cwd? string +--- +--- Set environment variables for the new process. Inherits the current environment with `NVIM` set +--- to |v:servername|. --- @field env? table +--- +--- `env` defines the job environment exactly, instead of merging current environment. Note: if +--- `env` is `nil`, the current environment is used but without `NVIM` set. --- @field clear_env? 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. +--- @field stdin? string|string[]|true +--- +--- Handle output from stdout. +--- (Default: `true`) +--- @field stdout? fun(err:string?, data: string?)|boolean +--- +--- Handle output from stderr. +--- (Default: `true`) +--- @field stderr? fun(err:string?, data: string?)|boolean +--- +--- Handle stdout and stderr as text. Normalizes line endings by replacing `\r\n` with `\n`. --- @field text? boolean ---- @field timeout? integer Timeout in ms +--- +--- Run the command with a time limit in ms. Upon timeout the process is sent the TERM signal (15) +--- and the exit code is set to 124. +--- @field timeout? integer +--- +--- 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. --- @field detach? boolean --- @class vim.SystemCompleted --- @field code integer --- @field signal integer ---- @field stdout? string ---- @field stderr? string +--- @field stdout? string `nil` if stdout is disabled or has a custom handler. +--- @field stderr? string `nil` if stderr is disabled or has a custom handler. ---- @class vim.SystemState +--- @class (package) vim.SystemState --- @field cmd string[] --- @field handle? uv.uv_process_t --- @field timer? uv.uv_timer_t @@ -48,13 +76,9 @@ local function close_handle(handle) end --- @class vim.SystemObj ---- @field cmd string[] ---- @field pid integer +--- @field cmd string[] Command name and args +--- @field pid integer Process ID --- @field private _state vim.SystemState ---- @field wait fun(self: vim.SystemObj, timeout?: integer): vim.SystemCompleted ---- @field kill fun(self: vim.SystemObj, signal: integer|string) ---- @field write fun(self: vim.SystemObj, data?: string|string[]) ---- @field is_closing fun(self: vim.SystemObj): boolean local SystemObj = {} --- @param state vim.SystemState @@ -67,7 +91,17 @@ local function new_systemobj(state) }, { __index = SystemObj }) end ---- @param signal integer|string +--- Sends a signal to the process. +--- +--- The signal can be specified as an integer or as a string. +--- +--- Example: +--- ```lua +--- local obj = vim.system({'sleep', '10'}) +--- obj:kill('TERM') -- sends SIGTERM to the process +--- ``` +--- +--- @param signal integer|string Signal to send to the process. function SystemObj:kill(signal) self._state.handle:kill(signal) end @@ -79,6 +113,23 @@ function SystemObj:_timeout(signal) self:kill(signal or SIG.TERM) end +--- Waits for the process to complete or until the specified timeout elapses. +--- +--- This method blocks execution until the associated process has exited or +--- the optional `timeout` (in milliseconds) has been reached. If the process +--- does not exit before the timeout, it is forcefully terminated with SIGKILL +--- (signal 9), and the exit code is set to 124. +--- +--- If no `timeout` is provided, the method will wait indefinitely (or use the +--- timeout specified in the options when the process was started). +--- +--- Example: +--- ```lua +--- local obj = vim.system({'echo', 'hello'}, { text = true }) +--- local result = obj:wait(1000) -- waits up to 1000ms +--- print(result.code, result.signal, result.stdout, result.stderr) +--- ``` +--- --- @param timeout? integer --- @return vim.SystemCompleted function SystemObj:wait(timeout) @@ -99,6 +150,23 @@ function SystemObj:wait(timeout) return state.result end +--- Writes data to the stdin of the process or closes stdin. +--- +--- If `data` is a list of strings, each string is written followed by a +--- newline. +--- +--- If `data` is a string, it is written as-is. +--- +--- If `data` is `nil`, the write side of the stream is shut down and the pipe +--- is closed. +--- +--- Example: +--- ```lua +--- local obj = vim.system({'cat'}, { stdin = true }) +--- obj:write({'hello', 'world'}) -- writes 'hello\nworld\n' to stdin +--- obj:write(nil) -- closes stdin +--- ``` +--- --- @param data string[]|string|nil function SystemObj:write(data) local stdin = self._state.stdin @@ -127,6 +195,12 @@ function SystemObj:write(data) end end +--- Checks if the process handle is closing or already closed. +--- +--- This method returns `true` if the underlying process handle is either +--- `nil` or is in the process of closing. It is useful for determining +--- whether it is safe to perform operations on the process handle. +--- --- @return boolean function SystemObj:is_closing() local handle = self._state.handle @@ -228,8 +302,6 @@ end local is_win = vim.fn.has('win32') == 1 -local M = {} - --- @param cmd string --- @param opts uv.spawn.options --- @param on_exit fun(code: integer, signal: integer) @@ -282,7 +354,7 @@ local function _on_exit(state, code, signal, on_exit) -- #30846: Do not close stdout/stderr here, as they may still have data to -- read. They will be closed in uv.read_start on EOF. - local check = assert(uv.new_check()) + local check = uv.new_check() check:start(function() for _, pipe in pairs({ state.stdin, state.stdout, state.stderr }) do if not pipe:is_closing() then @@ -333,7 +405,7 @@ end --- @param opts? vim.SystemOpts --- @param on_exit? fun(out: vim.SystemCompleted) --- @return vim.SystemObj -function M.run(cmd, opts, on_exit) +local function run(cmd, opts, on_exit) vim.validate('cmd', cmd, 'table') vim.validate('opts', opts, 'table', true) vim.validate('on_exit', on_exit, 'function', true) @@ -397,4 +469,41 @@ function M.run(cmd, opts, on_exit) return obj end -return M +--- Runs a system command or throws an error if {cmd} cannot be run. +--- +--- Examples: +--- +--- ```lua +--- local on_exit = function(obj) +--- print(obj.code) +--- print(obj.signal) +--- print(obj.stdout) +--- print(obj.stderr) +--- end +--- +--- -- Runs asynchronously: +--- vim.system({'echo', 'hello'}, { text = true }, on_exit) +--- +--- -- Runs synchronously: +--- local obj = vim.system({'echo', 'hello'}, { text = true }):wait() +--- -- { code = 0, signal = 0, stdout = 'hello\n', stderr = '' } +--- +--- ``` +--- +--- See |uv.spawn()| for more details. Note: unlike |uv.spawn()|, vim.system +--- throws an error if {cmd} cannot be run. +--- +--- @param cmd string[] Command to execute +--- @param opts vim.SystemOpts? +--- @param on_exit? fun(out: vim.SystemCompleted) Called when subprocess exits. When provided, the command runs +--- asynchronously. See return of SystemObj:wait(). +--- +--- @return vim.SystemObj +--- @overload fun(cmd: string, on_exit: fun(out: vim.SystemCompleted)): vim.SystemObj +function vim.system(cmd, opts, on_exit) + if type(opts) == 'function' then + on_exit = opts + opts = nil + end + return run(cmd, opts, on_exit) +end diff --git a/src/gen/gen_steps.zig b/src/gen/gen_steps.zig index 563a4418df..7f2e0faea6 100644 --- a/src/gen/gen_steps.zig +++ b/src/gen/gen_steps.zig @@ -75,6 +75,7 @@ pub fn nvim_gen_sources( "_init_packages", "inspect", "_editor", + "_system", "filetype", "fs", "F", diff --git a/src/gen/gen_vimdoc.lua b/src/gen/gen_vimdoc.lua index 501f51bc9c..f932fadb53 100755 --- a/src/gen/gen_vimdoc.lua +++ b/src/gen/gen_vimdoc.lua @@ -141,6 +141,7 @@ local config = { 'builtin.lua', '_options.lua', '_editor.lua', + '_system.lua', '_inspector.lua', 'shared.lua', 'loader.lua', @@ -172,6 +173,7 @@ local config = { 'runtime/lua/vim/uri.lua', 'runtime/lua/vim/ui.lua', 'runtime/lua/vim/_extui.lua', + 'runtime/lua/vim/_system.lua', 'runtime/lua/vim/filetype.lua', 'runtime/lua/vim/keymap.lua', 'runtime/lua/vim/fs.lua', @@ -215,6 +217,8 @@ local config = { name = name:lower() if name == '_editor' then return 'Lua module: vim' + elseif name == '_system' then + return 'Lua module: vim.system' elseif name == '_options' then return 'LUA-VIMSCRIPT BRIDGE' elseif name == 'builtin' then @@ -243,6 +247,8 @@ local config = { helptag_fmt = function(name) if name == '_editor' then return 'lua-vim' + elseif name == '_system' then + return 'lua-vim-system' elseif name == '_options' then return 'lua-vimscript' elseif name == 'tohtml' then diff --git a/src/nvim/CMakeLists.txt b/src/nvim/CMakeLists.txt index f881679f8d..f198f94cf5 100644 --- a/src/nvim/CMakeLists.txt +++ b/src/nvim/CMakeLists.txt @@ -335,6 +335,7 @@ set(VIM_MODULE_FILE ${GENERATED_DIR}/lua/vim_module.generated.h) # NVIM_RUNTIME_DIR set(LUA_DEFAULTS_MODULE_SOURCE ${NVIM_RUNTIME_DIR}/lua/vim/_defaults.lua) set(LUA_EDITOR_MODULE_SOURCE ${NVIM_RUNTIME_DIR}/lua/vim/_editor.lua) +set(LUA_SYSTEM_MODULE_SOURCE ${NVIM_RUNTIME_DIR}/lua/vim/_system.lua) set(LUA_FILETYPE_MODULE_SOURCE ${NVIM_RUNTIME_DIR}/lua/vim/filetype.lua) set(LUA_FS_MODULE_SOURCE ${NVIM_RUNTIME_DIR}/lua/vim/fs.lua) set(LUA_F_MODULE_SOURCE ${NVIM_RUNTIME_DIR}/lua/vim/F.lua) @@ -626,6 +627,7 @@ add_custom_command( ${LUA_INIT_PACKAGES_MODULE_SOURCE} "vim._init_packages" ${LUA_INSPECT_MODULE_SOURCE} "vim.inspect" ${LUA_EDITOR_MODULE_SOURCE} "vim._editor" + ${LUA_SYSTEM_MODULE_SOURCE} "vim._system" ${LUA_FILETYPE_MODULE_SOURCE} "vim.filetype" ${LUA_FS_MODULE_SOURCE} "vim.fs" ${LUA_F_MODULE_SOURCE} "vim.F" @@ -640,6 +642,7 @@ add_custom_command( ${LUA_INIT_PACKAGES_MODULE_SOURCE} ${LUA_INSPECT_MODULE_SOURCE} ${LUA_EDITOR_MODULE_SOURCE} + ${LUA_SYSTEM_MODULE_SOURCE} ${LUA_FILETYPE_MODULE_SOURCE} ${LUA_FS_MODULE_SOURCE} ${LUA_F_MODULE_SOURCE}