local log = require("log") local util = require("util") local plugins_dir = vim.fs.joinpath(vim.fn.stdpath("config"), "plugins") ---@param path string ---@return boolean success ---@return string? err local function exec(path) local chunk, load_err = loadfile(path) if not chunk then return false, load_err end local ok, call_err = pcall(chunk) if not ok then return false, call_err end return true end ---@class ow.Pack.PluginSpec ---@field [1] string ---@field name? string ---@field version? string | vim.VersionRange ---@field build? string[] | fun(self: ow.Pack.Plugin) ---@field ts_parser? ow.TS.ParserField ---@class ow.Pack.Plugin : ow.Pack.PluginSpec ---@field name string ---@field path string ---@param name string ---@return string? local function normalize_name(name) name = name:gsub("%.lua$", "") name = name:gsub("%.nvim$", "") if name == "" then return nil end return name:lower() end ---@param name string ---@return string? local function plugin_config_path(name) local normalized = normalize_name(name) if not normalized then log.error("Invalid plugin name: %s", name) return end return vim.fs.joinpath(plugins_dir, normalized .. ".lua") end ---@param name string ---@param required boolean local function load(name, required) local path = plugin_config_path(name) if not path then return end if vim.uv.fs_stat(path) then local ok, err = exec(path) if not ok then log.error("Failed to load %s: %s", name, err) end elseif required then log.error("No config file found for %s", name) end end ---@param spec string | ow.Pack.PluginSpec ---@return vim.pack.Spec local function to_pack_spec(spec) if type(spec) == "string" then return { src = spec } end return { src = spec[1], name = spec.name, version = spec.version, data = { build = spec.build, ts_parser = spec.ts_parser, }, } end ---@param plugin ow.Pack.Plugin local function run_build(plugin) if type(plugin.build) == "function" then plugin.build(plugin) return elseif type(plugin.build) == "table" then 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 return end log.error("Invalid build parameter for %s", plugin.name) end ---@param plugin ow.Pack.Plugin local function run_ts_build(plugin) local ts = require("ts") for _, p in ipairs(ts.normalize(plugin.ts_parser)) do ts.build(plugin, p) end end ---@class ow.Pack.Event.Data ---@field active boolean ---@field kind "install" | "update" | "delete" ---@field spec vim.pack.Spec ---@field path string ---@class ow.Pack.Event : vim.api.keyset.create_autocmd.callback_args ---@field data ow.Pack.Event.Data ---@param plugin ow.Pack.Plugin ---@param events ow.Pack.Event[] local function process_events(plugin, events) if not plugin.build and not plugin.ts_parser then return end for _, ev in ipairs(events) do if ev.data.spec.name == plugin.name and ev.event == "PackChanged" and (ev.data.kind == "install" or ev.data.kind == "update") then if plugin.ts_parser then run_ts_build(plugin) else run_build(plugin) end end end end ---@type uv.uv_fs_event_t? local watcher = nil ---@type (fun(filename: string) | ow.Util.KeyedDebouncer)? local reload = nil ---@class ow.Pack ---@field plugins ow.Pack.Plugin[] local M = { plugins = {}, } ---@return string[] function M.get_names() 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) end ---@param name string function M.reload(name) load(name, true) end function M.watch() if watcher then return end watcher = assert(vim.uv.new_fs_event()) reload = util.keyed_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) assert(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(filename) end)) end function M.unwatch() if not watcher then return end if reload then reload:close() reload = nil end watcher:stop() watcher:close() watcher = nil end ---@param specs (string | ow.Pack.PluginSpec)[] function M.setup(specs) ---@type table local events = {} local group = vim.api.nvim_create_augroup("ow.Pack", { clear = true }) local id = vim.api.nvim_create_autocmd( { "PackChangedPre", "PackChanged" }, { group = group, ---@param ev ow.Pack.Event callback = function(ev) local name = ev.data.spec.name if not name then return end if not events[name] then events[name] = {} end table.insert(events[name], ev) end, } ) vim.pack.add(vim.tbl_map(to_pack_spec, specs), { load = function(data) if not data.spec.name then log.error("Missing name for plugin: %s", data.spec.src) return end local d = data.spec.data or {} ---@type ow.Pack.Plugin local plugin = { [1] = data.spec.src, name = data.spec.name, version = data.spec.version, build = d.build, ts_parser = d.ts_parser, path = data.path, } table.insert(M.plugins, plugin) if not d.ts_parser then vim.cmd.packadd(plugin.name) end end, }) vim.api.nvim_del_autocmd(id) for _, plugin in ipairs(M.plugins) do local ev = events[plugin.name] if ev then process_events(plugin, ev) end load(plugin.name, false) end end return M