From 5b69014c0c143761b64ee4f40a6b5f846a3fdfcd Mon Sep 17 00:00:00 2001 From: Oscar Wallberg Date: Tue, 26 May 2026 20:35:57 +0200 Subject: [PATCH] feat(pack): support file:// dev plugins via symlink --- lua/pack.lua | 153 +++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 142 insertions(+), 11 deletions(-) diff --git a/lua/pack.lua b/lua/pack.lua index c805d48..e9fa91e 100644 --- a/lua/pack.lua +++ b/lua/pack.lua @@ -79,6 +79,77 @@ local function is_url(src) return src:find("://") ~= nil 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 ---@return vim.pack.Spec local function to_pack_spec(spec) @@ -267,22 +338,59 @@ end function M.setup(specs) local pack_specs = {} local order = {} + local dev_changed = {} 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 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)) else - vim.cmd.packadd(src) - local runtime = - vim.api.nvim_get_runtime_file("pack/*/opt/" .. src, false) - ---@type ow.Pack.Plugin - local plugin = { - src = src, - name = src, - path = runtime[1] or "", - } - M.plugins[plugin.src] = plugin + local ok_add, add_err = pcall(vim.cmd.packadd, src) + if not ok_add then + log.error("pack: failed to packadd %s: %s", src, add_err) + else + local runtime = + vim.api.nvim_get_runtime_file("pack/*/opt/" .. src, false) + ---@type ow.Pack.Plugin + local plugin = { + src = src, + name = src, + path = runtime[1] or "", + } + M.plugins[plugin.src] = plugin + end end end @@ -303,6 +411,9 @@ function M.setup(specs) M.plugins[plugin.src] = plugin vim.cmd.packadd(plugin.name) end) + for src, data in pairs(dev_changed) do + changed[src] = data + end for _, src in ipairs(order) do local plugin = M.plugins[src] @@ -343,6 +454,26 @@ end ---@param names? string[] ---@param opts? table 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) end