diff options
| author | Justin M. Keyes <justinkz@gmail.com> | 2025-08-28 23:43:10 -0400 |
|---|---|---|
| committer | Justin M. Keyes <justinkz@gmail.com> | 2025-12-30 01:44:24 -0500 |
| commit | 20e77c5d886af54d1f7b6844cffc11129f579ad9 (patch) | |
| tree | e7098a90f9333fa52ae98b0bdd9d0cc82830131f /runtime/lua/vim/_system.lua | |
| parent | 03377b95523324a2a1657435f12c13a493ee5360 (diff) | |
build: ship "_core/*" as bytecode (built-into Nvim binary)
Problem:
We want to encourage implementing core features in Lua instead of C, but
it's clumsy because:
- Core Lua code (built into `nvim` so it is available even if VIMRUNTIME
is missing/invalid) requires manually updating CMakeLists.txt, or
stuffing it into `_editor.lua`.
- Core Lua modules are not organized similar to C modules, `_editor.lua`
is getting too big.
Solution:
- Introduce `_core/` where core Lua code can live. All Lua modules added
there will automatically be included as bytecode in the `nvim` binary.
- Move these core modules into `_core/*`:
```
_defaults.lua
_editor.lua
_options.lua
_system.lua
shared.lua
```
TODO:
- Move `_extui/ => _core/ui2/`
Diffstat (limited to 'runtime/lua/vim/_system.lua')
| -rw-r--r-- | runtime/lua/vim/_system.lua | 513 |
1 files changed, 0 insertions, 513 deletions
diff --git a/runtime/lua/vim/_system.lua b/runtime/lua/vim/_system.lua deleted file mode 100644 index 00c362696f..0000000000 --- a/runtime/lua/vim/_system.lua +++ /dev/null @@ -1,513 +0,0 @@ -local uv = vim.uv - ---- @class vim.SystemOpts ---- @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<string,string|number> ---- ---- `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 ---- ---- 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 `nil` if stdout is disabled or has a custom handler. ---- @field stderr? string `nil` if stderr is disabled or has a custom handler. - ---- @class (package) vim.SystemState ---- @field cmd string[] ---- @field handle? uv.uv_process_t ---- @field timer? uv.uv_timer_t ---- @field pid? integer ---- @field timeout? integer ---- @field done? boolean|'timeout' ---- @field stdin? uv.uv_stream_t ---- @field stdout? uv.uv_stream_t ---- @field stderr? uv.uv_stream_t ---- @field stdout_data? string[] ---- @field stderr_data? string[] ---- @field result? vim.SystemCompleted - ---- @enum vim.SystemSig -local SIG = { - HUP = 1, -- Hangup - INT = 2, -- Interrupt from keyboard - KILL = 9, -- Kill signal - TERM = 15, -- Termination signal - -- STOP = 17,19,23 -- Stop the process -} - ----@param handle uv.uv_handle_t? -local function close_handle(handle) - if handle and not handle:is_closing() then - handle:close() - end -end - ---- @class vim.SystemObj ---- @field cmd string[] Command name and args ---- @field pid integer Process ID ---- @field private _state vim.SystemState -local SystemObj = {} - ---- @param state vim.SystemState ---- @return vim.SystemObj -local function new_systemobj(state) - return setmetatable({ - cmd = state.cmd, - pid = state.pid, - _state = state, - }, { __index = SystemObj }) -end - ---- 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('sigterm') -- sends SIGTERM to the process ---- ``` ---- ---- @param signal integer|string Signal to send to the process. See |luv-constants|. -function SystemObj:kill(signal) - self._state.handle:kill(signal) -end - ---- @package ---- @param signal? vim.SystemSig -function SystemObj:_timeout(signal) - self._state.done = 'timeout' - 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) - local state = self._state - - local done = vim.wait(timeout or state.timeout or vim._maxint, function() - return state.result ~= nil - end, nil, true) - - if not done then - -- Send sigkill since this cannot be caught - self:_timeout(SIG.KILL) - vim.wait(timeout or state.timeout or vim._maxint, function() - return state.result ~= nil - end, nil, true) - end - - 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 - - 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() - close_handle(stdin) - end) - end) - 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 - return handle == nil or handle:is_closing() or false -end - ---- @param output? fun(err: string?, data: string?)|false ---- @param text? boolean ---- @return uv.uv_stream_t? pipe ---- @return fun(err: string?, data: string?)? handler ---- @return string[]? data -local function setup_output(output, text) - if output == false then - return - end - - local bucket --- @type string[]? - local handler --- @type fun(err: string?, data: string?) - - if type(output) == 'function' then - handler = output - else - bucket = {} - handler = function(err, data) - if err then - error(err) - end - if text and data then - bucket[#bucket + 1] = data:gsub('\r\n', '\n') - else - bucket[#bucket + 1] = data - end - end - end - - local pipe = assert(uv.new_pipe(false)) - - --- @param err? string - --- @param data? string - local function handler_with_close(err, data) - handler(err, data) - if data == nil then - pipe:read_stop() - pipe:close() - end - end - - return pipe, handler_with_close, bucket -end - ---- @param input? string|string[]|boolean ---- @return uv.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() --- @type table<string,string> - 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 not env and clear_env then - return - end - - env = env or {} - if not clear_env then - --- @type table<string,string|number> - env = vim.tbl_extend('force', base_env(), env) - end - - 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 - -local is_win = vim.fn.has('win32') == 1 - ---- @param cmd string ---- @param opts uv.spawn.options ---- @param on_exit fun(code: integer, signal: integer) ---- @param on_error fun() ---- @return uv.uv_process_t, integer -local function spawn(cmd, opts, on_exit, on_error) - if is_win then - local cmd1 = vim.fn.exepath(cmd) - if cmd1 ~= '' then - cmd = cmd1 - end - end - - local handle, pid_or_err = uv.spawn(cmd, opts, on_exit) - if not handle then - on_error() - if opts.cwd and not uv.fs_stat(opts.cwd) then - error(("%s (cwd): '%s'"):format(pid_or_err, opts.cwd)) - elseif vim.fn.executable(cmd) == 0 then - error(("%s (cmd): '%s'"):format(pid_or_err, cmd)) - else - error(pid_or_err) - end - end - return handle, pid_or_err --[[@as integer]] -end - ---- @param timeout integer ---- @param cb fun() ---- @return uv.uv_timer_t -local function timer_oneshot(timeout, cb) - local timer = assert(uv.new_timer()) - timer:start(timeout, 0, function() - timer:stop() - timer:close() - cb() - end) - return timer -end - ---- @param state vim.SystemState ---- @param code integer ---- @param signal integer ---- @param on_exit fun(result: vim.SystemCompleted)? -local function _on_exit(state, code, signal, on_exit) - close_handle(state.handle) - close_handle(state.stdin) - close_handle(state.timer) - - -- #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 = 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() - check:close() - - if state.done == nil then - state.done = true - end - - if (code == 0 or code == 1) and state.done == 'timeout' then - -- Unix: code == 0 - -- Windows: code == 1 - code = 124 - end - - local stdout_data = state.stdout_data - local stderr_data = state.stderr_data - - 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 - ---- @param state vim.SystemState -local function _on_error(state) - close_handle(state.handle) - close_handle(state.stdin) - close_handle(state.stdout) - close_handle(state.stderr) - close_handle(state.timer) -end - ---- Run a system command ---- ---- @param cmd string[] ---- @param opts? vim.SystemOpts ---- @param on_exit? fun(out: vim.SystemCompleted) ---- @return vim.SystemObj -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) - - opts = opts or {} - - local stdout, stdout_handler, stdout_data = setup_output(opts.stdout, opts.text) - local stderr, stderr_handler, stderr_data = setup_output(opts.stderr, opts.text) - local stdin, towrite = setup_input(opts.stdin) - - --- @type vim.SystemState - local state = { - done = false, - cmd = cmd, - timeout = opts.timeout, - stdin = stdin, - stdout = stdout, - stdout_data = stdout_data, - stderr = stderr, - stderr_data = stderr_data, - } - - --- @diagnostic disable-next-line:missing-fields - state.handle, state.pid = spawn(cmd[1], { - args = vim.list_slice(cmd, 2), - stdio = { stdin, stdout, stderr }, - cwd = opts.cwd, - --- @diagnostic disable-next-line:assign-type-mismatch - env = setup_env(opts.env, opts.clear_env), - detached = opts.detach, - hide = true, - }, function(code, signal) - _on_exit(state, code, signal, on_exit) - end, function() - _on_error(state) - end) - - if stdout and stdout_handler then - stdout:read_start(stdout_handler) - end - - if stderr and stderr_handler then - stderr:read_start(stderr_handler) - 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 = timer_oneshot(opts.timeout, function() - if state.handle and state.handle:is_active() then - obj:_timeout() - end - end) - end - - return obj -end - ---- Runs a system command or throws an error if {cmd} cannot be run. ---- ---- The command runs directly (not in 'shell') so shell builtins such as "echo" in cmd.exe, cmdlets ---- in powershell, or "help" in bash, will not work unless you actually invoke a shell: ---- `vim.system({'bash', '-c', 'help'})`. ---- ---- 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 |
