aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim/async.lua
diff options
context:
space:
mode:
Diffstat (limited to 'runtime/lua/vim/async.lua')
-rw-r--r--runtime/lua/vim/async.lua317
1 files changed, 317 insertions, 0 deletions
diff --git a/runtime/lua/vim/async.lua b/runtime/lua/vim/async.lua
new file mode 100644
index 0000000000..8483b45add
--- /dev/null
+++ b/runtime/lua/vim/async.lua
@@ -0,0 +1,317 @@
+local core = require('vim.async._core')
+local validate = vim.validate
+local new_queue = require('vim.async._queue')
+local runtime = require('vim.async._runtime')
+local F = vim.F
+
+--- Structured async API for Lua code that waits on event-loop work.
+---
+--- `vim.async` lets Lua code wait for timers, callbacks, and other tasks
+--- without blocking Nvim's event loop. Async work runs inside tasks, which can
+--- pause at checkpoints and manage child tasks created while they are running.
+---
+--- Start async work with [vim.async.run()]. Inside a task, use
+--- [vim.async.await()] to wait for callback-style APIs or other tasks without
+--- blocking the event loop. Use [vim.async.pawait()] when an awaited operation
+--- can fail and the current task should continue.
+---
+--- Examples in this help use `local async = vim.async` for brevity.
+---
+--- Example: run async work without blocking Nvim:
+---
+--- ```lua
+--- local async = vim.async
+---
+--- async.run(function()
+--- vim.notify('waiting...')
+--- async.sleep(1000)
+--- vim.notify('done')
+--- end)
+--- ```
+---
+--- Example: await a callback-style API. Callback results are returned
+--- unchanged, so an error-first callback still returns `err, value`:
+---
+--- ```lua
+--- local async = vim.async
+---
+--- async.run(function()
+--- local err, stat = async.await(2, fs_stat, 'notes.txt')
+--- if err then
+--- error(err, 0)
+--- end
+--- print(('notes.txt is %d bytes'):format(stat.size))
+--- end)
+--- ```
+---
+--- A task has two roles:
+---
+--- - it is a handle that can be awaited, waited for, or closed
+--- - it is a scope for child tasks created while the task is running
+---
+--- [vim.async.run()] creates a task. A top-level task starts immediately. A
+--- task created while another task is running becomes a child of that task, and
+--- its function starts when the parent reaches its next checkpoint. A parent
+--- task finishes only after its attached children finish. If a child fails
+--- without being handled, the parent fails and closes the remaining children.
+---
+--- Use [Task:detach()] for background work that should keep running after the
+--- current task finishes. A detached task becomes top-level work; the original
+--- parent no longer waits for it or closes it.
+---
+--- Awaiting a task observes that task's result; it does not attach the task to
+--- the awaiter or change which task owns it. Ownership is decided when the task
+--- is created.
+---
+--- Scheduling is cooperative. When a task awaits a timer, I/O operation,
+--- callback, or another task, `vim.async` saves the Lua stack and returns
+--- control to the event loop. Other callbacks can run while the task is paused.
+--- Nothing interrupts synchronous Lua code in the middle of a stack frame.
+---
+--- Checkpoints are the places where a task can pause, start pending child
+--- tasks, observe cancellation, and receive unhandled child failures. Inside a
+--- task, these operations are checkpoints:
+---
+--- - `vim.async.await(...)`
+--- - `vim.async.pawait(...)`
+--- - `vim.async.checkpoint()`
+--- - successful return from the task function, which is the final checkpoint
+--- for child management
+---
+--- Convenience APIs such as [vim.async.sleep()] and [vim.async.timeout()] can
+--- also checkpoint because they call checkpointing APIs internally.
+---
+--- Closing a task closes its attached children. Cancellation is cooperative:
+--- [Task:close()] marks a task as closing, and the task observes that state at
+--- a checkpoint. If a task is suspended on a closable operation such as a timer
+--- or child task, `vim.async` closes that operation before reporting the
+--- cancellation.
+---
+--- Use [vim.async.await()] inside a task to suspend until work completes. It
+--- accepts a task, a callback-taking function, or an argument position plus a
+--- callback-taking function. `await(task)` returns the task result or raises
+--- the task failure. [vim.async.pawait()] is the async counterpart to `pcall()`
+--- for recoverable awaited-operation failures; it returns `ok, ...` instead of
+--- failing the current task for that awaited operation. It does not suppress
+--- cancellation or a failure already pending on the current task.
+---
+--- From synchronous code, use [Task:wait()] or [Task:pwait()] to pump the event
+--- loop until the task completes. Use [Task:on_complete()] to observe
+--- completion without blocking.
+---
+--- Coordination helpers work with task handles. [vim.async.iter()] yields
+--- completed task handles in completion order, [vim.async.timeout()] awaits a
+--- task with a deadline, and `vim.async.semaphore(permits)` creates a
+--- [vim.async.Semaphore] that limits how many tasks can hold a permit for a
+--- section at once.
+--- @class vim.async: vim.async._core
+local M = setmetatable({}, { __index = core })
+
+M.semaphore = require('vim.async._semaphore')
+
+--- @param unsubscribe fun()[]
+local function unsubscribe_all(unsubscribe)
+ for _, unsub in ipairs(unsubscribe) do
+ unsub()
+ end
+end
+
+--- Create an async function from a callback-style function.
+---
+--- The callback is inserted at argument position `argc`. If `func` returns a
+--- closable handle, it is closed when the awaiting task is cancelled.
+---
+--- This is a reusable wrapper around [vim.async.await()]. Use it when the same
+--- callback API is awaited from more than one place.
+---
+--- ```lua
+--- local async = vim.async
+--- local fs_stat = async.wrap(2, vim.uv.fs_stat)
+---
+--- async.run(function()
+--- local err, stat = fs_stat(vim.api.nvim_buf_get_name(0))
+--- if not err and stat then
+--- print(stat.size)
+--- end
+--- end)
+--- ```
+---
+--- @generic T, R
+--- @param argc integer
+--- @param func fun(...: T..., callback: fun(...: R...)): vim.async.Closable?
+--- @return async fun(...: T...): R...
+function M.wrap(argc, func)
+ validate('argc', argc, 'number')
+ validate('func', func, 'callable')
+ --- @async
+ return function(...)
+ return M.await(argc, func, ...)
+ end
+end
+
+--- Iterate completed tasks in completion order.
+---
+--- The iterator yields task handles, not task results. Use
+--- [vim.async.await()] or [vim.async.pawait()] to retrieve each result. The
+--- tasks are observed in the order they complete, regardless of the order in
+--- the input list.
+---
+--- ```lua
+--- local async = vim.async
+---
+--- async.run(function()
+--- local tasks = {
+--- async.run(function() return 'cache', read_cache() end):detach(),
+--- async.run(function() return 'disk', read_file() end):detach(),
+--- }
+---
+--- for task in async.iter(tasks) do
+--- local ok, source, text = async.pawait(task)
+--- if ok then
+--- for _, other in ipairs(tasks) do
+--- if other ~= task then
+--- other:close()
+--- end
+--- end
+--- print(('loaded from %s'):format(source))
+--- return text
+--- end
+--- end
+--- end)
+--- ```
+---
+--- If code must support PUC Lua 5.1, use the direct-call form instead of a
+--- generic `for` loop. The iterator may need to suspend while waiting for the
+--- next completed task, and PUC Lua 5.1 cannot yield from a generic-for
+--- iterator call.
+---
+--- ```lua
+--- local next_task = async.iter(tasks)
+--- while true do
+--- local task = next_task()
+--- if task == nil then
+--- break
+--- end
+--- async.await(task)
+--- end
+--- ```
+--- @async
+--- @generic R
+--- @param tasks vim.async.Task<R>[] A list of tasks to wait for and iterate over.
+--- @return async fun(): vim.async.Task<R>? iterator that yields each completed task.
+function M.iter(tasks)
+ validate('tasks', tasks, 'table')
+
+ local remaining = #tasks
+ local queue = new_queue()
+ local unsubscribe = {} --- @type fun()[]
+
+ if remaining == 0 then
+ queue:put_nowait()
+ else
+ for _, task in ipairs(tasks) do
+ unsubscribe[#unsubscribe + 1] = task:on_complete(function()
+ remaining = remaining - 1
+ queue:put_nowait(task)
+ if remaining == 0 then
+ queue:put_nowait()
+ end
+ end)
+ end
+ end
+
+ local proxy = newproxy(true)
+ getmetatable(proxy).__gc = function()
+ unsubscribe_all(unsubscribe)
+ end
+
+ --- @async
+ return function()
+ local _ = proxy -- Keep the GC proxy alive with the iterator.
+ return queue:get()
+ end
+end
+
+--- Asynchronously sleep for a given duration.
+---
+--- Suspends the current task for the given duration without blocking the event
+--- loop. After the delay and timer cleanup complete, `sleep()` returns to its
+--- caller through the runtime's `schedule` hook.
+---
+--- ```lua
+--- vim.async.run(function()
+--- vim.async.sleep(100)
+--- vim.notify('resumed later')
+--- end)
+--- ```
+--- @async
+--- @param duration integer ms
+function M.sleep(duration)
+ validate('duration', duration, 'number')
+ M.await(function(callback)
+ local timer = runtime.new_timer()
+ timer:start(duration, 0, callback)
+ return timer
+ end)
+ -- Timer cleanup resumes this function directly from its close callback.
+ -- Yield once more so M.sleep() returns through the runtime scheduler.
+ M.await(runtime.schedule)
+end
+
+--- Await a task with a timeout.
+---
+--- If the task completes first, returns or raises the task result as
+--- [vim.async.await()] would. If the deadline wins, closes the task and raises
+--- `"timeout"` after the target task finishes cancellation cleanup.
+---
+--- ```lua
+--- local async = vim.async
+---
+--- async.run(function()
+--- local task = async.run(read_file, 'notes.txt')
+--- local text = async.timeout(5000, task)
+--- show_buffer(text)
+--- end)
+--- ```
+--- @async
+--- @generic R
+--- @param duration integer Timeout duration in milliseconds
+--- @param task vim.async.Task<R>
+--- @return R...
+function M.timeout(duration, task)
+ validate('duration', duration, 'number')
+ validate('task', task, 'table')
+
+ local timed_out = false
+ local timer = M.run('__timeout', function()
+ M.sleep(duration)
+ timed_out = true
+ task:close()
+ end)
+ --- @diagnostic disable-next-line: invisible
+ timer._hidden = true
+
+ local result = F.pack_len(M.pawait(task))
+ timer:close()
+ M.pawait(timer)
+
+ if timed_out then
+ error('timeout', 0)
+ end
+
+ if not result[1] then
+ error(result[2], 0)
+ end
+
+ return unpack(result, 2, result.n)
+end
+
+if type(vim) == 'table' then
+ runtime.config({
+ wait = vim.wait,
+ schedule = vim.schedule,
+ new_timer = vim.uv.new_timer,
+ })
+end
+
+return M