288 lines
7.0 KiB
Lua
288 lines
7.0 KiB
Lua
local log = require("log")
|
|
local util = require("util")
|
|
|
|
local config_dir = vim.fn.stdpath("config")
|
|
if type(config_dir) == "table" then
|
|
config_dir = assert(config_dir[1])
|
|
end
|
|
local plugins_dir = vim.fs.joinpath(config_dir, "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)
|
|
|
|
---@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 src string
|
|
---@return boolean
|
|
local function is_url(src)
|
|
return src:find("://") ~= nil
|
|
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,
|
|
},
|
|
}
|
|
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
|
|
|
|
---@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
|
|
|
|
---@type uv.uv_fs_event_t?
|
|
local watcher = nil
|
|
---@type ow.Util.KeyedDebounceHandle<string>?
|
|
local on_change_handle = 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_plugin(name)
|
|
load(name, true)
|
|
end
|
|
|
|
function M.watch()
|
|
if watcher then
|
|
return
|
|
end
|
|
|
|
watcher = assert(vim.uv.new_fs_event())
|
|
local on_change, handle = util.keyed_debounce(
|
|
---@param filename string
|
|
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,
|
|
200
|
|
)
|
|
on_change_handle = handle
|
|
|
|
assert(watcher:start(
|
|
plugins_dir,
|
|
{},
|
|
---@param err string?
|
|
---@param filename string
|
|
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
|
|
on_change(filename)
|
|
end
|
|
))
|
|
end
|
|
|
|
function M.unwatch()
|
|
if not watcher then
|
|
return
|
|
end
|
|
|
|
watcher:stop()
|
|
watcher:close()
|
|
watcher = nil
|
|
if on_change_handle then
|
|
on_change_handle.close()
|
|
on_change_handle = nil
|
|
end
|
|
end
|
|
|
|
---@param specs vim.pack.Spec[]
|
|
---@param on_load fun(data: { spec: vim.pack.Spec, path: string })
|
|
---@return table<string, true> changed names installed or updated during this call
|
|
function M.install(specs, on_load)
|
|
---@type table<string, true>
|
|
local changed = {}
|
|
local group =
|
|
vim.api.nvim_create_augroup("ow.Pack.install", { clear = true })
|
|
local id = vim.api.nvim_create_autocmd("PackChanged", {
|
|
group = group,
|
|
---@param ev ow.Pack.Event
|
|
callback = function(ev)
|
|
local name = ev.data.spec.name
|
|
if
|
|
name
|
|
and (ev.data.kind == "install" or ev.data.kind == "update")
|
|
then
|
|
changed[name] = true
|
|
end
|
|
end,
|
|
})
|
|
vim.pack.add(specs, { load = on_load })
|
|
vim.api.nvim_del_autocmd(id)
|
|
return changed
|
|
end
|
|
|
|
---@param specs (string | ow.Pack.PluginSpec)[]
|
|
function M.setup(specs)
|
|
local pack_specs = {}
|
|
for _, spec in ipairs(specs) do
|
|
local src = type(spec) == "string" and spec or spec[1]
|
|
if is_url(src) then
|
|
table.insert(pack_specs, to_pack_spec(spec))
|
|
else
|
|
vim.cmd.packadd(src)
|
|
local runtime =
|
|
vim.api.nvim_get_runtime_file("pack/*/opt/" .. src, false)
|
|
---@type ow.Pack.Plugin
|
|
local plugin = {
|
|
[1] = src,
|
|
name = src,
|
|
path = runtime[1] or "",
|
|
}
|
|
table.insert(M.plugins, plugin)
|
|
end
|
|
end
|
|
|
|
local changed = M.install(pack_specs, 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,
|
|
path = data.path,
|
|
}
|
|
table.insert(M.plugins, plugin)
|
|
vim.cmd.packadd(plugin.name)
|
|
end)
|
|
|
|
for _, plugin in ipairs(M.plugins) do
|
|
if plugin.build and changed[plugin.name] then
|
|
run_build(plugin)
|
|
end
|
|
load(plugin.name, false)
|
|
end
|
|
end
|
|
|
|
return M
|