Files
nvim/lua/pack.lua
T

326 lines
8.0 KiB
Lua

local log = require("log")
local config_dir = vim.fn.stdpath("config")
---@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:match("[^.]+")
if name then
return name:lower()
end
return nil
end
---@param plugin ow.Pack.Plugin
local function load(plugin)
local name = normalize_name(plugin.name)
if not name then
log.error("Invalid plugin name: %s", plugin.name)
return
end
local path = string.format("%s/lua/plugins/%s.lua", config_dir, name)
if vim.uv.fs_stat(path) then
local ok, err = exec(path)
if not ok then
log.error("Failed to load %s: %s", plugin.name, err)
end
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 table<string, uv.uv_timer_t>
local timers = {}
---@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)
local normalized = normalize_name(name)
if not normalized then
log.error("Invalid plugin name: %s", name)
return
end
local path = string.format("%s/lua/plugins/%s.lua", config_dir, normalized)
if not vim.uv.fs_stat(path) then
log.error("No config file found for %s", name)
return
end
local ok, err = exec(path)
if ok then
log.info("Reloaded %s", name)
else
log.error("Failed to reload %s: %s", name, err)
end
end
function M.watch()
if watcher then
return
end
local plugins_dir = vim.fs.joinpath(config_dir, "lua/plugins")
local err_msg, err_name
watcher, err_msg, err_name = vim.uv.new_fs_event()
if not watcher then
log.error(
"Failed to create fs event watcher: %s (%s)",
err_msg,
err_name
)
return
end
watcher:start(
plugins_dir,
{},
vim.schedule_wrap(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
---@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)
)
end
function M.unwatch()
if not watcher then
return
end
for key, timer in pairs(timers) do
timer:stop()
timer:close()
timers[key] = nil
end
watcher:stop()
watcher:close()
watcher = nil
end
---@param specs (string | ow.Pack.PluginSpec)[]
function M.setup(specs)
---@type table<string, ow.Pack.Event[]?>
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)
end
end
return M