refactor: vendor vim.system
This commit is contained in:
parent
badaef04f8
commit
19654d963e
|
@ -7,6 +7,10 @@ on:
|
|||
branches: [ main ]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
commit_lint:
|
||||
runs-on: ubuntu-latest
|
||||
|
@ -16,6 +20,7 @@ jobs:
|
|||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
|
@ -40,6 +45,9 @@ jobs:
|
|||
- name: Download Nvim
|
||||
run: make nvim
|
||||
|
||||
- name: Download Busted
|
||||
run: make busted
|
||||
|
||||
- name: Run Test
|
||||
run: make test
|
||||
|
||||
|
|
3
Makefile
3
Makefile
|
@ -51,6 +51,9 @@ LUAROCKS := luarocks --lua-version=5.1 --tree .luarocks
|
|||
.luarocks/bin/busted:
|
||||
$(LUAROCKS) install busted
|
||||
|
||||
.PHONY: busted
|
||||
busted: .luarocks/bin/busted
|
||||
|
||||
.PHONY: test
|
||||
test: $(NVIM_RUNNER) $(NVIM_TEST) .luarocks/bin/busted
|
||||
eval $$($(LUAROCKS) path) && $(NVIM_RUNNER)/bin/nvim -ll test/busted/runner.lua -v \
|
||||
|
|
|
@ -3,7 +3,7 @@ local scheduler = require('gitsigns.async').scheduler
|
|||
|
||||
local log = require('gitsigns.debug.log')
|
||||
local util = require('gitsigns.util')
|
||||
local subprocess = require('gitsigns.subprocess')
|
||||
local system = require('gitsigns.system').system
|
||||
|
||||
local gs_config = require('gitsigns.config')
|
||||
local config = gs_config.config
|
||||
|
@ -19,6 +19,9 @@ local error_once = require('gitsigns.message').error_once
|
|||
|
||||
local M = {}
|
||||
|
||||
--- @type fun(cmd: string[], opts?: SystemOpts): vim.SystemCompleted
|
||||
local asystem = async.wrap(system, 3)
|
||||
|
||||
--- @param file string
|
||||
--- @return boolean
|
||||
local function in_git_dir(file)
|
||||
|
@ -104,16 +107,15 @@ function M._set_version(version)
|
|||
return
|
||||
end
|
||||
|
||||
--- @type integer, integer, string?, string?
|
||||
local _, _, stdout, stderr = async.wait(2, subprocess.run_job, {
|
||||
command = 'git',
|
||||
args = { '--version' },
|
||||
})
|
||||
--- @type vim.SystemCompleted
|
||||
local obj = asystem({ 'git', '--version' })
|
||||
|
||||
local stdout = obj.stdout
|
||||
|
||||
local line = vim.split(stdout or '', '\n', { plain = true })[1]
|
||||
if not line then
|
||||
err("Unable to detect git version as 'git --version' failed to return anything")
|
||||
eprint(stderr)
|
||||
eprint(obj.stderr)
|
||||
return
|
||||
end
|
||||
assert(type(line) == 'string', 'Unexpected output: ' .. line)
|
||||
|
@ -122,14 +124,9 @@ function M._set_version(version)
|
|||
M.version = parse_version(parts[3])
|
||||
end
|
||||
|
||||
--- @class Gitsigns.Git.JobSpec
|
||||
--- @class Gitsigns.Git.JobSpec : SystemOpts
|
||||
--- @field command? string
|
||||
--- @field cwd? string
|
||||
--- @field writer? string[] | string
|
||||
--- @field suppress_stderr? boolean
|
||||
--- @field raw? boolean Do not strip trailing newlines from stdout
|
||||
--- @field text? boolean Convert CRLF to LF
|
||||
--- @field args? string[]
|
||||
--- @field ignore_error? boolean
|
||||
|
||||
--- @param args string[]
|
||||
--- @param spec? Gitsigns.Git.JobSpec
|
||||
|
@ -138,39 +135,35 @@ local git_command = async.create(function(args, spec)
|
|||
if not M.version then
|
||||
M._set_version(config._git_version)
|
||||
end
|
||||
|
||||
spec = spec or {}
|
||||
spec.command = spec.command or 'git'
|
||||
spec.args = spec.command == 'git'
|
||||
and {
|
||||
'--no-pager',
|
||||
'--literal-pathspecs',
|
||||
'-c',
|
||||
'gc.auto=0', -- Disable auto-packing which emits messages to stderr
|
||||
unpack(args),
|
||||
}
|
||||
or args
|
||||
|
||||
if not spec.cwd and not uv.cwd() then
|
||||
spec.cwd = vim.env.HOME
|
||||
local cmd = {
|
||||
spec.command or 'git',
|
||||
'--no-pager',
|
||||
'--literal-pathspecs',
|
||||
'-c',
|
||||
'gc.auto=0', -- Disable auto-packing which emits messages to stderr
|
||||
unpack(args),
|
||||
}
|
||||
|
||||
if spec.text == nil then
|
||||
spec.text = true
|
||||
end
|
||||
|
||||
--- @type integer, integer, string?, string?
|
||||
local _, _, stdout, stderr = async.wait(2, subprocess.run_job, spec)
|
||||
--- @type vim.SystemCompleted
|
||||
local obj = asystem(cmd, spec)
|
||||
local stdout = obj.stdout
|
||||
local stderr = obj.stderr
|
||||
|
||||
if not spec.suppress_stderr then
|
||||
if stderr then
|
||||
local cmd_str = table.concat({ spec.command, unpack(args) }, ' ')
|
||||
log.eprintf("Received stderr when running command\n'%s':\n%s", cmd_str, stderr)
|
||||
end
|
||||
end
|
||||
|
||||
if stdout and spec.text then
|
||||
stdout = stdout:gsub('\r\n', '\n')
|
||||
if not spec.ignore_error and obj.code > 0 then
|
||||
local cmd_str = table.concat(cmd, ' ')
|
||||
log.eprintf("Received exit code %d when running command\n'%s':\n%s", obj.code, cmd_str, stderr)
|
||||
end
|
||||
|
||||
local stdout_lines = vim.split(stdout or '', '\n', { plain = true })
|
||||
|
||||
if not spec.raw then
|
||||
if spec.text then
|
||||
-- If stdout ends with a newline, then remove the final empty string after
|
||||
-- the split
|
||||
if stdout_lines[#stdout_lines] == '' then
|
||||
|
@ -185,6 +178,10 @@ local git_command = async.create(function(args, spec)
|
|||
end
|
||||
end
|
||||
|
||||
if stderr == '' then
|
||||
stderr = nil
|
||||
end
|
||||
|
||||
return stdout_lines, stderr
|
||||
end, 2)
|
||||
|
||||
|
@ -205,6 +202,9 @@ function M.diff(file_cmp, file_buf, indent_heuristic, diff_algo)
|
|||
'--unified=0',
|
||||
file_cmp,
|
||||
file_buf,
|
||||
}, {
|
||||
-- git-diff implies --exit-code
|
||||
ignore_error = true,
|
||||
})
|
||||
end
|
||||
|
||||
|
@ -220,7 +220,7 @@ local function process_abbrev_head(gitdir, head_str, path, cmd)
|
|||
if head_str == 'HEAD' then
|
||||
local short_sha = git_command({ 'rev-parse', '--short', 'HEAD' }, {
|
||||
command = cmd or 'git',
|
||||
suppress_stderr = true,
|
||||
ignore_error = true,
|
||||
cwd = path,
|
||||
})[1] or ''
|
||||
if log.debug_mode and short_sha ~= '' then
|
||||
|
@ -243,7 +243,9 @@ local cygpath_convert ---@type fun(path: string): string
|
|||
|
||||
if has_cygpath then
|
||||
cygpath_convert = function(path)
|
||||
return git_command({ '-aw', path }, { command = 'cygpath' })[1]
|
||||
--- @type vim.SystemCompleted
|
||||
local obj = asystem({ 'cygpath', '-aw', path })
|
||||
return obj.stdout
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -298,8 +300,8 @@ function M.get_repo_info(path, cmd, gitdir, toplevel)
|
|||
})
|
||||
|
||||
local results = git_command(args, {
|
||||
command = cmd or 'git',
|
||||
suppress_stderr = true,
|
||||
command = cmd,
|
||||
ignore_error = true,
|
||||
cwd = toplevel or path,
|
||||
})
|
||||
|
||||
|
@ -375,7 +377,7 @@ end
|
|||
--- @param encoding? string
|
||||
--- @return string[] stdout, string? stderr
|
||||
function Repo:get_show_text(object, encoding)
|
||||
local stdout, stderr = self:command({ 'show', object }, { raw = true, suppress_stderr = true })
|
||||
local stdout, stderr = self:command({ 'show', object }, { text = false, ignore_error = true })
|
||||
|
||||
if encoding and encoding ~= 'utf-8' and iconv_supported(encoding) then
|
||||
for i, l in ipairs(stdout) do
|
||||
|
@ -398,7 +400,7 @@ end
|
|||
function Repo.new(dir, gitdir, toplevel)
|
||||
local self = setmetatable({}, { __index = Repo })
|
||||
|
||||
self.username = git_command({ 'config', 'user.name' })[1]
|
||||
self.username = git_command({ 'config', 'user.name' }, { ignore_error = true })[1]
|
||||
local info = M.get_repo_info(dir, nil, gitdir, toplevel)
|
||||
for k, v in
|
||||
pairs(info --[[@as table<string,any>]])
|
||||
|
@ -479,10 +481,10 @@ function Obj:file_info(file, silent)
|
|||
'--exclude-standard',
|
||||
'--eol',
|
||||
file or self.file,
|
||||
}, { suppress_stderr = true })
|
||||
}, { ignore_error = true })
|
||||
|
||||
if stderr and not silent then
|
||||
-- Suppress_stderr for the cases when we run:
|
||||
-- ignore_error for the cases when we run:
|
||||
-- git ls-files --others exists/nonexist
|
||||
if not stderr:match('^warning: could not open directory .*: No such file or directory') then
|
||||
log.eprint(stderr)
|
||||
|
@ -637,7 +639,7 @@ function Obj:run_blame(lines, lnum, ignore_whitespace)
|
|||
vim.list_extend(args, { '--ignore-revs-file', ignore_file })
|
||||
end
|
||||
|
||||
local results, stderr = self:command(args, { writer = lines, suppress_stderr = true })
|
||||
local results, stderr = self:command(args, { stdin = lines, ignore_error = true })
|
||||
if stderr then
|
||||
error_once('Error running git-blame: ' .. stderr)
|
||||
return
|
||||
|
@ -763,7 +765,7 @@ function Obj:stage_lines(lines)
|
|||
'--path',
|
||||
self.relpath,
|
||||
'--stdin',
|
||||
}, { writer = lines })
|
||||
}, { stdin = lines })
|
||||
|
||||
local new_object = stdout[1]
|
||||
|
||||
|
@ -797,7 +799,7 @@ function Obj.stage_hunks(self, hunks, invert)
|
|||
'--unidiff-zero',
|
||||
'-',
|
||||
}, {
|
||||
writer = patch,
|
||||
stdin = patch,
|
||||
})
|
||||
end
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ local log = require('gitsigns.debug.log')
|
|||
local dprint = log.dprint
|
||||
local dprintf = log.dprintf
|
||||
|
||||
local subprocess = require('gitsigns.subprocess')
|
||||
local system = require('gitsigns.system')
|
||||
local util = require('gitsigns.util')
|
||||
local run_diff = require('gitsigns.diff')
|
||||
|
||||
|
@ -512,7 +512,7 @@ M.update = throttle_by_id(function(bufnr)
|
|||
|
||||
update_cnt = update_cnt + 1
|
||||
|
||||
dprintf('updates: %s, jobs: %s', update_cnt, subprocess.job_cnt)
|
||||
dprintf('updates: %s, jobs: %s', update_cnt, system.job_cnt)
|
||||
end, true)
|
||||
|
||||
--- @param bufnr integer
|
||||
|
|
|
@ -1,116 +0,0 @@
|
|||
local log = require('gitsigns.debug.log')
|
||||
local uv = vim.loop
|
||||
|
||||
local M = {}
|
||||
|
||||
M.job_cnt = 0
|
||||
|
||||
--- @class Gitsigns.JobSpec
|
||||
--- @field command string
|
||||
--- @field args string[]
|
||||
--- @field cwd string
|
||||
--- @field writer string[] | string
|
||||
|
||||
--- @param ... uv.uv_pipe_t
|
||||
local function try_close(...)
|
||||
for i = 1, select('#', ...) do
|
||||
local pipe = select(i, ...)
|
||||
if pipe and not pipe:is_closing() then
|
||||
pipe:close()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- @param pipe uv.uv_pipe_t
|
||||
--- @param x string[]|string
|
||||
local function handle_writer(pipe, x)
|
||||
if type(x) == 'table' then
|
||||
for i, v in ipairs(x) do
|
||||
pipe:write(v)
|
||||
if i ~= #x then
|
||||
pipe:write('\n')
|
||||
else
|
||||
pipe:write('\n', function()
|
||||
try_close(pipe)
|
||||
end)
|
||||
end
|
||||
end
|
||||
elseif x then
|
||||
-- write is string
|
||||
pipe:write(x, function()
|
||||
try_close(pipe)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
--- @param pipe uv.uv_pipe_t
|
||||
--- @param output string[]
|
||||
local function handle_reader(pipe, output)
|
||||
pipe:read_start(function(err, data)
|
||||
if err then
|
||||
log.eprint(err)
|
||||
end
|
||||
if data then
|
||||
output[#output + 1] = data
|
||||
else
|
||||
try_close(pipe)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
--- @param obj Gitsigns.JobSpec
|
||||
--- @param callback fun(_: integer, _: integer, _: string?, _: string?)
|
||||
function M.run_job(obj, callback)
|
||||
local __FUNC__ = 'run_job'
|
||||
if log.debug_mode then
|
||||
local cmd = obj.command .. ' ' .. table.concat(obj.args, ' ')
|
||||
log.dprint(cmd)
|
||||
end
|
||||
|
||||
local stdout_data = {}
|
||||
local stderr_data = {}
|
||||
|
||||
local stdout = assert(uv.new_pipe(false))
|
||||
local stderr = assert(uv.new_pipe(false))
|
||||
local stdin --- @type uv.uv_pipe_t?
|
||||
if obj.writer then
|
||||
stdin = assert(uv.new_pipe(false))
|
||||
end
|
||||
|
||||
--- @type uv.uv_process_t?, integer|string
|
||||
local handle, _pid
|
||||
--- @diagnostic disable-next-line:missing-fields
|
||||
handle, _pid = uv.spawn(obj.command, {
|
||||
args = obj.args,
|
||||
stdio = { stdin, stdout, stderr },
|
||||
cwd = obj.cwd,
|
||||
}, function(code, signal)
|
||||
if handle then
|
||||
handle:close()
|
||||
end
|
||||
stdout:read_stop()
|
||||
stderr:read_stop()
|
||||
|
||||
try_close(stdin, stdout, stderr)
|
||||
|
||||
local stdout_result = #stdout_data > 0 and table.concat(stdout_data) or nil
|
||||
local stderr_result = #stderr_data > 0 and table.concat(stderr_data) or nil
|
||||
|
||||
callback(code, signal, stdout_result, stderr_result)
|
||||
end)
|
||||
|
||||
if not handle then
|
||||
try_close(stdin, stdout, stderr)
|
||||
error(debug.traceback('Failed to spawn process: ' .. vim.inspect(obj)))
|
||||
end
|
||||
|
||||
handle_reader(stdout, stdout_data)
|
||||
handle_reader(stderr, stderr_data)
|
||||
if stdin then
|
||||
handle_writer(stdin, obj.writer)
|
||||
end
|
||||
|
||||
M.job_cnt = M.job_cnt + 1
|
||||
end
|
||||
|
||||
return M
|
|
@ -0,0 +1,22 @@
|
|||
local log = require('gitsigns.debug.log')
|
||||
|
||||
local M = {
|
||||
job_cnt = 0,
|
||||
}
|
||||
|
||||
local system = vim.system or require('gitsigns.system.compat')
|
||||
|
||||
--- @param cmd string[]
|
||||
--- @param opts SystemOpts
|
||||
--- @param on_exit fun(obj: vim.SystemCompleted)
|
||||
--- @return vim.SystemObj
|
||||
function M.system(cmd, opts, on_exit)
|
||||
local __FUNC__ = 'run_job'
|
||||
if log.debug_mode then
|
||||
log.dprint(table.concat(cmd, ' '))
|
||||
end
|
||||
M.job_cnt = M.job_cnt + 1
|
||||
return system(cmd, opts, on_exit)
|
||||
end
|
||||
|
||||
return M
|
|
@ -0,0 +1,334 @@
|
|||
local uv = vim.loop
|
||||
|
||||
--- @param handle uv.uv_handle_t?
|
||||
local function close_handle(handle)
|
||||
if handle and not handle:is_closing() then
|
||||
handle:close()
|
||||
end
|
||||
end
|
||||
|
||||
--- @type 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 state vim.SystemState
|
||||
local function close_handles(state)
|
||||
close_handle(state.handle)
|
||||
close_handle(state.stdin)
|
||||
close_handle(state.stdout)
|
||||
close_handle(state.stderr)
|
||||
close_handle(state.timer)
|
||||
end
|
||||
|
||||
--- @class Pckr.SystemObj : vim.SystemObj
|
||||
--- @field private _state vim.SystemState
|
||||
local SystemObj = {}
|
||||
|
||||
--- @param state vim.SystemState
|
||||
--- @return vim.SystemObj
|
||||
local function new_systemobj(state)
|
||||
return setmetatable({
|
||||
pid = state.pid,
|
||||
_state = state,
|
||||
}, { __index = SystemObj })
|
||||
end
|
||||
|
||||
--- @param signal integer|string
|
||||
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
|
||||
|
||||
local MAX_TIMEOUT = 2 ^ 31
|
||||
|
||||
--- @param timeout? integer
|
||||
--- @return vim.SystemCompleted
|
||||
function SystemObj:wait(timeout)
|
||||
local state = self._state
|
||||
|
||||
local done = vim.wait(timeout or state.timeout or MAX_TIMEOUT, function()
|
||||
return state.result ~= nil
|
||||
end)
|
||||
|
||||
if not done then
|
||||
-- Send sigkill since this cannot be caught
|
||||
self:_timeout(SIG.KILL)
|
||||
vim.wait(timeout or state.timeout or MAX_TIMEOUT, function()
|
||||
return state.result ~= nil
|
||||
end)
|
||||
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() or false
|
||||
end
|
||||
|
||||
--- @param output fun(err:string?, data: string?)|false
|
||||
--- @return uv.uv_stream_t?
|
||||
--- @return fun(err:string?, data: string?)? 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
|
||||
|
||||
--- @param input string|string[]|true|nil
|
||||
--- @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
|
||||
|
||||
local environ = vim.fn.environ()
|
||||
environ['NVIM'] = vim.v.servername
|
||||
environ['NVIM_LISTEN_ADDRESS'] = nil
|
||||
|
||||
--- 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', environ, 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.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
|
||||
|
||||
--- @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)
|
||||
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
|
||||
|
||||
--- @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_handles(state)
|
||||
|
||||
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()
|
||||
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
|
||||
|
||||
--- Run a system command
|
||||
---
|
||||
--- @param cmd string[]
|
||||
--- @param opts? SystemOpts
|
||||
--- @param on_exit? fun(out: vim.SystemCompleted)
|
||||
--- @return vim.SystemObj
|
||||
local function system(cmd, opts, on_exit)
|
||||
local __FUNC__ = 'run_job'
|
||||
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 vim.SystemState
|
||||
local state = {
|
||||
done = false,
|
||||
cmd = cmd,
|
||||
timeout = opts.timeout,
|
||||
stdin = stdin,
|
||||
stdout = stdout,
|
||||
stderr = stderr,
|
||||
}
|
||||
|
||||
--- @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()
|
||||
close_handles(state)
|
||||
end)
|
||||
|
||||
if stdout then
|
||||
state.stdout_data = {}
|
||||
stdout:read_start(stdout_handler or default_handler(stdout, opts.text, state.stdout_data))
|
||||
end
|
||||
|
||||
if stderr then
|
||||
state.stderr_data = {}
|
||||
stderr:read_start(stderr_handler or default_handler(stderr, opts.text, state.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 = timer_oneshot(opts.timeout, function()
|
||||
if state.handle and state.handle:is_active() then
|
||||
obj:_timeout()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
return obj
|
||||
end
|
||||
|
||||
return system
|
Loading…
Reference in New Issue