Files
nvim/lua/pack.lua
T

346 lines
8.4 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
---@alias ow.Pack.Hook fun(ev: ow.Pack.Event.Data)
---@type table<string, ow.Pack.Hook>
local hooks = {}
---@class ow.Pack
---@field plugins ow.Pack.Plugin[]
local M = {
plugins = {},
}
---@param name string
---@param fn ow.Pack.Hook
function M.register_hook(name, fn)
hooks[name] = fn
end
local function setup_event_listener()
local group =
vim.api.nvim_create_augroup("ow.Pack.events", { clear = true })
vim.api.nvim_create_autocmd("PackChanged", {
group = group,
---@param ev ow.Pack.Event
callback = function(ev)
local name = ev.data.spec.name
if not name then
return
end
if ev.data.kind ~= "install" and ev.data.kind ~= "update" then
return
end
local hook = hooks[name]
if hook then
hook(ev.data)
end
end,
})
end
---@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
local w, err = vim.uv.new_fs_event()
if not w then
util.warning("pack: failed to create fs_event: %s", err)
return
end
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
)
local ok, err = w:start(
plugins_dir,
{},
---@param err string?
---@param filename string
function(err, filename)
if err then
log.error("pack: watch error for %s: %s", filename, err)
return
end
if not filename or not filename:match("%.lua$") then
return
end
on_change(filename)
end
)
if not ok then
util.warning("pack: failed to watch %s: %s", plugins_dir, err)
w:close()
handle:close()
return
end
on_change_handle = handle
watcher = w
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 then
if changed[plugin.name] then
run_build(plugin)
end
M.register_hook(plugin.name, function(ev)
plugin.path = ev.path
run_build(plugin)
end)
end
load(plugin.name, false)
end
setup_event_listener()
end
---@param names? string[]
---@param opts? table
function M.update(names, opts)
vim.pack.update(names, opts)
end
return M