diff --git a/lua/linter.lua b/lua/linter.lua index db23fa9..6d14cef 100644 --- a/lua/linter.lua +++ b/lua/linter.lua @@ -425,11 +425,12 @@ function Linter.add(bufnr, config) return end + local debouncer = util.debounce(function() + linter:run() + end, config.debounce) vim.api.nvim_create_autocmd(config.events, { buffer = linter.bufnr, - callback = util.debounce(function() - linter:run() - end, config.debounce), + callback = function() debouncer:call() end, group = linter.augroup, }) diff --git a/lua/pack.lua b/lua/pack.lua index a78ff57..e1d89f3 100644 --- a/lua/pack.lua +++ b/lua/pack.lua @@ -1,4 +1,5 @@ local log = require("log") +local util = require("util") local config_dir = vim.fn.stdpath("config") @@ -82,10 +83,9 @@ local function run_build(plugin) plugin.build(plugin) return elseif type(plugin.build) == "table" then - local ret = vim.system( - plugin.build --[[@as table]], - { cwd = plugin.path } - ):wait() + local ret = + vim.system(plugin.build --[[@as table]], { cwd = plugin.path }) + :wait() if ret.code ~= 0 then log.error("Build failed for %s: %s", plugin.name, ret.stderr or "") end @@ -120,7 +120,8 @@ local function process_events(plugin, events) end for _, ev in ipairs(events) do - if ev.data.spec.name == plugin.name + if + ev.data.spec.name == plugin.name and ev.event == "PackChanged" and (ev.data.kind == "install" or ev.data.kind == "update") then @@ -136,8 +137,8 @@ end ---@type uv.uv_fs_event_t? local watcher = nil ----@type table -local timers = {} +---@type ow.Util.Debouncer? +local reload = nil ---@class ow.Pack ---@field plugins ow.Pack.Plugin[] @@ -147,12 +148,16 @@ local M = { ---@return string[] function M.get_names() - return vim.tbl_map(function(p) return p.name end, M.plugins) + return vim.tbl_map(function(p) + return p.name + end, M.plugins) end ---@return string[] function M.get_paths() - return vim.tbl_map(function(p) return p.path end, M.plugins) + return vim.tbl_map(function(p) + return p.path + end, M.plugins) end ---@param name string @@ -196,61 +201,29 @@ function M.watch() return end - watcher:start( - plugins_dir, - {}, - vim.schedule_wrap(function(err, filename) - if err then - log.error("Watch error: %s", err) - return - end + reload = util.debounce(function(filename) + local path = vim.fs.joinpath(plugins_dir, filename) + if not vim.uv.fs_stat(path) then + return + end + local ok, load_err = exec(path) + if ok then + log.info("Reloaded %s", filename) + else + log.error("Failed to reload %s: %s", filename, load_err) + end + end, 100) - if not filename or not filename:match("%.lua$") then - return - end - - ---@type uv.uv_timer_t? - local timer = timers[filename] - if not timer then - timer, err_msg, err_name = vim.uv.new_timer() - if not timer then - log.error( - "Failed to create new timer: %s (%s)", err_msg, err_name - ) - return - end - timers[filename] = timer - else - timer:stop() - end - - timer:start( - 100, - 0, - vim.schedule_wrap(function() - timer:stop() - timer:close() - timers[filename] = nil - - local path = plugins_dir .. "/" .. filename - if not vim.uv.fs_stat(path) then - return - end - - local ok, load_err = exec(path) - if ok then - log.info("Reloaded %s", filename) - else - log.error( - "Failed to reload %s: %s", - filename, - load_err - ) - end - end) - ) - end) - ) + watcher:start(plugins_dir, {}, function(err, filename) + if err then + log.error("Watch error: %s", err) + return + end + if not filename or not filename:match("%.lua$") then + return + end + reload:call(filename) + end) end function M.unwatch() @@ -258,10 +231,9 @@ function M.unwatch() return end - for key, timer in pairs(timers) do - timer:stop() - timer:close() - timers[key] = nil + if reload then + reload:cancel() + reload = nil end watcher:stop() @@ -281,12 +253,14 @@ function M.setup(specs) ---@param ev ow.Pack.Event callback = function(ev) local name = ev.data.spec.name - if not name then return end + if not name then + return + end if not events[name] then events[name] = {} end table.insert(events[name], ev) - end + end, } ) vim.pack.add(vim.tbl_map(to_pack_spec, specs), { @@ -309,7 +283,7 @@ function M.setup(specs) if not d.ts_parser then vim.cmd.packadd(plugin.name) end - end + end, }) vim.api.nvim_del_autocmd(id) diff --git a/lua/util.lua b/lua/util.lua index 8a39890..f103d40 100644 --- a/lua/util.lua +++ b/lua/util.lua @@ -295,49 +295,67 @@ function Util.is_list_or_nil(val, t) end end ---- Creates a debounced function that delays execution of `fn` until after `delay` milliseconds have ---- elapsed since the last time it was invoked. ----@param fn fun(...) Function to be debounced ----@param delay integer Debounce delay in milliseconds ----@return fun(...) function Debounced function -function Util.debounce(fn, delay) - ---@type uv.uv_timer_t? - local timer = nil +local NIL_KEY = {} - return function(...) - local args = vim.F.pack_len(...) - if timer then - timer:stop() - timer = nil - end +---@class ow.Util.Debouncer +---@field private _fn fun(id: any, ...) +---@field private _delay integer +---@field private _timers table +local Debouncer = {} +Debouncer.__index = Debouncer - timer = vim.defer_fn(function() - timer = nil - fn(vim.F.unpack_len(args)) - end, delay) +---@param fn fun(id: any, ...) +---@param delay integer +---@return ow.Util.Debouncer +function Debouncer.new(fn, delay) + return setmetatable({ _fn = fn, _delay = delay, _timers = {} }, Debouncer) +end + +---@param id? any +---@param ... any +function Debouncer:call(id, ...) + local key = id == nil and NIL_KEY or id + local args = vim.F.pack_len(...) + self:cancel(id) + self._timers[key] = vim.defer_fn(function() + self._timers[key] = nil + self._fn(id, vim.F.unpack_len(args)) + end, self._delay) +end + +---@param timer uv.uv_timer_t +local function dispose(timer) + timer:stop() + timer:close() +end + +---@param id? any +function Debouncer:cancel(id) + local key = id == nil and NIL_KEY or id + local timer = self._timers[key] + if timer then + dispose(timer) + self._timers[key] = nil end end ---- Creates a debounced function that delays execution of `fn` until after `delay` milliseconds have ---- elapsed since the last time it was invoked with the same unique identifier. ----@param fn fun(...) Function to be debounced ----@param delay integer Debounce delay in milliseconds ----@return fun(id: any, ...) function Debounced function, where `id` is a unique identifier -function Util.debounce_with_id(fn, delay) - local map = {} - - return function(id, ...) - local args = vim.F.pack_len(...) - if map[id] then - map[id]:stop() - map[id] = nil - end - - map[id] = vim.defer_fn(function() - map[id] = nil - fn(vim.F.unpack_len(args)) - end, delay) +function Debouncer:cancel_all() + for _, t in pairs(self._timers) do + dispose(t) end + self._timers = {} +end + +Util.Debouncer = Debouncer + +--- Creates a debounced function that delays execution of `fn` until after `delay` milliseconds have +--- elapsed since the last time it was invoked. Use `d:call(id, ...)` to debounce per-id, or +--- `d:call(...)` for a single shared slot. +---@param fn fun(id: any, ...) Function to be debounced +---@param delay integer Debounce delay in milliseconds +---@return ow.Util.Debouncer +function Util.debounce(fn, delay) + return Debouncer.new(fn, delay) end function Util.get_hl_source(name)