2024-06-26 14:11:31 +01:00
|
|
|
---@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
|
2024-06-27 12:06:39 +01:00
|
|
|
---@type Async
|
|
|
|
M.current = nil
|
2024-06-26 14:11:31 +01:00
|
|
|
|
|
|
|
---@class Async
|
|
|
|
---@field co thread
|
|
|
|
---@field opts AsyncOpts
|
2024-06-27 12:06:39 +01:00
|
|
|
---@field sleeping? boolean
|
2024-06-26 14:11:31 +01:00
|
|
|
local Async = {}
|
|
|
|
|
|
|
|
---@param fn async fun()
|
|
|
|
---@param opts? AsyncOpts
|
|
|
|
---@return Async
|
|
|
|
function Async.new(fn, opts)
|
|
|
|
local self = setmetatable({}, { __index = Async })
|
|
|
|
self.co = coroutine.create(fn)
|
|
|
|
self.opts = opts or {}
|
|
|
|
return self
|
|
|
|
end
|
|
|
|
|
|
|
|
function Async:running()
|
|
|
|
return coroutine.status(self.co) ~= "dead"
|
|
|
|
end
|
|
|
|
|
2024-06-27 12:06:39 +01:00
|
|
|
function Async:sleep(ms)
|
|
|
|
self.sleeping = true
|
|
|
|
vim.defer_fn(function()
|
|
|
|
self.sleeping = false
|
|
|
|
end, ms)
|
2024-06-27 23:35:38 +01:00
|
|
|
coroutine.yield()
|
2024-06-27 12:06:39 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
function Async:suspend()
|
|
|
|
self.sleeping = true
|
|
|
|
end
|
|
|
|
|
|
|
|
function Async:resume()
|
|
|
|
self.sleeping = false
|
|
|
|
end
|
|
|
|
|
2024-06-26 14:11:31 +01:00
|
|
|
function Async:step()
|
2024-06-27 12:06:39 +01:00
|
|
|
if self.sleeping then
|
|
|
|
return true
|
|
|
|
end
|
2024-06-26 14:11:31 +01:00
|
|
|
local status = coroutine.status(self.co)
|
|
|
|
if status == "suspended" then
|
2024-06-27 12:06:39 +01:00
|
|
|
M.current = self
|
2024-06-26 14:11:31 +01:00
|
|
|
local ok, res = coroutine.resume(self.co)
|
2024-06-27 12:06:39 +01:00
|
|
|
M.current = nil
|
2024-06-26 14:11:31 +01:00
|
|
|
if not ok then
|
|
|
|
if self.opts.on_error then
|
|
|
|
self.opts.on_error(tostring(res))
|
|
|
|
end
|
|
|
|
elseif res then
|
2024-06-27 13:43:35 +01:00
|
|
|
if self.opts.on_yield then
|
2024-06-26 14:11:31 +01:00
|
|
|
self.opts.on_yield(res)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
if self:running() then
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
if self.opts.on_done then
|
|
|
|
self.opts.on_done()
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
function M.step()
|
|
|
|
M._running = true
|
|
|
|
local budget = 1 * 1e6
|
|
|
|
local start = vim.loop.hrtime()
|
|
|
|
local count = #M._queue
|
|
|
|
local i = 0
|
|
|
|
while #M._queue > 0 and vim.loop.hrtime() - start < budget do
|
|
|
|
---@type Async
|
|
|
|
local state = table.remove(M._queue, 1)
|
|
|
|
if state:step() then
|
|
|
|
table.insert(M._queue, state)
|
|
|
|
end
|
|
|
|
i = i + 1
|
|
|
|
if i >= count then
|
|
|
|
break
|
|
|
|
end
|
|
|
|
end
|
|
|
|
M._running = false
|
|
|
|
if #M._queue == 0 then
|
|
|
|
return M._executor:stop()
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
---@param async Async
|
|
|
|
function M.add(async)
|
|
|
|
table.insert(M._queue, async)
|
|
|
|
if not M._executor:is_active() then
|
|
|
|
M._executor:start(vim.schedule_wrap(M.step))
|
|
|
|
end
|
|
|
|
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)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2024-06-27 13:43:35 +01:00
|
|
|
---@async
|
|
|
|
---@param ms number
|
|
|
|
function M.sleep(ms)
|
|
|
|
assert(M.current, "Not in an async context")
|
|
|
|
M.current:sleep(ms)
|
|
|
|
end
|
|
|
|
|
2024-06-26 14:11:31 +01:00
|
|
|
return M
|