-
-
Notifications
You must be signed in to change notification settings - Fork 186
/
async.lua
191 lines (162 loc) · 4.86 KB
/
async.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
-- Order by highest number of return types
local M = {}
local Async_T = {}
-- Handle for an object currently running on the event loop.
-- The coroutine is paused while this is active.
-- Must provide methods cancel() and is_cancelled()
--
-- Handle gets updated on each call to a wrapped functions, so provide access
-- to it via a proxy
-- 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.
-- Store all the async threads in a weak table so we don't prevent them from
-- being garbage collected
--- @type table<thread,uv.uv_handle_t>
local handles = setmetatable({}, { __mode = 'k' })
--- Returns whether the current execution context is async.
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
end
end
-- 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
end
function Async_T.new(co)
local handle = setmetatable({}, { __index = Async_T })
handles[co] = handle
return handle
end
-- Analogous to uv.is_closing
function Async_T:is_cancelled()
return self._current and self._current:is_cancelled()
end
local function run(func, callback, ...)
local co = coroutine.create(func)
local handle = Async_T.new(co)
local function step(...)
local ret = { coroutine.resume(co, ...) }
local stat = ret[1]
if not stat then
local err = ret[2] --[[@as string]]
error(
string.format('The coroutine failed with this message: %s\n%s', err, debug.traceback(co))
)
end
if coroutine.status(co) == 'dead' then
if callback then
callback(unpack(ret, 2, table.maxn(ret)))
end
return
end
--- @type integer, function
local nargs, fn = ret[2], ret[3]
assert(type(fn) == 'function', 'type error :: expected func')
local args = { select(4, unpack(ret)) }
args[nargs] = step
local r = fn(unpack(args, 1, nargs))
if is_Async_T(r) then
handle._current = r
end
end
step(...)
return handle
end
function M.wait(argc, func, ...)
-- 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 = { ... } --- @type any[]
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
---Creates an async function with a callback style function.
---@param func function: A callback style function to be converted. The last argument must be the callback.
---@param argc number: The number of arguments of func. Must be included.
---@return function: Returns an async function
function M.wrap(func, argc)
assert(argc)
return function(...)
if not M.running() then
return func(...)
end
return M.wait(argc, func, ...)
end
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
---@generic F: function
---@param func F
---@param argc integer
---@return F
function M.create(func, argc)
argc = argc or 0
return function(...)
if M.running() then
return func(...)
end
local callback = select(argc + 1, ...)
return run(func, callback, unpack({ ... }, 1, argc))
end
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
---@generic F: function
---@param func F
---@return async F
function M.void(func)
return function(...)
if M.running() then
return func(...)
end
return run(func, nil, ...)
end
end
---An async function that when called will yield to the Neovim scheduler to be
---able to call the API.
M.scheduler = M.wrap(vim.schedule, 1)
--- @param buf? integer
M.scheduler_if_buf_valid = M.wrap(function(buf, cb)
vim.schedule(function()
if not buf or vim.api.nvim_buf_is_valid(buf) then
cb()
end
end)
end, 2)
return M