mirror of
https://github.com/SpaceVim/SpaceVim.git
synced 2025-02-03 09:50:04 +08:00
291 lines
6.2 KiB
Lua
291 lines
6.2 KiB
Lua
local feedkeys = require('cmp.utils.feedkeys')
|
|
local config = require('cmp.config')
|
|
|
|
local async = {}
|
|
|
|
---@class cmp.AsyncThrottle
|
|
---@field public running boolean
|
|
---@field public timeout integer
|
|
---@field public sync function(self: cmp.AsyncThrottle, timeout: integer|nil)
|
|
---@field public stop function
|
|
---@field public __call function
|
|
|
|
---@type uv_timer_t[]
|
|
local timers = {}
|
|
|
|
vim.api.nvim_create_autocmd('VimLeavePre', {
|
|
callback = function()
|
|
for _, timer in pairs(timers) do
|
|
if timer and not timer:is_closing() then
|
|
timer:stop()
|
|
timer:close()
|
|
end
|
|
end
|
|
end,
|
|
})
|
|
|
|
---@param fn function
|
|
---@param timeout integer
|
|
---@return cmp.AsyncThrottle
|
|
async.throttle = function(fn, timeout)
|
|
local time = nil
|
|
local timer = assert(vim.loop.new_timer())
|
|
local _async = nil ---@type Async?
|
|
timers[#timers + 1] = timer
|
|
return setmetatable({
|
|
running = false,
|
|
timeout = timeout,
|
|
sync = function(self, timeout_)
|
|
vim.wait(timeout_ or 1000, function()
|
|
return not self.running
|
|
end)
|
|
end,
|
|
stop = function(reset_time)
|
|
if reset_time ~= false then
|
|
time = nil
|
|
end
|
|
timer:stop()
|
|
if _async then
|
|
_async:cancel()
|
|
_async = nil
|
|
end
|
|
end,
|
|
}, {
|
|
__call = function(self, ...)
|
|
local args = { ... }
|
|
|
|
if time == nil then
|
|
time = vim.loop.now()
|
|
end
|
|
|
|
self.running = true
|
|
self.stop(false)
|
|
timer:start(math.max(1, self.timeout - (vim.loop.now() - time)), 0, function()
|
|
vim.schedule(function()
|
|
time = nil
|
|
local ret = fn(unpack(args))
|
|
if async.is_async(ret) then
|
|
---@cast ret Async
|
|
_async = ret
|
|
_async:await(function(_, error)
|
|
self.running = false
|
|
if error and error ~= 'abort' then
|
|
vim.notify(error, vim.log.levels.ERROR)
|
|
end
|
|
end)
|
|
else
|
|
self.running = false
|
|
end
|
|
end)
|
|
end)
|
|
end,
|
|
})
|
|
end
|
|
|
|
---Control async tasks.
|
|
async.step = function(...)
|
|
local tasks = { ... }
|
|
local next
|
|
next = function(...)
|
|
if #tasks > 0 then
|
|
table.remove(tasks, 1)(next, ...)
|
|
end
|
|
end
|
|
table.remove(tasks, 1)(next)
|
|
end
|
|
|
|
---Timeout callback function
|
|
---@param fn function
|
|
---@param timeout integer
|
|
---@return function
|
|
async.timeout = function(fn, timeout)
|
|
local timer
|
|
local done = false
|
|
local callback = function(...)
|
|
if not done then
|
|
done = true
|
|
timer:stop()
|
|
timer:close()
|
|
fn(...)
|
|
end
|
|
end
|
|
timer = vim.loop.new_timer()
|
|
timer:start(timeout, 0, function()
|
|
callback()
|
|
end)
|
|
return callback
|
|
end
|
|
|
|
---@alias cmp.AsyncDedup fun(callback: function): function
|
|
|
|
---Create deduplicated callback
|
|
---@return function
|
|
async.dedup = function()
|
|
local id = 0
|
|
return function(callback)
|
|
id = id + 1
|
|
|
|
local current = id
|
|
return function(...)
|
|
if current == id then
|
|
callback(...)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
---Convert async process as sync
|
|
async.sync = function(runner, timeout)
|
|
local done = false
|
|
runner(function()
|
|
done = true
|
|
end)
|
|
vim.wait(timeout, function()
|
|
return done
|
|
end, 10, false)
|
|
end
|
|
|
|
---Wait and callback for next safe state.
|
|
async.debounce_next_tick = function(callback)
|
|
local running = false
|
|
return function()
|
|
if running then
|
|
return
|
|
end
|
|
running = true
|
|
vim.schedule(function()
|
|
running = false
|
|
callback()
|
|
end)
|
|
end
|
|
end
|
|
|
|
---Wait and callback for consuming next keymap.
|
|
async.debounce_next_tick_by_keymap = function(callback)
|
|
return function()
|
|
feedkeys.call('', '', callback)
|
|
end
|
|
end
|
|
|
|
local Scheduler = {}
|
|
Scheduler._queue = {}
|
|
Scheduler._executor = assert(vim.loop.new_check())
|
|
|
|
function Scheduler.step()
|
|
local budget = config.get().performance.async_budget * 1e6
|
|
local start = vim.loop.hrtime()
|
|
while #Scheduler._queue > 0 and vim.loop.hrtime() - start < budget do
|
|
local a = table.remove(Scheduler._queue, 1)
|
|
a:_step()
|
|
if a.running then
|
|
table.insert(Scheduler._queue, a)
|
|
end
|
|
end
|
|
if #Scheduler._queue == 0 then
|
|
return Scheduler._executor:stop()
|
|
end
|
|
end
|
|
|
|
---@param a Async
|
|
function Scheduler.add(a)
|
|
table.insert(Scheduler._queue, a)
|
|
if not Scheduler._executor:is_active() then
|
|
Scheduler._executor:start(vim.schedule_wrap(Scheduler.step))
|
|
end
|
|
end
|
|
|
|
--- @alias AsyncCallback fun(result?:any, error?:string)
|
|
|
|
--- @class Async
|
|
--- @field running boolean
|
|
--- @field result? any
|
|
--- @field error? string
|
|
--- @field callbacks AsyncCallback[]
|
|
--- @field thread thread
|
|
local Async = {}
|
|
Async.__index = Async
|
|
|
|
function Async.new(fn)
|
|
local self = setmetatable({}, Async)
|
|
self.callbacks = {}
|
|
self.running = true
|
|
self.thread = coroutine.create(fn)
|
|
Scheduler.add(self)
|
|
return self
|
|
end
|
|
|
|
---@param result? any
|
|
---@param error? string
|
|
function Async:_done(result, error)
|
|
self.running = false
|
|
self.result = result
|
|
self.error = error
|
|
for _, callback in ipairs(self.callbacks) do
|
|
callback(result, error)
|
|
end
|
|
end
|
|
|
|
function Async:_step()
|
|
local ok, res = coroutine.resume(self.thread)
|
|
if not ok then
|
|
return self:_done(nil, res)
|
|
elseif res == 'abort' then
|
|
return self:_done(nil, 'abort')
|
|
elseif coroutine.status(self.thread) == 'dead' then
|
|
return self:_done(res)
|
|
end
|
|
end
|
|
|
|
function Async:cancel()
|
|
self.running = false
|
|
end
|
|
|
|
---@param cb AsyncCallback
|
|
function Async:await(cb)
|
|
if not cb then
|
|
error('callback is required')
|
|
end
|
|
if self.running then
|
|
table.insert(self.callbacks, cb)
|
|
else
|
|
cb(self.result, self.error)
|
|
end
|
|
end
|
|
|
|
function Async:sync()
|
|
while self.running do
|
|
vim.wait(10)
|
|
end
|
|
return self.error and error(self.error) or self.result
|
|
end
|
|
|
|
--- @return boolean
|
|
function async.is_async(obj)
|
|
return obj and type(obj) == 'table' and getmetatable(obj) == Async
|
|
end
|
|
|
|
--- @return fun(...): Async
|
|
function async.wrap(fn)
|
|
return function(...)
|
|
local args = { ... }
|
|
return Async.new(function()
|
|
return fn(unpack(args))
|
|
end)
|
|
end
|
|
end
|
|
|
|
-- This will yield when called from a coroutine
|
|
function async.yield(...)
|
|
if not coroutine.isyieldable() then
|
|
error('Trying to yield from a non-yieldable context')
|
|
return ...
|
|
end
|
|
return coroutine.yield(...)
|
|
end
|
|
|
|
function async.abort()
|
|
return async.yield('abort')
|
|
end
|
|
|
|
return async
|