mirror of https://github.com/folke/lazy.nvim.git
refactor: async processes
This commit is contained in:
parent
4319846b8c
commit
a36ebd2a75
|
@ -1,78 +1,122 @@
|
|||
---@class AsyncOpts
|
||||
---@field on_done? fun()
|
||||
---@field on_error? fun(err:string)
|
||||
---@field on_yield? fun(res:any)
|
||||
|
||||
local M = {}
|
||||
|
||||
---@type Async[]
|
||||
M._queue = {}
|
||||
M._executor = assert(vim.loop.new_check())
|
||||
M._running = false
|
||||
---@type Async
|
||||
M.current = nil
|
||||
|
||||
---@type table<thread, Async>
|
||||
M._threads = setmetatable({}, { __mode = "k" })
|
||||
|
||||
---@alias AsyncEvent "done" | "error" | "yield" | "ok"
|
||||
|
||||
---@class Async
|
||||
---@field co thread
|
||||
---@field opts AsyncOpts
|
||||
---@field sleeping? boolean
|
||||
---@field _co thread
|
||||
---@field _fn fun()
|
||||
---@field _suspended? boolean
|
||||
---@field _on table<AsyncEvent, fun(res:any, async:Async)[]>
|
||||
local Async = {}
|
||||
|
||||
---@param fn async fun()
|
||||
---@param opts? AsyncOpts
|
||||
---@return Async
|
||||
function Async.new(fn, opts)
|
||||
function Async.new(fn)
|
||||
local self = setmetatable({}, { __index = Async })
|
||||
self.co = coroutine.create(fn)
|
||||
self.opts = opts or {}
|
||||
return self:init(fn)
|
||||
end
|
||||
|
||||
---@param fn async fun()
|
||||
---@return Async
|
||||
function Async:init(fn)
|
||||
self._fn = fn
|
||||
self._on = {}
|
||||
self._co = coroutine.create(function()
|
||||
local ok, err = pcall(self._fn)
|
||||
if not ok then
|
||||
self:_emit("error", err)
|
||||
end
|
||||
self:_emit("done")
|
||||
end)
|
||||
M._threads[self._co] = self
|
||||
return M.add(self)
|
||||
end
|
||||
|
||||
function Async:restart()
|
||||
assert(not self:running(), "Cannot restart a running async")
|
||||
self:init(self._fn)
|
||||
end
|
||||
|
||||
---@param event AsyncEvent
|
||||
---@param cb async fun(res:any, async:Async)
|
||||
function Async:on(event, cb)
|
||||
self._on[event] = self._on[event] or {}
|
||||
table.insert(self._on[event], cb)
|
||||
return self
|
||||
end
|
||||
|
||||
function Async:running()
|
||||
return coroutine.status(self.co) ~= "dead"
|
||||
---@private
|
||||
---@param event AsyncEvent
|
||||
---@param res any
|
||||
function Async:_emit(event, res)
|
||||
for _, cb in ipairs(self._on[event] or {}) do
|
||||
cb(res, self)
|
||||
end
|
||||
end
|
||||
|
||||
function Async:running()
|
||||
return coroutine.status(self._co) ~= "dead"
|
||||
end
|
||||
|
||||
---@async
|
||||
function Async:sleep(ms)
|
||||
self.sleeping = true
|
||||
self._suspended = true
|
||||
vim.defer_fn(function()
|
||||
self.sleeping = false
|
||||
self._suspended = false
|
||||
end, ms)
|
||||
coroutine.yield()
|
||||
end
|
||||
|
||||
---@async
|
||||
function Async:suspend()
|
||||
self.sleeping = true
|
||||
self._suspended = true
|
||||
if coroutine.running() == self._co then
|
||||
coroutine.yield()
|
||||
end
|
||||
end
|
||||
|
||||
function Async:resume()
|
||||
self.sleeping = false
|
||||
self._suspended = false
|
||||
end
|
||||
|
||||
function Async:wait()
|
||||
local async = M.running()
|
||||
if coroutine.running() == self._co then
|
||||
error("Cannot wait on self")
|
||||
end
|
||||
|
||||
while self:running() do
|
||||
if async then
|
||||
coroutine.yield()
|
||||
else
|
||||
vim.wait(10)
|
||||
end
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
function Async:step()
|
||||
if self.sleeping then
|
||||
if self._suspended then
|
||||
return true
|
||||
end
|
||||
local status = coroutine.status(self.co)
|
||||
local status = coroutine.status(self._co)
|
||||
if status == "suspended" then
|
||||
M.current = self
|
||||
local ok, res = coroutine.resume(self.co)
|
||||
M.current = nil
|
||||
local ok, res = coroutine.resume(self._co)
|
||||
if not ok then
|
||||
if self.opts.on_error then
|
||||
self.opts.on_error(tostring(res))
|
||||
end
|
||||
error(res)
|
||||
elseif res then
|
||||
if self.opts.on_yield then
|
||||
self.opts.on_yield(res)
|
||||
self:_emit("yield", res)
|
||||
end
|
||||
end
|
||||
end
|
||||
if self:running() then
|
||||
return true
|
||||
end
|
||||
if self.opts.on_done then
|
||||
self.opts.on_done()
|
||||
end
|
||||
return self:running()
|
||||
end
|
||||
|
||||
function M.step()
|
||||
|
@ -107,32 +151,24 @@ function M.add(async)
|
|||
return async
|
||||
end
|
||||
|
||||
---@param fn async fun()
|
||||
---@param opts? AsyncOpts
|
||||
function M.run(fn, opts)
|
||||
return M.add(Async.new(fn, opts))
|
||||
end
|
||||
|
||||
---@generic T: async fun()
|
||||
---@param fn T
|
||||
---@param opts? AsyncOpts
|
||||
---@return T
|
||||
function M.wrap(fn, opts)
|
||||
return function(...)
|
||||
local args = { ... }
|
||||
---@async
|
||||
local wrapped = function()
|
||||
return fn(unpack(args))
|
||||
end
|
||||
return M.run(wrapped, opts)
|
||||
function M.running()
|
||||
local co = coroutine.running()
|
||||
if co then
|
||||
local async = M._threads[co]
|
||||
assert(async, "In coroutine without async context")
|
||||
return async
|
||||
end
|
||||
end
|
||||
|
||||
---@async
|
||||
---@param ms number
|
||||
function M.sleep(ms)
|
||||
assert(M.current, "Not in an async context")
|
||||
M.current:sleep(ms)
|
||||
local async = M.running()
|
||||
assert(async, "Not in an async context")
|
||||
async:sleep(ms)
|
||||
end
|
||||
|
||||
M.Async = Async
|
||||
M.new = Async.new
|
||||
|
||||
return M
|
||||
|
|
|
@ -236,7 +236,7 @@ function M.clear(plugins)
|
|||
if plugin._.tasks then
|
||||
---@param task LazyTask
|
||||
plugin._.tasks = vim.tbl_filter(function(task)
|
||||
return task:is_running() or task:has_errors()
|
||||
return task:running() or task:has_errors()
|
||||
end, plugin._.tasks)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,67 +1,133 @@
|
|||
local Async = require("lazy.async")
|
||||
local Config = require("lazy.core.config")
|
||||
|
||||
local M = {}
|
||||
|
||||
---@type table<uv_process_t, true>
|
||||
M.running = {}
|
||||
|
||||
M.signals = {
|
||||
"HUP",
|
||||
"INT",
|
||||
"QUIT",
|
||||
"ILL",
|
||||
"TRAP",
|
||||
"ABRT",
|
||||
"BUS",
|
||||
"FPE",
|
||||
"KILL",
|
||||
"USR1",
|
||||
"SEGV",
|
||||
"USR2",
|
||||
"PIPE",
|
||||
"ALRM",
|
||||
"TERM",
|
||||
"CHLD",
|
||||
"CONT",
|
||||
"STOP",
|
||||
"TSTP",
|
||||
"TTIN",
|
||||
"TTOU",
|
||||
"URG",
|
||||
"XCPU",
|
||||
"XFSZ",
|
||||
"VTALRM",
|
||||
"PROF",
|
||||
"WINCH",
|
||||
"IO",
|
||||
"PWR",
|
||||
"EMT",
|
||||
"SYS",
|
||||
"INFO",
|
||||
}
|
||||
|
||||
---@diagnostic disable-next-line: no-unknown
|
||||
local uv = vim.uv
|
||||
|
||||
---@class ProcessOpts
|
||||
---@field args string[]
|
||||
---@field cwd? string
|
||||
---@field on_line? fun(string)
|
||||
---@field on_line? fun(line:string)
|
||||
---@field on_exit? fun(ok:boolean, output:string)
|
||||
---@field on_data? fun(string)
|
||||
---@field on_data? fun(data:string, is_stderr?:boolean)
|
||||
---@field timeout? number
|
||||
---@field env? table<string,string>
|
||||
|
||||
---@param opts? ProcessOpts
|
||||
---@param cmd string
|
||||
function M.spawn(cmd, opts)
|
||||
opts = opts or {}
|
||||
opts.timeout = opts.timeout or (Config.options.git and Config.options.git.timeout * 1000)
|
||||
local M = {}
|
||||
|
||||
---@type table<uv_process_t, LazyProcess>
|
||||
M.running = setmetatable({}, { __mode = "k" })
|
||||
|
||||
---@class LazyProcess: Async
|
||||
---@field handle? uv_process_t
|
||||
---@field pid? number
|
||||
---@field cmd string
|
||||
---@field opts ProcessOpts
|
||||
---@field timeout? uv_timer_t
|
||||
---@field timedout? boolean
|
||||
---@field data string
|
||||
---@field check? uv_check_t
|
||||
---@field code? number
|
||||
---@field signal? number
|
||||
local Process = setmetatable({}, { __index = Async.Async })
|
||||
|
||||
---@param cmd string|string[]
|
||||
---@param opts? ProcessOpts
|
||||
function Process.new(cmd, opts)
|
||||
local self = setmetatable({}, { __index = Process })
|
||||
---@async
|
||||
Process.init(self, function()
|
||||
self:_run()
|
||||
end)
|
||||
opts = opts or {}
|
||||
opts.args = opts.args or {}
|
||||
if type(cmd) == "table" then
|
||||
self.cmd = table.remove(cmd, 1)
|
||||
vim.list_extend(opts.args, cmd)
|
||||
else
|
||||
self.cmd = cmd
|
||||
end
|
||||
opts.timeout = opts.timeout or (Config.options.git and Config.options.git.timeout * 1000)
|
||||
-- make sure the cwd is valid
|
||||
if not opts.cwd and type(uv.cwd()) ~= "string" then
|
||||
opts.cwd = uv.os_homedir()
|
||||
end
|
||||
opts.on_line = opts.on_line and vim.schedule_wrap(opts.on_line) or nil
|
||||
opts.on_data = opts.on_data and vim.schedule_wrap(opts.on_data) or nil
|
||||
self.data = ""
|
||||
self.opts = opts
|
||||
self.code = 1
|
||||
self.signal = 0
|
||||
return self
|
||||
end
|
||||
|
||||
---@async
|
||||
function Process:_run()
|
||||
self:guard()
|
||||
local stdout = assert(uv.new_pipe())
|
||||
local stderr = assert(uv.new_pipe())
|
||||
self.handle = uv.spawn(self.cmd, {
|
||||
stdio = { nil, stdout, stderr },
|
||||
args = self.opts.args,
|
||||
cwd = self.opts.cwd,
|
||||
env = self:env(),
|
||||
}, function(code, signal)
|
||||
self.code = code
|
||||
self.signal = signal
|
||||
if self.timeout then
|
||||
self.timeout:stop()
|
||||
end
|
||||
self.handle:close()
|
||||
stdout:close()
|
||||
stderr:close()
|
||||
self:resume()
|
||||
end)
|
||||
|
||||
if self.handle then
|
||||
M.running[self.handle] = self
|
||||
stdout:read_start(function(err, data)
|
||||
self:on_data(err, data)
|
||||
end)
|
||||
stderr:read_start(function(err, data)
|
||||
self:on_data(err, data, true)
|
||||
end)
|
||||
self:suspend()
|
||||
while not (self.handle:is_closing() and stdout:is_closing() and stderr:is_closing()) do
|
||||
coroutine.yield()
|
||||
end
|
||||
else
|
||||
self.data = "Failed to spawn process " .. self.cmd .. " " .. vim.inspect(self.opts)
|
||||
end
|
||||
self:on_exit()
|
||||
end
|
||||
|
||||
function Process:on_exit()
|
||||
self.data = self.data:gsub("[^\r\n]+\r", "")
|
||||
if self.timedout then
|
||||
self.data = self.data .. "\n" .. "Process was killed because it reached the timeout"
|
||||
elseif self.signal ~= 0 then
|
||||
self.data = self.data .. "\n" .. "Process was killed with SIG" .. M.signals[self.signal]:upper()
|
||||
end
|
||||
if self.opts.on_exit then
|
||||
self.opts.on_exit(self.code == 0 and self.signal == 0, self.data)
|
||||
end
|
||||
end
|
||||
|
||||
function Process:guard()
|
||||
if self.opts.timeout then
|
||||
self.timeout = assert(uv.new_timer())
|
||||
self.timeout:start(self.opts.timeout, 0, function()
|
||||
self.timedout = true
|
||||
self:kill()
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
function Process:env()
|
||||
---@type table<string, string>
|
||||
local env = vim.tbl_extend("force", {
|
||||
GIT_SSH_COMMAND = "ssh -oBatchMode=yes",
|
||||
}, uv.os_environ(), opts.env or {})
|
||||
}, uv.os_environ(), self.opts.env or {})
|
||||
env.GIT_DIR = nil
|
||||
env.GIT_WORK_TREE = nil
|
||||
env.GIT_TERMINAL_PROMPT = "0"
|
||||
|
@ -72,147 +138,105 @@ function M.spawn(cmd, opts)
|
|||
for k, v in pairs(env) do
|
||||
env_flat[#env_flat + 1] = k .. "=" .. v
|
||||
end
|
||||
|
||||
local stdout = assert(uv.new_pipe())
|
||||
local stderr = assert(uv.new_pipe())
|
||||
|
||||
local output = ""
|
||||
---@type uv_process_t?
|
||||
local handle = nil
|
||||
|
||||
---@type uv_timer_t
|
||||
local timeout
|
||||
local killed = false
|
||||
if opts.timeout then
|
||||
timeout = assert(uv.new_timer())
|
||||
timeout:start(opts.timeout, 0, function()
|
||||
if M.kill(handle) then
|
||||
killed = true
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
-- make sure the cwd is valid
|
||||
if not opts.cwd and type(uv.cwd()) ~= "string" then
|
||||
opts.cwd = uv.os_homedir()
|
||||
end
|
||||
|
||||
handle = uv.spawn(cmd, {
|
||||
stdio = { nil, stdout, stderr },
|
||||
args = opts.args,
|
||||
cwd = opts.cwd,
|
||||
env = env_flat,
|
||||
}, function(exit_code, signal)
|
||||
---@cast handle uv_process_t
|
||||
M.running[handle] = nil
|
||||
if timeout then
|
||||
timeout:stop()
|
||||
timeout:close()
|
||||
end
|
||||
handle:close()
|
||||
stdout:close()
|
||||
stderr:close()
|
||||
local check = assert(uv.new_check())
|
||||
check:start(function()
|
||||
if not stdout:is_closing() or not stderr:is_closing() then
|
||||
return
|
||||
end
|
||||
check:stop()
|
||||
if opts.on_exit then
|
||||
output = output:gsub("[^\r\n]+\r", "")
|
||||
if killed then
|
||||
output = output .. "\n" .. "Process was killed because it reached the timeout"
|
||||
elseif signal ~= 0 then
|
||||
output = output .. "\n" .. "Process was killed with SIG" .. M.signals[signal]
|
||||
end
|
||||
|
||||
vim.schedule(function()
|
||||
opts.on_exit(exit_code == 0 and signal == 0, output)
|
||||
end)
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
if not handle then
|
||||
if opts.on_exit then
|
||||
opts.on_exit(false, "Failed to spawn process " .. cmd .. " " .. vim.inspect(opts))
|
||||
end
|
||||
return
|
||||
end
|
||||
M.running[handle] = true
|
||||
|
||||
---@param data? string
|
||||
local function on_output(err, data)
|
||||
assert(not err, err)
|
||||
|
||||
if data then
|
||||
if opts.on_data then
|
||||
vim.schedule(function()
|
||||
opts.on_data(data)
|
||||
end)
|
||||
end
|
||||
output = output .. data:gsub("\r\n", "\n")
|
||||
local lines = vim.split(vim.trim(output:gsub("\r$", "")):gsub("[^\n\r]+\r", ""), "\n")
|
||||
|
||||
if opts.on_line then
|
||||
vim.schedule(function()
|
||||
opts.on_line(lines[#lines])
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
uv.read_start(stdout, on_output)
|
||||
uv.read_start(stderr, on_output)
|
||||
|
||||
return handle
|
||||
return env_flat
|
||||
end
|
||||
|
||||
function M.kill(handle)
|
||||
if handle and not handle:is_closing() then
|
||||
M.running[handle] = nil
|
||||
uv.process_kill(handle, "sigint")
|
||||
return true
|
||||
---@param signals uv.aliases.signals|uv.aliases.signals[]|nil
|
||||
function Process:kill(signals)
|
||||
if not self.handle or self.handle:is_closing() then
|
||||
return
|
||||
end
|
||||
signals = signals or { "sigterm", "sigkill" }
|
||||
signals = type(signals) == "table" and signals or { signals }
|
||||
---@cast signals uv.aliases.signals[]
|
||||
local timer = assert(uv.new_timer())
|
||||
timer:start(0, 1000, function()
|
||||
if self.handle and not self.handle:is_closing() and #signals > 0 then
|
||||
self.handle:kill(table.remove(signals, 1))
|
||||
else
|
||||
timer:stop()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
---@param err? string
|
||||
---@param data? string
|
||||
---@param is_stderr? boolean
|
||||
function Process:on_data(err, data, is_stderr)
|
||||
assert(not err, err)
|
||||
if not data then
|
||||
return
|
||||
end
|
||||
|
||||
if self.opts.on_data then
|
||||
self.opts.on_data(data, is_stderr)
|
||||
end
|
||||
self.data = self.data .. data:gsub("\r\n", "\n")
|
||||
local lines = vim.split(vim.trim(self.data:gsub("\r$", "")):gsub("[^\n\r]+\r", ""), "\n")
|
||||
|
||||
if self.opts.on_line then
|
||||
self.opts.on_line(lines[#lines])
|
||||
end
|
||||
end
|
||||
|
||||
M.signals = {
|
||||
"hup",
|
||||
"int",
|
||||
"quit",
|
||||
"ill",
|
||||
"trap",
|
||||
"abrt",
|
||||
"bus",
|
||||
"fpe",
|
||||
"kill",
|
||||
"usr1",
|
||||
"segv",
|
||||
"usr2",
|
||||
"pipe",
|
||||
"alrm",
|
||||
"term",
|
||||
"chld",
|
||||
"cont",
|
||||
"stop",
|
||||
"tstp",
|
||||
"ttin",
|
||||
"ttou",
|
||||
"urg",
|
||||
"xcpu",
|
||||
"xfsz",
|
||||
"vtalrm",
|
||||
"prof",
|
||||
"winch",
|
||||
"io",
|
||||
"pwr",
|
||||
"emt",
|
||||
"sys",
|
||||
"info",
|
||||
}
|
||||
|
||||
---@param cmd string|string[]
|
||||
---@param opts? ProcessOpts
|
||||
function M.spawn(cmd, opts)
|
||||
return Process.new(cmd, opts)
|
||||
end
|
||||
|
||||
function M.abort()
|
||||
for handle in pairs(M.running) do
|
||||
M.kill(handle)
|
||||
for _, proc in pairs(M.running) do
|
||||
proc:kill()
|
||||
end
|
||||
end
|
||||
|
||||
---@param cmd string[]
|
||||
---@param opts? {cwd:string, env:table}
|
||||
---@async
|
||||
---@param cmd string|string[]
|
||||
---@param opts? ProcessOpts
|
||||
function M.exec(cmd, opts)
|
||||
opts = opts or {}
|
||||
---@type string[]
|
||||
local lines
|
||||
local job = vim.fn.jobstart(cmd, {
|
||||
cwd = opts.cwd,
|
||||
pty = false,
|
||||
env = opts.env,
|
||||
stdout_buffered = true,
|
||||
on_stdout = function(_, _lines)
|
||||
lines = _lines
|
||||
end,
|
||||
})
|
||||
|
||||
if job <= 0 then
|
||||
error("Failed to start job: " .. vim.inspect(cmd))
|
||||
local proc = M.spawn(cmd, opts)
|
||||
proc:wait()
|
||||
if proc.code ~= 0 then
|
||||
error("Process failed with code " .. proc.code)
|
||||
end
|
||||
|
||||
local Async = require("lazy.async")
|
||||
local async = Async.current
|
||||
if async then
|
||||
while vim.fn.jobwait({ job }, 0)[1] == -1 do
|
||||
async:sleep(10)
|
||||
end
|
||||
else
|
||||
vim.fn.jobwait({ job })
|
||||
end
|
||||
|
||||
return lines
|
||||
return vim.split(proc.data, "\n")
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
|
@ -16,7 +16,6 @@ local Task = require("lazy.manage.task")
|
|||
---@class Runner
|
||||
---@field _plugins table<string,LazyPlugin>
|
||||
---@field _pipeline PipelineStep[]
|
||||
---@field _on_done fun()[]
|
||||
---@field _opts RunnerOpts
|
||||
---@field _running? Async
|
||||
local Runner = {}
|
||||
|
@ -38,7 +37,6 @@ function Runner.new(opts)
|
|||
for _, plugin in ipairs(pp) do
|
||||
self._plugins[plugin.name] = plugin
|
||||
end
|
||||
self._on_done = {}
|
||||
|
||||
---@param step string|(TaskOptions|{[1]:string})
|
||||
self._pipeline = vim.tbl_map(function(step)
|
||||
|
@ -61,15 +59,9 @@ end
|
|||
|
||||
function Runner:start()
|
||||
---@async
|
||||
self._running = Async.run(function()
|
||||
self._running = Async.new(function()
|
||||
self:_start()
|
||||
end, {
|
||||
on_done = function()
|
||||
for _, cb in ipairs(self._on_done) do
|
||||
cb()
|
||||
end
|
||||
end,
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
---@async
|
||||
|
@ -97,7 +89,7 @@ function Runner:_start()
|
|||
for _, name in ipairs(names) do
|
||||
state[name] = state[name] or { step = 0 }
|
||||
local s = state[name]
|
||||
local is_running = s.task and s.task:is_running()
|
||||
local is_running = s.task and s.task:running()
|
||||
local step = self._pipeline[s.step]
|
||||
|
||||
if is_running then
|
||||
|
@ -185,14 +177,10 @@ function Runner:wait(cb)
|
|||
end
|
||||
return self
|
||||
end
|
||||
|
||||
if cb then
|
||||
table.insert(self._on_done, cb)
|
||||
self._running:on("done", cb)
|
||||
else
|
||||
-- sync wait
|
||||
while self:is_running() do
|
||||
vim.wait(10)
|
||||
end
|
||||
self._running:wait()
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
|
|
@ -15,16 +15,15 @@ local colors = Config.options.headless.colors
|
|||
---@field msg string
|
||||
---@field level? number
|
||||
|
||||
---@class LazyTask
|
||||
---@class LazyTask: Async
|
||||
---@field plugin LazyPlugin
|
||||
---@field name string
|
||||
---@field private _log LazyMsg[]
|
||||
---@field private _started? number
|
||||
---@field private _started number
|
||||
---@field private _ended? number
|
||||
---@field private _opts TaskOptions
|
||||
---@field private _running Async
|
||||
---@field private _level number
|
||||
local Task = {}
|
||||
local Task = setmetatable({}, { __index = Async.Async })
|
||||
|
||||
---@class TaskOptions: {[string]:any}
|
||||
---@field on_done? fun(task:LazyTask)
|
||||
|
@ -35,17 +34,21 @@ local Task = {}
|
|||
---@param task LazyTaskFn
|
||||
function Task.new(plugin, name, task, opts)
|
||||
local self = setmetatable({}, { __index = Task })
|
||||
---@async
|
||||
Task.init(self, function()
|
||||
self:_run(task)
|
||||
end)
|
||||
self:set_level()
|
||||
self._opts = opts or {}
|
||||
self._log = {}
|
||||
self:set_level()
|
||||
self.plugin = plugin
|
||||
self.name = name
|
||||
self._started = vim.uv.hrtime()
|
||||
---@param other LazyTask
|
||||
plugin._.tasks = vim.tbl_filter(function(other)
|
||||
return other.name ~= name or other:is_running()
|
||||
return other.name ~= name or other:running()
|
||||
end, plugin._.tasks or {})
|
||||
table.insert(plugin._.tasks, self)
|
||||
self:_start(task)
|
||||
return self
|
||||
end
|
||||
|
||||
|
@ -75,10 +78,6 @@ function Task:status()
|
|||
return msg ~= "" and msg or nil
|
||||
end
|
||||
|
||||
function Task:is_running()
|
||||
return self._ended == nil
|
||||
end
|
||||
|
||||
function Task:has_errors()
|
||||
return self._level >= vim.log.levels.ERROR
|
||||
end
|
||||
|
@ -92,31 +91,24 @@ function Task:set_level(level)
|
|||
self._level = level or vim.log.levels.TRACE
|
||||
end
|
||||
|
||||
---@private
|
||||
---@async
|
||||
---@param task LazyTaskFn
|
||||
function Task:_start(task)
|
||||
assert(not self._started, "task already started")
|
||||
assert(not self._ended, "task already done")
|
||||
|
||||
function Task:_run(task)
|
||||
if Config.headless() and Config.options.headless.task then
|
||||
self:log("Running task " .. self.name, vim.log.levels.INFO)
|
||||
end
|
||||
|
||||
self._started = vim.uv.hrtime()
|
||||
---@async
|
||||
self._running = Async.run(function()
|
||||
task(self, self._opts)
|
||||
end, {
|
||||
on_done = function()
|
||||
self
|
||||
:on("done", function()
|
||||
self:_done()
|
||||
end,
|
||||
on_error = function(err)
|
||||
end)
|
||||
:on("error", function(err)
|
||||
self:error(err)
|
||||
end,
|
||||
on_yield = function(res)
|
||||
self:log(res)
|
||||
end,
|
||||
})
|
||||
end)
|
||||
:on("yield", function(msg)
|
||||
self:log(msg)
|
||||
end)
|
||||
task(self, self._opts)
|
||||
end
|
||||
|
||||
---@param msg string|string[]
|
||||
|
@ -163,13 +155,6 @@ end
|
|||
|
||||
---@private
|
||||
function Task:_done()
|
||||
assert(self._started, "task not started")
|
||||
assert(not self._ended, "task already done")
|
||||
|
||||
if self._running and self._running:running() then
|
||||
return
|
||||
end
|
||||
|
||||
if Config.headless() and Config.options.headless.task then
|
||||
local ms = math.floor(self:time() + 0.5)
|
||||
self:log("Finished task " .. self.name .. " in " .. ms .. "ms", vim.log.levels.INFO)
|
||||
|
@ -186,13 +171,7 @@ function Task:_done()
|
|||
end
|
||||
|
||||
function Task:time()
|
||||
if not self._started then
|
||||
return 0
|
||||
end
|
||||
if not self._ended then
|
||||
return (vim.uv.hrtime() - self._started) / 1e6
|
||||
end
|
||||
return (self._ended - self._started) / 1e6
|
||||
return ((self._ended or vim.uv.hrtime()) - self._started) / 1e6
|
||||
end
|
||||
|
||||
---@async
|
||||
|
@ -201,7 +180,6 @@ end
|
|||
function Task:spawn(cmd, opts)
|
||||
opts = opts or {}
|
||||
local on_line = opts.on_line
|
||||
local on_exit = opts.on_exit
|
||||
|
||||
local headless = Config.headless() and Config.options.headless.process
|
||||
|
||||
|
@ -214,35 +192,28 @@ function Task:spawn(cmd, opts)
|
|||
end
|
||||
end
|
||||
|
||||
self._running:suspend()
|
||||
|
||||
local running = true
|
||||
local ret = { ok = true, output = "" }
|
||||
---@param output string
|
||||
function opts.on_exit(ok, output)
|
||||
if not headless then
|
||||
self:log(vim.trim(output), ok and vim.log.levels.DEBUG or vim.log.levels.ERROR)
|
||||
end
|
||||
ret = { ok = ok, output = output }
|
||||
running = false
|
||||
self._running:resume()
|
||||
end
|
||||
|
||||
if headless then
|
||||
opts.on_data = function(data)
|
||||
-- prefix with plugin name
|
||||
local prefix = self:prefix()
|
||||
io.write(Terminal.prefix(data, prefix))
|
||||
io.write(Terminal.prefix(data, self:prefix()))
|
||||
end
|
||||
end
|
||||
Process.spawn(cmd, opts)
|
||||
coroutine.yield()
|
||||
assert(not running, "process still running?")
|
||||
if on_exit then
|
||||
pcall(on_exit, ret.ok, ret.output)
|
||||
|
||||
local proc = Process.spawn(cmd, opts)
|
||||
proc:wait()
|
||||
|
||||
local ok = proc.code == 0 and proc.signal == 0
|
||||
if not headless then
|
||||
local msg = vim.trim(proc.data)
|
||||
if #msg > 0 then
|
||||
self:log(vim.trim(proc.data), ok and vim.log.levels.DEBUG or vim.log.levels.ERROR)
|
||||
end
|
||||
coroutine.yield()
|
||||
return ret.ok
|
||||
end
|
||||
|
||||
if opts.on_exit then
|
||||
pcall(opts.on_exit, ok, proc.data)
|
||||
end
|
||||
return ok
|
||||
end
|
||||
|
||||
function Task:prefix()
|
||||
|
@ -253,10 +224,4 @@ function Task:prefix()
|
|||
or plugin .. " " .. task .. " | "
|
||||
end
|
||||
|
||||
function Task:wait()
|
||||
while self:is_running() do
|
||||
vim.wait(10)
|
||||
end
|
||||
end
|
||||
|
||||
return Task
|
||||
|
|
|
@ -76,6 +76,13 @@ function M.throttle(ms, fn)
|
|||
local timer = vim.uv.new_timer()
|
||||
local pending = false
|
||||
|
||||
---@type Async
|
||||
local async
|
||||
|
||||
local function running()
|
||||
return async and async:running()
|
||||
end
|
||||
|
||||
return function()
|
||||
if timer:is_active() then
|
||||
pending = true
|
||||
|
@ -85,7 +92,10 @@ function M.throttle(ms, fn)
|
|||
0,
|
||||
ms,
|
||||
vim.schedule_wrap(function()
|
||||
fn()
|
||||
if running() then
|
||||
return
|
||||
end
|
||||
async = require("lazy.async").new(fn)
|
||||
if pending then
|
||||
pending = false
|
||||
else
|
||||
|
|
|
@ -51,7 +51,7 @@ function M:update()
|
|||
if plugin._.tasks then
|
||||
for _, task in ipairs(plugin._.tasks) do
|
||||
self.progress.total = self.progress.total + 1
|
||||
if not task:is_running() then
|
||||
if not task:running() then
|
||||
self.progress.done = self.progress.done + 1
|
||||
end
|
||||
end
|
||||
|
@ -356,7 +356,7 @@ end
|
|||
function M:diagnostics(plugin)
|
||||
local skip = false
|
||||
for _, task in ipairs(plugin._.tasks or {}) do
|
||||
if task:is_running() then
|
||||
if task:running() then
|
||||
self:diagnostic({
|
||||
severity = vim.diagnostic.severity.WARN,
|
||||
message = task.name .. (task:status() and (": " .. task:status()) or ""),
|
||||
|
|
|
@ -28,7 +28,7 @@ return {
|
|||
return true
|
||||
end
|
||||
return has_task(plugin, function(task)
|
||||
return task:is_running()
|
||||
return task:running()
|
||||
end)
|
||||
end,
|
||||
title = "Working",
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
---@module 'luassert'
|
||||
local Async = require("lazy.async")
|
||||
local Process = require("lazy.manage.process")
|
||||
|
||||
describe("process", function()
|
||||
it("runs sync", function()
|
||||
local lines = Process.exec({ "echo", "-n", "hello" })
|
||||
assert.are.same({ "hello" }, lines)
|
||||
end)
|
||||
|
||||
it("runs sync from async context", function()
|
||||
local lines ---@type string[]
|
||||
local async = Async.new(function()
|
||||
lines = Process.exec({ "echo", "-n", "hello" })
|
||||
end)
|
||||
async:wait()
|
||||
|
||||
assert.are.same({ "hello" }, lines)
|
||||
end)
|
||||
end)
|
|
@ -21,9 +21,9 @@ describe("task", function()
|
|||
|
||||
it("simple function", function()
|
||||
local task = Task.new(plugin, "test", function() end, opts)
|
||||
assert(task:is_running())
|
||||
assert(task:running())
|
||||
task:wait()
|
||||
assert(not task:is_running())
|
||||
assert(not task:running())
|
||||
assert(task_result.done)
|
||||
end)
|
||||
|
||||
|
@ -31,9 +31,9 @@ describe("task", function()
|
|||
local task = Task.new(plugin, "test", function()
|
||||
error("test")
|
||||
end, opts)
|
||||
assert(task:is_running())
|
||||
assert(task:running())
|
||||
task:wait()
|
||||
assert(not task:is_running())
|
||||
assert(not task:running())
|
||||
assert(task_result.done)
|
||||
assert(task_result.error)
|
||||
assert(task:has_errors() and task:output(vim.log.levels.ERROR):find("test"))
|
||||
|
@ -46,12 +46,12 @@ describe("task", function()
|
|||
coroutine.yield()
|
||||
running = false
|
||||
end, opts)
|
||||
assert(task:is_running())
|
||||
assert(task:running())
|
||||
assert(running)
|
||||
assert(task:is_running())
|
||||
assert(task:running())
|
||||
task:wait()
|
||||
assert(not running)
|
||||
assert(not task:is_running())
|
||||
assert(not task:running())
|
||||
assert(task_result.done)
|
||||
assert(not task:has_errors())
|
||||
end)
|
||||
|
@ -60,19 +60,19 @@ describe("task", function()
|
|||
local task = Task.new(plugin, "spawn_errors", function(task)
|
||||
task:spawn("foobar")
|
||||
end, opts)
|
||||
assert(task:is_running())
|
||||
assert(task:running())
|
||||
task:wait()
|
||||
assert(not task:is_running())
|
||||
assert(not task:running())
|
||||
assert(task_result.done)
|
||||
assert(task:has_errors() and task:output(vim.log.levels.ERROR):find("Failed to spawn"), task.output)
|
||||
assert(task:has_errors() and task:output(vim.log.levels.ERROR):find("Failed to spawn"), task:output())
|
||||
end)
|
||||
|
||||
it("spawn", function()
|
||||
local task = Task.new(plugin, "test", function(task)
|
||||
task:spawn("echo", { args = { "foo" } })
|
||||
end, opts)
|
||||
assert(task:is_running())
|
||||
assert(task:is_running())
|
||||
assert(task:running())
|
||||
assert(task:running())
|
||||
task:wait()
|
||||
assert.same(task:output(), "foo")
|
||||
assert(task_result.done)
|
||||
|
@ -84,8 +84,8 @@ describe("task", function()
|
|||
task:spawn("echo", { args = { "foo" } })
|
||||
task:spawn("echo", { args = { "bar" } })
|
||||
end, opts)
|
||||
assert(task:is_running())
|
||||
assert(task:is_running())
|
||||
assert(task:running())
|
||||
assert(task:running())
|
||||
task:wait()
|
||||
assert(task:output() == "foo\nbar" or task:output() == "bar\nfoo", task:output())
|
||||
assert(task_result.done)
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
#!/bin/sh
|
||||
|
||||
nvim -l tests/busted.lua tests -o utfTerminal
|
||||
nvim -l tests/busted.lua tests -o utfTerminal "$@"
|
||||
|
|
42
vim.toml
42
vim.toml
|
@ -8,42 +8,14 @@ any = true
|
|||
[jit]
|
||||
any = true
|
||||
|
||||
[[describe.args]]
|
||||
type = "string"
|
||||
[[describe.args]]
|
||||
type = "function"
|
||||
|
||||
[[it.args]]
|
||||
type = "string"
|
||||
[[it.args]]
|
||||
type = "function"
|
||||
|
||||
[[before_each.args]]
|
||||
type = "function"
|
||||
[[after_each.args]]
|
||||
type = "function"
|
||||
|
||||
[assert.is_not]
|
||||
[assert]
|
||||
any = true
|
||||
|
||||
[[assert.equals.args]]
|
||||
type = "any"
|
||||
[[assert.equals.args]]
|
||||
type = "any"
|
||||
[[assert.equals.args]]
|
||||
type = "any"
|
||||
required = false
|
||||
[describe]
|
||||
any = true
|
||||
|
||||
[[assert.same.args]]
|
||||
type = "any"
|
||||
[[assert.same.args]]
|
||||
type = "any"
|
||||
[it]
|
||||
any = true
|
||||
|
||||
[[assert.truthy.args]]
|
||||
type = "any"
|
||||
|
||||
[[assert.spy.args]]
|
||||
type = "any"
|
||||
|
||||
[[assert.stub.args]]
|
||||
type = "any"
|
||||
[before_each.args]
|
||||
any = true
|
||||
|
|
Loading…
Reference in New Issue