From 7070cf6adec417804ad7af92f77d3d0019f48c28 Mon Sep 17 00:00:00 2001 From: Folke Lemaitre Date: Sat, 22 Jun 2024 22:18:26 +0200 Subject: [PATCH] feat: rewrite of spec resolving --- lua/lazy/core/fragments.lua | 159 +++++++++++++++ lua/lazy/core/loader.lua | 8 +- lua/lazy/core/meta.lua | 245 +++++++++++++++++++++++ lua/lazy/core/plugin.lua | 375 +++-------------------------------- lua/lazy/core/util.lua | 2 +- lua/lazy/health.lua | 18 -- lua/lazy/types.lua | 51 +++-- tests/core/plugin_spec.lua | 128 ++++++++---- tests/handlers/keys_spec.lua | 1 + 9 files changed, 558 insertions(+), 429 deletions(-) create mode 100644 lua/lazy/core/fragments.lua create mode 100644 lua/lazy/core/meta.lua diff --git a/lua/lazy/core/fragments.lua b/lua/lazy/core/fragments.lua new file mode 100644 index 0000000..7096f55 --- /dev/null +++ b/lua/lazy/core/fragments.lua @@ -0,0 +1,159 @@ +local Config = require("lazy.core.config") + +local M = {} + +M._fid = 0 + +local function next_id() + M._fid = M._fid + 1 + return M._fid +end + +---@class LazyFragments +---@field fragments table +---@field frag_stack number[] +---@field dep_stack number[] +---@field dirty table +---@field spec LazySpecLoader +local F = {} + +---@param spec LazySpecLoader +---@return LazyFragments +function M.new(spec) + local self = setmetatable({}, { __index = F }) + self.fragments = {} + self.frag_stack = {} + self.dep_stack = {} + self.spec = spec + self.dirty = {} + return self +end + +---@param id number +function F:get(id) + return self.fragments[id] +end + +---@param id number +function F:del(id) + -- del fragment + local fragment = self.fragments[id] + if not fragment then + return + end + + self.dirty[id] = true + + -- remove from parent + local pid = fragment.pid + if pid then + local parent = self.fragments[pid] + if parent.frags then + ---@param fid number + parent.frags = vim.tbl_filter(function(fid) + return fid ~= id + end, parent.frags) + end + if parent.deps then + ---@param fid number + parent.deps = vim.tbl_filter(function(fid) + return fid ~= id + end, parent.deps) + end + self.dirty[pid] = true + end + + -- remove children + if fragment.frags then + for _, fid in ipairs(fragment.frags) do + self:del(fid) + end + end + + self.fragments[id] = nil +end + +---@param plugin LazyPluginSpec +function F:add(plugin) + local id = next_id() + + local pid = self.frag_stack[#self.frag_stack] + + ---@type LazyFragment + local fragment = { + id = id, + pid = pid, + name = plugin.name, + url = plugin.url, + dir = plugin.dir, + spec = plugin --[[@as LazyPlugin]], + } + + -- short url / ref + if plugin[1] then + local slash = plugin[1]:find("/", 1, true) + if slash then + local prefix = plugin[1]:sub(1, 4) + if prefix == "http" or prefix == "git@" then + fragment.url = fragment.url or plugin[1] + else + fragment.name = fragment.name or plugin[1]:sub(slash + 1) + fragment.url = fragment.url or Config.options.git.url_format:format(plugin[1]) + end + else + fragment.name = fragment.name or plugin[1] + end + end + + -- name + fragment.name = fragment.name + or fragment.url and self.spec.get_name(fragment.url) + or fragment.dir and self.spec.get_name(fragment.dir) + if not fragment.name then + return self.spec:error("Invalid plugin spec " .. vim.inspect(plugin)) + end + + if type(plugin.config) == "table" then + self.spec:warn( + "{" .. fragment.name .. "}: setting a table to `Plugin.config` is deprecated. Please use `Plugin.opts` instead" + ) + ---@diagnostic disable-next-line: assign-type-mismatch + plugin.opts = plugin.config + plugin.config = nil + end + + self.fragments[id] = fragment + + -- add to parent + if pid then + local parent = self.fragments[pid] + parent.frags = parent.frags or {} + table.insert(parent.frags, id) + end + + -- add to parent's deps + local did = self.dep_stack[#self.dep_stack] + if did and did == pid then + fragment.dep = true + local parent = self.fragments[did] + parent.deps = parent.deps or {} + table.insert(parent.deps, id) + end + + table.insert(self.frag_stack, id) + -- dependencies + if plugin.dependencies then + table.insert(self.dep_stack, id) + self.spec:normalize(plugin.dependencies) + table.remove(self.dep_stack) + end + -- child specs + if plugin.specs then + self.spec:normalize(plugin.specs) + end + table.remove(self.frag_stack) + + return fragment +end + +return M diff --git a/lua/lazy/core/loader.lua b/lua/lazy/core/loader.lua index ffd8753..a0233ad 100644 --- a/lua/lazy/core/loader.lua +++ b/lua/lazy/core/loader.lua @@ -105,7 +105,7 @@ function M.startup() M.source(vim.env.VIMRUNTIME .. "/filetype.lua") -- backup original rtp - local rtp = vim.opt.rtp:get() + local rtp = vim.opt.rtp:get() --[[@as string[] ]] -- 1. run plugin init Util.track({ start = "init" }) @@ -136,7 +136,7 @@ function M.startup() if not path:find("after/?$") then -- these paths don't will already have their ftdetect ran, -- by sourcing filetype.lua above, so skip them - M.did_ftdetect[path] = true + M.did_ftdetect[path] = path M.packadd(path) end end @@ -144,7 +144,9 @@ function M.startup() -- 4. load after plugins Util.track({ start = "after" }) - for _, path in ipairs(vim.opt.rtp:get()) do + for _, path in + ipairs(vim.opt.rtp:get() --[[@as string[] ]]) + do if path:find("after/?$") then M.source_runtime(path, "plugin") end diff --git a/lua/lazy/core/meta.lua b/lua/lazy/core/meta.lua new file mode 100644 index 0000000..9c40238 --- /dev/null +++ b/lua/lazy/core/meta.lua @@ -0,0 +1,245 @@ +local Config = require("lazy.core.config") +local Util = require("lazy.core.util") + +---@class LazyMeta +---@field plugins table +---@field str_to_meta table +---@field frag_to_meta table +---@field dirty table +---@field spec LazySpecLoader +---@field fragments LazyFragments +local M = {} + +---@param spec LazySpecLoader +---@return LazyMeta +function M.new(spec) + local self = setmetatable({}, { __index = M }) + self.spec = spec + self.fragments = require("lazy.core.fragments").new(spec) + self.plugins = {} + self.frag_to_meta = {} + self.str_to_meta = {} + self.dirty = {} + return self +end + +---@param name string +function M:del(name) + local meta = self.plugins[name] + if not meta then + return + end + for _, fid in ipairs(meta._.frags or {}) do + self.fragments:del(fid) + end + self.plugins[name] = nil +end + +---@param plugin LazyPluginSpec +function M:add(plugin) + local fragment = self.fragments:add(plugin) + if not fragment then + return + end + + local meta = self.plugins[fragment.name] + or fragment.url and self.str_to_meta[fragment.url] + or fragment.dir and self.str_to_meta[fragment.dir] + + if not meta then + meta = { name = fragment.name, _ = { frags = {} } } + local url, dir = fragment.url, fragment.dir + -- add to index + if url then + self.str_to_meta[url] = meta + end + if dir then + self.str_to_meta[dir] = meta + end + end + + table.insert(meta._.frags, fragment.id) + + if plugin.name then + -- handle renames + if meta.name ~= plugin.name then + self.plugins[meta.name] = nil + meta.name = plugin.name + end + end + + self.plugins[meta.name] = meta + self.frag_to_meta[fragment.id] = meta + self.dirty[meta.name] = true +end + +function M:rebuild() + for fid in pairs(self.fragments.dirty) do + local meta = self.frag_to_meta[fid] + if meta then + if self.fragments:get(fid) then + -- fragment still exists, so mark plugin as dirty + self.dirty[meta.name] = true + else + -- fragment was deleted, so remove it from plugin + ---@param f number + meta._.frags = vim.tbl_filter(function(f) + return f ~= fid + end, meta._.frags) + -- if no fragments left, delete plugin + if #meta._.frags == 0 then + self:del(meta.name) + else + self.dirty[meta.name] = true + end + end + end + end + self.fragments.dirty = {} + for n, _ in pairs(self.dirty) do + self:_rebuild(n) + end +end + +---@param name string +function M:_rebuild(name) + local plugin = self.plugins[name] + if not plugin or #plugin._.frags == 0 then + self.plugins[name] = nil + return + end + setmetatable(plugin, nil) + plugin.dependencies = {} + + local super = nil + plugin.url = nil + plugin._.dep = true + plugin.optional = true + + assert(#plugin._.frags > 0, "no fragments found for plugin " .. name) + + for _, fid in ipairs(plugin._.frags) do + local fragment = self.fragments:get(fid) + assert(fragment, "fragment " .. fid .. " not found, for plugin " .. name) + ---@diagnostic disable-next-line: no-unknown + super = setmetatable(fragment.spec, super and { __index = super } or nil) + plugin._.dep = plugin._.dep and fragment.dep + plugin.optional = plugin.optional and (rawget(fragment.spec, "optional") == true) + plugin.url = fragment.url or plugin.url + + -- dependencies + for _, dep in ipairs(fragment.deps or {}) do + table.insert(plugin.dependencies, self.fragments:get(dep).name) + end + end + + super = super or {} + + -- dir / dev + plugin.dev = super.dev + plugin.dir = super.dir + if plugin.dir then + plugin.dir = Util.norm(plugin.dir) + else + if plugin.dev == nil and plugin.url then + for _, pattern in ipairs(Config.options.dev.patterns) do + if plugin.url:find(pattern, 1, true) then + plugin.dev = true + break + end + end + end + if plugin.dev == true then + local dev_dir = type(Config.options.dev.path) == "string" and Config.options.dev.path .. "/" .. plugin.name + or Util.norm(Config.options.dev.path(plugin)) + if not Config.options.dev.fallback or vim.fn.isdirectory(dev_dir) == 1 then + plugin.dir = dev_dir + else + plugin.dev = false + end + end + plugin.dir = plugin.dir or Config.options.root .. "/" .. plugin.name + end + + if #plugin.dependencies == 0 and not super.dependencies then + plugin.dependencies = nil + end + if not plugin.optional and not super.optional then + plugin.optional = nil + end + setmetatable(plugin, { __index = super }) + + self.dirty[plugin.name] = nil + return plugin +end + +---@param plugin LazyPlugin +function M:disable(plugin) + plugin._.kind = "disabled" + self:del(plugin.name) + self.spec.disabled[plugin.name] = plugin +end + +function M:fix_cond() + for _, plugin in pairs(self.plugins) do + local cond = plugin.cond + if cond == nil then + cond = Config.options.defaults.cond + end + if cond == false or (type(cond) == "function" and not cond(plugin)) then + plugin._.cond = false + local stack = { plugin } + while #stack > 0 do + local p = table.remove(stack) --[[@as LazyPlugin]] + if not self.spec.ignore_installed[p.name] then + for _, dep in ipairs(p.dependencies or {}) do + table.insert(stack, self.plugins[dep]) + end + self.spec.ignore_installed[p.name] = true + end + end + plugin.enabled = false + end + end +end + +function M:fix_optional() + if self.spec.optional then + return 0 + end + local changes = 0 + for _, plugin in pairs(self.plugins) do + if plugin.optional then + changes = changes + 1 + self:del(plugin.name) + end + end + self:rebuild() + return changes +end + +function M:fix_disabled() + local changes = 0 + for _, plugin in pairs(self.plugins) do + if plugin.enabled == false or (type(plugin.enabled) == "function" and not plugin.enabled()) then + changes = changes + 1 + self:disable(plugin) + end + end + self:rebuild() + return changes +end + +function M:fix() + Util.track("resolve plugins") + self:rebuild() + + self:fix_cond() + + -- selene: allow(empty_loop) + while self:fix_disabled() + self:fix_optional() > 0 do + end + Util.track() +end + +return M diff --git a/lua/lazy/core/plugin.lua b/lua/lazy/core/plugin.lua index de6d228..6aec6f1 100644 --- a/lua/lazy/core/plugin.lua +++ b/lua/lazy/core/plugin.lua @@ -1,4 +1,5 @@ local Config = require("lazy.core.config") +local Meta = require("lazy.core.meta") local Pkg = require("lazy.pkg") local Util = require("lazy.core.util") @@ -7,46 +8,49 @@ local M = {} M.loading = false ---@class LazySpecLoader +---@field meta LazyMeta ---@field plugins table ----@field fragments table ---@field disabled table ----@field dirty table ---@field ignore_installed table ---@field modules string[] ---@field notifs {msg:string, level:number, file?:string}[] ---@field importing? string ---@field optional? boolean ---@field pkgs table ----@field names table local Spec = {} M.Spec = Spec -M.last_fid = 0 -M.fid_stack = {} ---@type number[] M.LOCAL_SPEC = ".lazy.lua" ---@param spec? LazySpec ---@param opts? {optional?:boolean} function Spec.new(spec, opts) - local self = setmetatable({}, { __index = Spec }) - self.plugins = {} - self.fragments = {} + local self = setmetatable({}, Spec) + self.meta = Meta.new(self) self.disabled = {} self.modules = {} - self.dirty = {} self.notifs = {} self.ignore_installed = {} self.pkgs = {} self.optional = opts and opts.optional - self.names = {} if spec then self:parse(spec) end return self end +function Spec:__index(key) + if Spec[key] then + return Spec[key] + end + if key == "plugins" then + self.meta:rebuild() + return self.meta.plugins + end +end + function Spec:parse(spec) self:normalize(spec) - self:fix_disabled() + self.meta:fix() end -- PERF: optimized code to get package name without using lua patterns @@ -58,136 +62,22 @@ function Spec.get_name(pkg) return slash and name:sub(#name - slash + 2) or pkg:gsub("%W+", "_") end ----@param plugin LazyPlugin ----@param results? string[] -function Spec:add(plugin, results) - -- check if we already processed this spec. Can happen when a user uses the same instance of a spec in multiple specs - -- see https://github.com/folke/lazy.nvim/issues/45 - if rawget(plugin, "_") then - if results then - table.insert(results, plugin.name) - end - return plugin - end +---@param plugin LazyPluginSpec +function Spec:add(plugin) + self.meta:add(plugin) - local is_ref = plugin[1] and not plugin[1]:find("/", 1, true) - - if not plugin.url and not is_ref and plugin[1] then - local prefix = plugin[1]:sub(1, 4) - if prefix == "http" or prefix == "git@" then - plugin.url = plugin[1] - else - plugin.url = Config.options.git.url_format:format(plugin[1]) - end - end - - ---@type string? - local dir - - if plugin.dir then - dir = Util.norm(plugin.dir) - -- local plugin - plugin.name = plugin.name or Spec.get_name(plugin.dir) - elseif plugin.url then - if plugin.name then - self.names[plugin.url] = plugin.name - local name = Spec.get_name(plugin.url) - if name and self.plugins[name] then - self.plugins[name].name = plugin.name - self.plugins[plugin.name] = self.plugins[name] - self.plugins[name] = nil - end - else - plugin.name = self.names[plugin.url] or Spec.get_name(plugin.url) - end - -- check for dev plugins - if plugin.dev == nil then - for _, pattern in ipairs(Config.options.dev.patterns) do - if plugin.url:find(pattern, 1, true) then - plugin.dev = true - break - end - end - end - elseif is_ref then - plugin.name = plugin[1] - else - self:error("Invalid plugin spec " .. vim.inspect(plugin)) - return - end - - if not plugin.name or plugin.name == "" then - self:error("Plugin spec " .. vim.inspect(plugin) .. " has no name") - return - end - - -- dev plugins - if plugin.dev then - local dir_dev - if type(Config.options.dev.path) == "string" then - dir_dev = Config.options.dev.path .. "/" .. plugin.name - else - dir_dev = Util.norm(Config.options.dev.path(plugin)) - end - if not Config.options.dev.fallback or vim.fn.isdirectory(dir_dev) == 1 then - dir = dir_dev - end - elseif plugin.dev == false then - -- explicitly select the default path - dir = Config.options.root .. "/" .. plugin.name - end - - if type(plugin.config) == "table" then - self:warn( - "{" .. plugin.name .. "}: setting a table to `Plugin.config` is deprecated. Please use `Plugin.opts` instead" - ) - ---@diagnostic disable-next-line: assign-type-mismatch - plugin.opts = plugin.config - plugin.config = nil - end - - local fpid = M.fid_stack[#M.fid_stack] - - M.last_fid = M.last_fid + 1 - plugin._ = { - dir = dir, - fid = M.last_fid, - fpid = fpid, - dep = fpid ~= nil, - module = self.importing, - } - self.fragments[plugin._.fid] = plugin - -- remote plugin - plugin.dir = plugin._.dir or (plugin.name and (Config.options.root .. "/" .. plugin.name)) or nil - - if fpid then - local parent = self.fragments[fpid] - parent._.fdeps = parent._.fdeps or {} - table.insert(parent._.fdeps, plugin._.fid) - end - - if plugin.dependencies then - table.insert(M.fid_stack, plugin._.fid) - plugin.dependencies = self:normalize(plugin.dependencies, {}) - table.remove(M.fid_stack) - end + ---@diagnostic disable-next-line: cast-type-mismatch + ---@cast plugin LazyPlugin -- import the plugin's spec if Config.options.pkg.enabled and plugin.dir and not self.pkgs[plugin.dir] then self.pkgs[plugin.dir] = true local pkg = Pkg.get_spec(plugin) if pkg then - self:normalize(pkg, nil) + self:normalize(pkg) end end - if self.plugins[plugin.name] then - plugin = self:merge(self.plugins[plugin.name], plugin) - end - self.plugins[plugin.name] = plugin - if results then - table.insert(results, plugin.name) - end return plugin end @@ -199,166 +89,6 @@ function Spec:warn(msg) self:log(msg, vim.log.levels.WARN) end ---- Rebuilds a plugin spec excluding any removed fragments ----@param name? string -function Spec:rebuild(name) - if not name then - for n, _ in pairs(self.dirty) do - self:rebuild(n) - end - self.dirty = {} - end - local plugin = self.plugins[name] - if not plugin then - return - end - - local fragments = {} ---@type LazyPlugin[] - - repeat - local super = plugin._.super - if self.fragments[plugin._.fid] then - plugin._.dep = plugin._.fpid ~= nil - plugin._.super = nil - if plugin._.fdeps then - plugin.dependencies = {} - for _, cid in ipairs(plugin._.fdeps) do - if self.fragments[cid] then - table.insert(plugin.dependencies, self.fragments[cid].name) - end - end - end - setmetatable(plugin, nil) - table.insert(fragments, 1, plugin) - end - plugin = super - until not plugin - - if #fragments == 0 then - self.plugins[name] = nil - return - end - - plugin = fragments[1] - for i = 2, #fragments do - plugin = self:merge(plugin, fragments[i]) - end - self.plugins[name] = plugin -end - ---- Recursively removes all fragments from a plugin spec or a given fragment ----@param id string|number Plugin name or fragment id ----@param opts {self: boolean} -function Spec:remove_fragments(id, opts) - local fids = {} ---@type number[] - - if type(id) == "number" then - fids[1] = id - else - local plugin = self.plugins[id] - repeat - if plugin._.fpid then - local parent = self.fragments[plugin._.fpid] - if parent then - parent._.fdeps = vim.tbl_filter(function(fid) - return fid ~= plugin._.fid - end, parent._.fdeps) - self.dirty[parent.name] = true - end - end - fids[#fids + 1] = plugin._.fid - plugin = plugin._.super - until not plugin - end - - for _, fid in ipairs(fids) do - local fragment = self.fragments[fid] - if fragment then - for _, cid in ipairs(fragment._.fdeps or {}) do - self:remove_fragments(cid, { self = true }) - end - if opts.self then - self.fragments[fid] = nil - end - self.dirty[fragment.name] = true - end - end -end - -function Spec:fix_cond() - for _, plugin in pairs(self.plugins) do - local cond = plugin.cond - if cond == nil then - cond = Config.options.defaults.cond - end - if cond == false or (type(cond) == "function" and not cond(plugin)) then - plugin._.cond = false - local stack = { plugin } - while #stack > 0 do - local p = table.remove(stack) - if not self.ignore_installed[p.name] then - for _, dep in ipairs(p.dependencies or {}) do - table.insert(stack, self.plugins[dep]) - end - self.ignore_installed[p.name] = true - end - end - plugin.enabled = false - end - end -end - -function Spec:fix_optional() - if not self.optional then - ---@param plugin LazyPlugin - local function all_optional(plugin) - return (not plugin) or (rawget(plugin, "optional") and all_optional(plugin._.super)) - end - - -- handle optional plugins - for _, plugin in pairs(self.plugins) do - if plugin.optional and all_optional(plugin) then - -- remove all optional fragments - self:remove_fragments(plugin.name, { self = true }) - self.plugins[plugin.name] = nil - end - end - end -end - -function Spec:fix_disabled() - for _, plugin in pairs(self.plugins) do - if not plugin.name or not plugin.dir then - self:error("Plugin spec for **" .. plugin.name .. "** not found.\n```lua\n" .. vim.inspect(plugin) .. "\n```") - self.plugins[plugin.name] = nil - end - end - - self:fix_optional() - self:rebuild() - - self:fix_cond() - self:rebuild() - - self.dirty = {} - - for _, plugin in pairs(self.plugins) do - local disabled = plugin.enabled == false or (type(plugin.enabled) == "function" and not plugin.enabled()) - if disabled then - plugin._.kind = "disabled" - -- remove all child fragments - self:remove_fragments(plugin.name, { self = false }) - self.plugins[plugin.name] = nil - self.disabled[plugin.name] = plugin - end - end - self:rebuild() - - -- check optional plugins again - self:fix_optional() - self:rebuild() -end - ---@param msg string ---@param level number function Spec:log(msg, level) @@ -378,25 +108,17 @@ function Spec:report(level) end ---@param spec LazySpec|LazySpecImport ----@param results? string[] -function Spec:normalize(spec, results) +function Spec:normalize(spec) if type(spec) == "string" then - if not spec:find("/", 1, true) then - -- spec is a plugin name - if results then - table.insert(results, spec) - end - else - self:add({ spec }, results) - end + self:add({ spec }) elseif #spec > 1 or Util.is_list(spec) then ---@cast spec LazySpec[] for _, s in ipairs(spec) do - self:normalize(s, results) + self:normalize(s) end elseif spec[1] or spec.dir or spec.url then - ---@cast spec LazyPlugin - local plugin = self:add(spec, results) + ---@cast spec LazyPluginSpec + local plugin = self:add(spec) ---@diagnostic disable-next-line: cast-type-mismatch ---@cast plugin LazySpecImport if plugin and plugin.import then @@ -408,7 +130,6 @@ function Spec:normalize(spec, results) else self:error("Invalid plugin spec " .. vim.inspect(spec)) end - return results end ---@param spec LazySpecImport @@ -492,41 +213,6 @@ function Spec:import(spec) end end ----@param old LazyPlugin ----@param new LazyPlugin ----@return LazyPlugin -function Spec:merge(old, new) - new._.dep = old._.dep and new._.dep - - if new.url and old.url and new.url ~= old.url then - self:warn("Two plugins with the same name and different url:\n" .. vim.inspect({ old = old, new = new })) - end - - if new.dependencies and old.dependencies then - Util.extend(new.dependencies, old.dependencies) - end - - local new_dir = new._.dir or old._.dir or (new.name and (Config.options.root .. "/" .. new.name)) or nil - if new_dir ~= old.dir then - local msg = "Plugin `" .. new.name .. "` changed `dir`:\n- from: `" .. old.dir .. "`\n- to: `" .. new_dir .. "`" - if new._.rtp_loaded or old._.rtp_loaded then - msg = msg - .. "\n\nThis plugin was already partially loaded, so we did not change it's `dir`.\nPlease fix your config." - self:error(msg) - new_dir = old.dir - else - self:warn(msg) - end - end - new.dir = new_dir - new._.rtp_loaded = new._.rtp_loaded or old._.rtp_loaded - - new._.super = old - setmetatable(new, { __index = old }) - - return new -end - function M.update_state() ---@type string[] local cloning = {} @@ -631,6 +317,7 @@ function M.load() Config.spec = Spec.new() local specs = { + ---@diagnostic disable-next-line: param-type-mismatch vim.deepcopy(Config.options.spec), } specs[#specs + 1] = M.find_local_spec() @@ -655,10 +342,10 @@ function M.load() for name, plugin in pairs(existing) do if Config.plugins[name] then local dep = Config.plugins[name]._.dep - local super = Config.plugins[name]._.super + local frags = Config.plugins[name]._.frags Config.plugins[name]._ = plugin._ Config.plugins[name]._.dep = dep - Config.plugins[name]._.super = super + Config.plugins[name]._.frags = frags end end Util.track() @@ -725,8 +412,9 @@ function M._values(root, plugin, prop, is_list) if not plugin[prop] then return {} end + local super = getmetatable(plugin) ---@type table - local ret = plugin._.super and M._values(root, plugin._.super, prop, is_list) or {} + local ret = super and M._values(root, super.__index, prop, is_list) or {} local values = rawget(plugin, prop) if not values then @@ -742,6 +430,7 @@ function M._values(root, plugin, prop, is_list) else ---@type {path:string[], list:any[]}[] local lists = {} + ---@diagnostic disable-next-line: no-unknown for _, key in ipairs(plugin[prop .. "_extend"] or {}) do local path = vim.split(key, ".", { plain = true }) local r = Util.key_get(ret, path) diff --git a/lua/lazy/core/util.lua b/lua/lazy/core/util.lua index f72ab21..d4fa47c 100644 --- a/lua/lazy/core/util.lua +++ b/lua/lazy/core/util.lua @@ -93,7 +93,7 @@ function M.pretty_trace(opts) end ---@generic R ----@param fn fun():R +---@param fn fun():R? ---@param opts? string|{msg:string, on_error:fun(msg)} ---@return R function M.try(fn, opts) diff --git a/lua/lazy/health.lua b/lua/lazy/health.lua index 7b06f1d..6202de6 100644 --- a/lua/lazy/health.lua +++ b/lua/lazy/health.lua @@ -59,7 +59,6 @@ function M.check() else for _, plugin in pairs(spec.plugins) do M.check_valid(plugin) - M.check_override(plugin) end if #spec.notifs > 0 then error("Issues were reported when loading your specs:") @@ -88,23 +87,6 @@ function M.check_valid(plugin) end end ----@param plugin LazyPlugin -function M.check_override(plugin) - if not plugin._.super then - return - end - - local Handler = require("lazy.core.handler") - local skip = { "dependencies", "_", "opts", 1 } - vim.list_extend(skip, vim.tbl_values(Handler.types)) - - for key, value in pairs(plugin._.super) do - if not vim.tbl_contains(skip, key) and plugin[key] and plugin[key] ~= value then - warn("{" .. plugin.name .. "}: overriding <" .. key .. ">") - end - end -end - M.valid = { 1, "_", diff --git a/lua/lazy/types.lua b/lua/lazy/types.lua index 831327e..dabfa58 100644 --- a/lua/lazy/types.lua +++ b/lua/lazy/types.lua @@ -2,30 +2,26 @@ ---@alias LazyPluginKind "normal"|"clean"|"disabled" ---@class LazyPluginState ----@field fid number id of the plugin spec fragment ----@field fpid? number parent id of the plugin spec fragment ----@field fdeps? number[] children ids of the fragment ----@field loaded? {[string]:string}|{time:number} ----@field installed? boolean ----@field tasks? LazyTask[] ----@field working? boolean ----@field dirty? boolean ----@field updated? {from:string, to:string} ----@field is_local? boolean ----@field updates? {from:GitInfo, to:GitInfo} ----@field cloned? boolean ----@field outdated? boolean ----@field kind? LazyPluginKind ----@field dep? boolean True if this plugin is only in the spec as a dependency ----@field cond? boolean ----@field super? LazyPlugin ----@field module? string ----@field dir? string Explicit dir or dev set for this plugin ----@field rtp_loaded? boolean ----@field handlers? LazyPluginHandlers ---@field cache? table +---@field cloned? boolean +---@field cond? boolean +---@field dep? boolean True if this plugin is only in the spec as a dependency +---@field dir? string Explicit dir or dev set for this plugin +---@field dirty? boolean +---@field frags? number[] +---@field handlers? LazyPluginHandlers +---@field installed? boolean +---@field is_local? boolean +---@field kind? LazyPluginKind +---@field loaded? {[string]:string}|{time:number} +---@field outdated? boolean ---@field rocks? LazyRock[] ---@field rocks_installed? boolean +---@field rtp_loaded? boolean +---@field tasks? LazyTask[] +---@field updated? {from:string, to:string} +---@field updates? {from:GitInfo, to:GitInfo} +---@field working? boolean ---@alias PluginOpts table|fun(self:LazyPlugin, opts:table):table? @@ -66,6 +62,7 @@ ---@class LazyPlugin: LazyPluginBase,LazyPluginHandlers,LazyPluginHooks,LazyPluginRef ---@field dependencies? string[] +---@field specs? string|string[]|LazyPluginSpec[] ---@field _ LazyPluginState ---@class LazyPluginSpecHandlers @@ -77,6 +74,7 @@ ---@class LazyPluginSpec: LazyPluginBase,LazyPluginSpecHandlers,LazyPluginHooks,LazyPluginRef ---@field dependencies? string|string[]|LazyPluginSpec[] +---@field specs? string|string[]|LazyPluginSpec[] ---@alias LazySpec string|LazyPluginSpec|LazySpecImport|LazySpec[] @@ -85,3 +83,14 @@ ---@field name? string ---@field enabled? boolean|(fun():boolean) ---@field cond? boolean|(fun():boolean) + +---@class LazyFragment +---@field id number +---@field pid? number +---@field deps? number[] +---@field frags? number[] +---@field dep? boolean +---@field name string +---@field url? string +---@field dir? string +---@field spec LazyPlugin diff --git a/tests/core/plugin_spec.lua b/tests/core/plugin_spec.lua index 548b5f5..4a09bb9 100644 --- a/tests/core/plugin_spec.lua +++ b/tests/core/plugin_spec.lua @@ -6,6 +6,10 @@ local assert = require("luassert") Config.setup() +local function inspect(obj) + return vim.inspect(obj):gsub("%s+", " ") +end + ---@param plugins LazyPlugin[]|LazyPlugin local function clean(plugins) local p = plugins @@ -14,6 +18,7 @@ local function clean(plugins) plugin._.fid = nil plugin._.fpid = nil plugin._.fdeps = nil + plugin._.frags = nil if plugin._.dep == false then plugin._.dep = nil end @@ -28,7 +33,7 @@ describe("plugin spec url/name", function() { { "foo/bar" }, { [1] = "foo/bar", name = "bar", url = "https://github.com/foo/bar.git" } }, { { "https://foo.bar" }, { [1] = "https://foo.bar", name = "foo.bar", url = "https://foo.bar" } }, { { "foo/bar", name = "foobar" }, { [1] = "foo/bar", name = "foobar", url = "https://github.com/foo/bar.git" } }, - { { "foo/bar", url = "123" }, { [1] = "foo/bar", name = "123", url = "123" } }, + { { "foo/bar", url = "123" }, { [1] = "foo/bar", name = "bar", url = "123" } }, { { url = "https://foobar" }, { name = "foobar", url = "https://foobar" } }, { { { url = "https://foo", name = "foobar" }, { url = "https://foo" } }, @@ -45,18 +50,22 @@ describe("plugin spec url/name", function() for _, test in ipairs(tests) do test[2]._ = {} - it("parses " .. vim.inspect(test[1]):gsub("%s+", " "), function() + it("parses " .. inspect(test[1]), function() if not test[2].dir then test[2].dir = Config.options.root .. "/" .. test[2].name end local spec = Plugin.Spec.new(test[1]) - local plugins = vim.tbl_values(spec.plugins) - plugins[1]._ = {} + local all = vim.deepcopy(spec.plugins) + local plugins = vim.tbl_values(all) + plugins = vim.tbl_map(function(plugin) + plugin._ = {} + return plugin + end, plugins) local notifs = vim.tbl_filter(function(notif) return notif.level > 3 end, spec.notifs) assert(#notifs == 0, vim.inspect(spec.notifs)) - assert.equal(1, #plugins) + assert.equal(1, #plugins, vim.inspect(all)) plugins[1]._.super = nil assert.same(test[2], plugins[1]) end) @@ -90,7 +99,40 @@ describe("plugin spec dir", function() for _, test in ipairs(tests) do local dir = vim.fn.expand(test[1]) local input = vim.list_slice(test, 2) - it("parses dir " .. vim.inspect(input):gsub("%s+", " "), function() + it("parses dir " .. inspect(input), function() + local spec = Plugin.Spec.new(input) + local plugins = vim.tbl_values(spec.plugins) + assert(spec:report() == 0) + assert.equal(1, #plugins) + assert.same(dir, plugins[1].dir) + end) + end +end) + +describe("plugin dev", function() + local tests = { + { + { "lewis6991/gitsigns.nvim", opts = {}, dev = true }, + { "lewis6991/gitsigns.nvim" }, + }, + { + { "lewis6991/gitsigns.nvim", opts = {}, dev = true }, + { "gitsigns.nvim" }, + }, + { + { "lewis6991/gitsigns.nvim", opts = {} }, + { "lewis6991/gitsigns.nvim", dev = true }, + }, + { + { "lewis6991/gitsigns.nvim", opts = {} }, + { "gitsigns.nvim", dev = true }, + }, + } + + for _, test in ipairs(tests) do + local dir = vim.fn.expand("~/projects/gitsigns.nvim") + local input = test + it("parses dir " .. inspect(input), function() local spec = Plugin.Spec.new(input) local plugins = vim.tbl_values(spec.plugins) assert(spec:report() == 0) @@ -126,7 +168,7 @@ describe("plugin spec opt", function() for _, plugin in pairs(spec.plugins) do plugin.dir = nil end - assert.same(clean(spec.plugins), { + assert.same({ bar = { "foo/bar", _ = {}, @@ -150,7 +192,7 @@ describe("plugin spec opt", function() name = "dep2", url = "https://github.com/foo/dep2.git", }, - }) + }, clean(spec.plugins)) end end) @@ -369,45 +411,45 @@ describe("plugin spec opt", function() end) describe("plugin opts", function() - it("correctly parses opts", function() - ---@type {spec:LazySpec, opts:table}[] - local tests = { - { - spec = { { "foo/foo", opts = { a = 1, b = 1 } }, { "foo/foo", opts = { a = 2 } } }, - opts = { a = 2, b = 1 }, - }, - { - spec = { { "foo/foo", config = { a = 1, b = 1 } }, { "foo/foo", opts = { a = 2 } } }, - opts = { a = 2, b = 1 }, - }, - { - spec = { { "foo/foo", opts = { a = 1, b = 1 } }, { "foo/foo", config = { a = 2 } } }, - opts = { a = 2, b = 1 }, - }, - { - spec = { { "foo/foo", config = { a = 1, b = 1 } }, { "foo/foo", config = { a = 2 } } }, - opts = { a = 2, b = 1 }, - }, - { - spec = { { "foo/foo", config = { a = 1, b = 1 } }, { "foo/foo", config = { a = vim.NIL } } }, - opts = { b = 1 }, - }, - { - spec = { { "foo/foo", config = { a = 1, b = 1 } }, { "foo/foo" } }, - opts = { a = 1, b = 1 }, - }, - { - spec = { { "foo/foo" }, { "foo/foo" } }, - opts = {}, - }, - } + ---@type {spec:LazySpec, opts:table}[] + local tests = { + { + spec = { { "foo/foo", opts = { a = 1, b = 1 } }, { "foo/foo", opts = { a = 2 } } }, + opts = { a = 2, b = 1 }, + }, + { + spec = { { "foo/foo", config = { a = 1, b = 1 } }, { "foo/foo", opts = { a = 2 } } }, + opts = { a = 2, b = 1 }, + }, + { + spec = { { "foo/foo", opts = { a = 1, b = 1 } }, { "foo/foo", config = { a = 2 } } }, + opts = { a = 2, b = 1 }, + }, + { + spec = { { "foo/foo", config = { a = 1, b = 1 } }, { "foo/foo", config = { a = 2 } } }, + opts = { a = 2, b = 1 }, + }, + { + spec = { { "foo/foo", config = { a = 1, b = 1 } }, { "foo/foo", config = { a = vim.NIL } } }, + opts = { b = 1 }, + }, + { + spec = { { "foo/foo", config = { a = 1, b = 1 } }, { "foo/foo" } }, + opts = { a = 1, b = 1 }, + }, + { + spec = { { "foo/foo" }, { "foo/foo" } }, + opts = {}, + }, + } - for _, test in ipairs(tests) do + for _, test in ipairs(tests) do + it("correctly parses opts for " .. inspect(test.spec), function() local spec = Plugin.Spec.new(test.spec) assert(spec.plugins.foo) assert.same(test.opts, Plugin.values(spec.plugins.foo, "opts")) - end - end) + end) + end end) describe("plugin spec", function() diff --git a/tests/handlers/keys_spec.lua b/tests/handlers/keys_spec.lua index 6254db8..d6a9df4 100644 --- a/tests/handlers/keys_spec.lua +++ b/tests/handlers/keys_spec.lua @@ -1,3 +1,4 @@ +---@module 'luassert' local Keys = require("lazy.core.handler.keys") describe("keys", function()