-- Credit: https://gist.github.com/runiq/31aa5c4bf00f8e0843cd267880117201 -- local M = {} ---Validates args for `throttle()` and `debounce()`. local function td_validate(fn, ms) vim.validate { fn = { fn, "f" }, ms = { ms, function(v) return type(v) == "number" and v > 0 end, "number > 0", }, } end --- Throttles a function on the leading edge. Automatically `schedule_wrap()`s. ---@param fn fun(...) Function to throttle ---@param ms number Timeout in ms ---@return fun(...) wrapped_fn Throttled function ---@return uv_timer_t timer Remember to call `timer.close()` at the end or you will leak memory! function M.throttle_leading(fn, ms) td_validate(fn, ms) local timer = vim.loop.new_timer() local running = false local function wrapped_fn(...) if not running then timer:start(ms, 0, function() running = false end) running = true pcall(vim.schedule_wrap(fn), select(1, ...)) end end return wrapped_fn, timer end --- Throttles a function on the trailing edge. Automatically `schedule_wrap()`s. ---@param fn fun(...) Function to throttle ---@param ms number Timeout in ms ---@param last? boolean Whether to use the arguments of the last call to `fn` within the timeframe. --- Default: Use arguments of the first call. ---@return fun(...) wrapped_fn Throttled function ---@return uv_timer_t timer Remember to call `timer.close()` at the end or you will leak memory! function M.throttle_trailing(fn, ms, last) td_validate(fn, ms) local timer = vim.loop.new_timer() local running = false local wrapped_fn if not last then function wrapped_fn(...) if not running then local argv = { ... } local argc = select("#", ...) timer:start(ms, 0, function() running = false pcall(vim.schedule_wrap(fn), unpack(argv, 1, argc)) end) running = true end end else local argv, argc function wrapped_fn(...) argv = { ... } argc = select("#", ...) if not running then timer:start(ms, 0, function() running = false pcall(vim.schedule_wrap(fn), unpack(argv, 1, argc)) end) running = true end end end return wrapped_fn, timer end --- Debounces a function on the leading edge. Automatically `schedule_wrap()`s. ---@param fn fun(...) Function to debounce ---@param ms number Timeout in ms ---@return fun(...) wrapped_fn Debounced function ---@return uv_timer_t timer Remember to call `timer.close()` at the end or you will leak memory! function M.debounce_leading(fn, ms) td_validate(fn, ms) local timer = vim.loop.new_timer() local running = false local function wrapped_fn(...) timer:start(ms, 0, function() running = false end) if not running then running = true pcall(vim.schedule_wrap(fn), select(1, ...)) end end return wrapped_fn, timer end --- Debounces a function on the trailing edge. Automatically `schedule_wrap()`s. ---@param fn fun(...) Function to debounce ---@param ms number Timeout in ms ---@param first? boolean Whether to use the arguments of the first call to `fn` within the timeframe. --- Default: Use arguments of the last call. ---@return fun(...) wrapped_fn Debounced function ---@return uv_timer_t timer Remember to call `timer.close()` at the end or you will leak memory! function M.debounce_trailing(fn, ms, first) td_validate(fn, ms) local timer = vim.loop.new_timer() local wrapped_fn if not first then function wrapped_fn(...) local argv = { ... } local argc = select("#", ...) timer:start(ms, 0, function() pcall(vim.schedule_wrap(fn), unpack(argv, 1, argc)) end) end else local argv, argc function wrapped_fn(...) argv = argv or { ... } argc = argc or select("#", ...) timer:start(ms, 0, function() pcall(vim.schedule_wrap(fn), unpack(argv, 1, argc)) end) end end return wrapped_fn, timer end --- Test deferment methods (`{throttle,debounce}_{leading,trailing}()`). ---@param bouncer string Bouncer function to test ---@param ms? number Timeout in ms, default 2000. ---@param firstlast? boolean Whether to use the 'other' fn call strategy. function M.test_defer(bouncer, ms, firstlast) local bouncers = { tl = M.throttle_leading, tt = M.throttle_trailing, dl = M.debounce_leading, dt = M.debounce_trailing, } local timeout = ms or 2000 local bounced = bouncers[bouncer](function(i) vim.cmd('echom "' .. bouncer .. ": " .. i .. '"') end, timeout, firstlast) for i, _ in ipairs { 1, 2, 3, 4, 5 } do bounced(i) vim.schedule(function() vim.cmd("echom " .. i) end) vim.fn.call("wait", { 1000, "v:false" }) end end return M