summaryrefslogtreecommitdiffstatshomepage
path: root/runtime/lua/vim/_system.lua
diff options
context:
space:
mode:
authorJustin M. Keyes <justinkz@gmail.com>2025-08-28 23:43:10 -0400
committerJustin M. Keyes <justinkz@gmail.com>2025-12-30 01:44:24 -0500
commit20e77c5d886af54d1f7b6844cffc11129f579ad9 (patch)
treee7098a90f9333fa52ae98b0bdd9d0cc82830131f /runtime/lua/vim/_system.lua
parent03377b95523324a2a1657435f12c13a493ee5360 (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.lua513
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