Skip to content

test: new simplier/easier async module! #264

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lua/gitlinker.lua
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ local _link = function(opts)
lk.rev = opts.rev
end

async.scheduler()
async.schedule()
local ok, url = pcall(opts.router, lk, true)
-- logger:debug(
-- "|link| ok:%s, url:%s, router:%s",
Expand Down
285 changes: 73 additions & 212 deletions lua/gitlinker/async.lua
Original file line number Diff line number Diff line change
@@ -1,239 +1,100 @@
---@diagnostic disable: luadoc-miss-module-name, undefined-doc-name
--- Small async library for Neovim plugins
--- @module async
-- Store all the async threads in a weak table so we don't prevent them from
-- being garbage collected
local handles = setmetatable({}, { __mode = "k" })
local M = {}
-- Note: coroutine.running() was changed between Lua 5.1 and 5.2:
-- - 5.1: Returns the running coroutine, or nil when called by the main thread.
-- - 5.2: Returns the running coroutine plus a boolean, true when the running
-- coroutine is the main one.
--
-- For LuaJIT, 5.2 behaviour is enabled with LUAJIT_ENABLE_LUA52COMPAT
--
-- We need to handle both.
--- Returns whether the current execution context is async.
---
--- @treturn boolean?
function M.running()
local current = coroutine.running()
if current and handles[current] then
return true
end
end
local function is_Async_T(handle)
if
handle
and type(handle) == "table"
and vim.is_callable(handle.cancel)
and vim.is_callable(handle.is_cancelled)
then
return true
-- Copied from: <https://github.com/neovim/neovim/issues/19624#issuecomment-1202405058>

local co = coroutine

local async_thread = {
threads = {},
}

local function threadtostring(x)
if jit then
return string.format("%p", x)
else
return tostring(x):match("thread: (.*)")
end
end
local Async_T = {}
-- Analogous to uv.close
function Async_T:cancel(cb)
-- Cancel anything running on the event loop
if self._current and not self._current:is_cancelled() then
self._current:cancel(cb)
end

function async_thread.running()
local thread = co.running()
local id = threadtostring(thread)
return async_thread.threads[id]
end
function Async_T.new(co)
local handle = setmetatable({}, { __index = Async_T })
handles[co] = handle
return handle

function async_thread.create(fn)
local thread = co.create(fn)
local id = threadtostring(thread)
async_thread.threads[id] = true
return thread
end
-- Analogous to uv.is_closing
function Async_T:is_cancelled()
return self._current and self._current:is_cancelled()

function async_thread.finished(x)
if co.status(x) == "dead" then
local id = threadtostring(x)
async_thread.threads[id] = nil
return true
end
return false
end
--- Run a function in an async context.
--- @tparam function func
--- @tparam function callback
--- @tparam any ... Arguments for func
--- @treturn async_t Handle
function M.run(func, callback, ...)
vim.validate({
func = { func, "function" },
callback = { callback, "function", true },
})
local co = coroutine.create(func)
local handle = Async_T.new(co)

--- @param async_fn function
--- @param ... any
local function execute(async_fn, ...)
local thread = async_thread.create(async_fn)

local function step(...)
local ret = { coroutine.resume(co, ...) }
local ok = ret[1]
if not ok then
local err = ret[2]
local ret = { co.resume(thread, ...) }
local stat, err_or_fn, nargs = unpack(ret)

if not stat then
error(
string.format("The coroutine failed with this message:\n%s\n%s", err, debug.traceback(co))
string.format(
"The coroutine failed with this message: %s\n%s",
err_or_fn,
debug.traceback(thread)
)
)
end
if coroutine.status(co) == "dead" then
if callback then
callback(unpack(ret, 4, table.maxn(ret)))
end

if async_thread.finished(thread) then
return
end
local nargs, fn = ret[2], ret[3]

assert(type(err_or_fn) == "function", "The 1st parameter must be a lua function")

local ret_fn = err_or_fn
local args = { select(4, unpack(ret)) }
assert(type(fn) == "function", "type error :: expected func")
args[nargs] = step
local r = fn(unpack(args, 1, nargs))
if is_Async_T(r) then
handle._current = r
end
ret_fn(unpack(args, 1, nargs --[[@as integer]]))
end

step(...)
return handle
end
local function wait(argc, func, ...)
vim.validate({
argc = { argc, "number" },
func = { func, "function" },
})
-- Always run the wrapped functions in xpcall and re-raise the error in the
-- coroutine. This makes pcall work as normal.
local function pfunc(...)
local args = { ... }
local cb = args[argc]
args[argc] = function(...)
cb(true, ...)
end
xpcall(func, function(err)
cb(false, err, debug.traceback())
end, unpack(args, 1, argc))
end
local ret = { coroutine.yield(argc, pfunc, ...) }
local ok = ret[1]
if not ok then
local _, err, traceback = unpack(ret)
error(string.format("Wrapped function failed: %s\n%s", err, traceback))
end
return unpack(ret, 2, table.maxn(ret))
end
--- Wait on a callback style function
---
--- @tparam integer? argc The number of arguments of func.
--- @tparam function func callback style function to execute
--- @tparam any ... Arguments for func
function M.wait(...)
if type(select(1, ...)) == "number" then
return wait(...)
end
-- Assume argc is equal to the number of passed arguments.
return wait(select("#", ...) - 1, ...)
end
--- Use this to create a function which executes in an async context but
--- called from a non-async context. Inherently this cannot return anything
--- since it is non-blocking
--- @tparam function func
--- @tparam number argc The number of arguments of func. Defaults to 0
--- @tparam boolean strict Error when called in non-async context
--- @treturn function(...):async_t
function M.create(func, argc, strict)
vim.validate({
func = { func, "function" },
argc = { argc, "number", true },
})
argc = argc or 0
return function(...)
if M.running() then
if strict then
error("This function must run in a non-async context")
end
return func(...)
end
local callback = select(argc + 1, ...)
return M.run(func, callback, unpack({ ... }, 1, argc))
end
end
--- Create a function which executes in an async context but
--- called from a non-async context.
--- @tparam function func
--- @tparam boolean strict Error when called in non-async context
function M.void(func, strict)
vim.validate({ func = { func, "function" } })

local M = {}

--- @param func function
--- @param argc integer
--- @return function
M.wrap = function(func, argc)
return function(...)
if M.running() then
if strict then
error("This function must run in a non-async context")
end
if not async_thread.running() then
return func(...)
end
return M.run(func, nil, ...)
return co.yield(func, argc, ...)
end
end
--- Creates an async function with a callback style function.
---
--- @tparam function func A callback style function to be converted. The last argument must be the callback.
--- @tparam integer argc The number of arguments of func. Must be included.
--- @tparam boolean strict Error when called in non-async context
--- @treturn function Returns an async function
function M.wrap(func, argc, strict)
vim.validate({
argc = { argc, "number" },
})

--- @param func function
--- @return function
M.void = function(func)
return function(...)
if not M.running() then
if strict then
error("This function must run in an async context")
end
if async_thread.running() then
return func(...)
end
return M.wait(argc, func, ...)
end
end
--- Run a collection of async functions (`thunks`) concurrently and return when
--- all have finished.
--- @tparam function[] thunks
--- @tparam integer n Max number of thunks to run concurrently
--- @tparam function interrupt_check Function to abort thunks between calls
function M.join(thunks, n, interrupt_check)
local function run(finish)
if #thunks == 0 then
return finish()
end
local remaining = { select(n + 1, unpack(thunks)) }
local to_go = #thunks
local ret = {}
local function cb(...)
ret[#ret + 1] = { ... }
to_go = to_go - 1
if to_go == 0 then
finish(ret)
elseif not interrupt_check or not interrupt_check() then
if #remaining > 0 then
local next_task = table.remove(remaining)
next_task(cb)
end
end
end
for i = 1, math.min(n, #thunks) do
thunks[i](cb)
end
end
if not M.running() then
return run
end
return M.wait(1, false, run)
end
--- Partially applying arguments to an async function
--- @tparam function fn
--- @param ... arguments to apply to `fn`
function M.curry(fn, ...)
local args = { ... }
local nargs = select("#", ...)
return function(...)
local other = { ... }
for i = 1, select("#", ...) do
args[nargs + i] = other[i]
end
fn(unpack(args))
execute(func, ...)
end
end
--- An async function that when called will yield to the Neovim scheduler to be
--- able to call the neovim API.
M.scheduler = M.wrap(vim.schedule, 1, false)

M.schedule = M.wrap(vim.schedule, 1)

return M
4 changes: 2 additions & 2 deletions lua/gitlinker/linker.lua
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ local function make_linker(remote, file, rev)
end
-- logger.debug("|linker - Linker:make| rev:%s", vim.inspect(rev))

async.scheduler()
async.schedule()

if not file_provided then
local buf_path_on_root = path.buffer_relpath(root) --[[@as string]]
Expand All @@ -114,7 +114,7 @@ local function make_linker(remote, file, rev)
-- vim.inspect(file_in_rev_result)
-- )

async.scheduler()
async.schedule()

local file_changed = false
if not file_provided then
Expand Down
Loading