feat(pack): support file:// dev plugins via symlink

This commit is contained in:
2026-05-26 20:35:57 +02:00
parent feb10f543c
commit 5b69014c0c
+142 -11
View File
@@ -79,6 +79,77 @@ local function is_url(src)
return src:find("://") ~= nil return src:find("://") ~= nil
end end
---@param src string
---@return string?
local function file_url_to_path(src)
local path = src:match("^file://(/.+)$")
if not path then
return nil
end
return vim.fs.normalize(path)
end
---@param path string
---@return string?
local function plugin_name_from_path(path)
local name = vim.fs.basename(path)
if not name or name == "" or name == "." or name == ".." then
return nil
end
return name
end
local data_dir = vim.fn.stdpath("data")
if type(data_dir) == "table" then
data_dir = assert(data_dir[1])
end
local dev_opt_dir = vim.fs.joinpath(data_dir, "site", "pack", "dev", "opt")
---@param target string
---@param name string
---@return string? link
---@return boolean changed
local function ensure_dev_link(target, name)
if not vim.uv.fs_stat(target) then
log.error("pack: dev plugin path does not exist: %s", target)
return nil, false
end
local ok, info = pcall(vim.pack.get, { name })
if ok and info and #info > 0 then
pcall(vim.pack.del, { name }, { force = true })
end
vim.fn.mkdir(dev_opt_dir, "p")
local link = vim.fs.joinpath(dev_opt_dir, name)
local lstat = vim.uv.fs_lstat(link)
if lstat then
if lstat.type == "link" then
if vim.uv.fs_readlink(link) == target then
return link, false
end
local ok_unlink, err = vim.uv.fs_unlink(link)
if not ok_unlink then
log.error("pack: failed to unlink %s: %s", link, err)
return nil, false
end
else
log.error(
"pack: %s exists and is not a symlink; refusing to overwrite",
link
)
return nil, false
end
end
local ok_link, err = vim.uv.fs_symlink(target, link)
if not ok_link then
log.error("pack: failed to symlink %s -> %s: %s", link, target, err)
return nil, false
end
return link, true
end
---@param spec string | ow.Pack.Spec ---@param spec string | ow.Pack.Spec
---@return vim.pack.Spec ---@return vim.pack.Spec
local function to_pack_spec(spec) local function to_pack_spec(spec)
@@ -267,22 +338,59 @@ end
function M.setup(specs) function M.setup(specs)
local pack_specs = {} local pack_specs = {}
local order = {} local order = {}
local dev_changed = {}
for _, spec in ipairs(specs) do for _, spec in ipairs(specs) do
local spec_t = type(spec) == "table" and spec or nil
local src = type(spec) == "string" and spec or spec.src local src = type(spec) == "string" and spec or spec.src
table.insert(order, src) table.insert(order, src)
if is_url(src) then local dev_path = file_url_to_path(src)
if dev_path then
local name = (spec_t and spec_t.name)
or plugin_name_from_path(dev_path)
if not name then
log.error("pack: invalid plugin name derived from %s", src)
else
local link, did_change = ensure_dev_link(dev_path, name)
if link then
local ok_add, add_err = pcall(vim.cmd.packadd, name)
if not ok_add then
log.error(
"pack: failed to packadd %s: %s",
name,
add_err
)
else
---@type ow.Pack.Plugin
M.plugins[src] = {
src = src,
name = name,
version = spec_t and spec_t.version,
build = spec_t and spec_t.build,
path = link,
}
if did_change then
dev_changed[src] = { path = link }
end
end
end
end
elseif is_url(src) then
table.insert(pack_specs, to_pack_spec(spec)) table.insert(pack_specs, to_pack_spec(spec))
else else
vim.cmd.packadd(src) local ok_add, add_err = pcall(vim.cmd.packadd, src)
local runtime = if not ok_add then
vim.api.nvim_get_runtime_file("pack/*/opt/" .. src, false) log.error("pack: failed to packadd %s: %s", src, add_err)
---@type ow.Pack.Plugin else
local plugin = { local runtime =
src = src, vim.api.nvim_get_runtime_file("pack/*/opt/" .. src, false)
name = src, ---@type ow.Pack.Plugin
path = runtime[1] or "", local plugin = {
} src = src,
M.plugins[plugin.src] = plugin name = src,
path = runtime[1] or "",
}
M.plugins[plugin.src] = plugin
end
end end
end end
@@ -303,6 +411,9 @@ function M.setup(specs)
M.plugins[plugin.src] = plugin M.plugins[plugin.src] = plugin
vim.cmd.packadd(plugin.name) vim.cmd.packadd(plugin.name)
end) end)
for src, data in pairs(dev_changed) do
changed[src] = data
end
for _, src in ipairs(order) do for _, src in ipairs(order) do
local plugin = M.plugins[src] local plugin = M.plugins[src]
@@ -343,6 +454,26 @@ end
---@param names? string[] ---@param names? string[]
---@param opts? table ---@param opts? table
function M.update(names, opts) function M.update(names, opts)
if names then
local managed = {}
for _, plugin in pairs(M.plugins) do
if not file_url_to_path(plugin.src) and is_url(plugin.src) then
managed[plugin.name] = true
end
end
local filtered = {}
for _, name in ipairs(names) do
if managed[name] then
table.insert(filtered, name)
else
log.warning("pack: skipping %s (not managed by vim.pack)", name)
end
end
if #filtered == 0 then
return
end
names = filtered
end
vim.pack.update(names, opts) vim.pack.update(names, opts)
end end