mirror of
https://github.com/folke/lazy.nvim.git
synced 2025-06-28 19:34:15 +00:00
Plugins that are unused currently can still contribute to active plugins by the specs added to their "dependencies" field. This commit adds functionality to the "parse" method, repairing only the active plugins that could be affected by the aforementioned specs. This can be achieved by maintaining an extra administration table containing a copy of each added plugin instance.
611 lines
17 KiB
Lua
611 lines
17 KiB
Lua
local Config = require("lazy.core.config")
|
|
local Util = require("lazy.core.util")
|
|
local Handler = require("lazy.core.handler")
|
|
|
|
---@class LazyCorePlugin
|
|
local M = {}
|
|
M.loading = false
|
|
|
|
---@class LazySpecLoader
|
|
---@field repair_info table<string,LazyPlugin[]>
|
|
---@field plugins table<string, LazyPlugin>
|
|
---@field optional_only table<string, LazyPlugin>
|
|
---@field disabled table<string, LazyPlugin>
|
|
---@field modules string[]
|
|
---@field notifs {msg:string, level:number, file?:string}[]
|
|
---@field importing? string
|
|
---@field optional? boolean
|
|
local Spec = {}
|
|
M.Spec = Spec
|
|
|
|
---@param spec? LazySpec
|
|
---@param opts? {optional?:boolean}
|
|
function Spec.new(spec, opts)
|
|
local self = setmetatable({}, { __index = Spec })
|
|
self.repair_info = {}
|
|
self.plugins = {}
|
|
self.optional_only = {}
|
|
self.disabled = {}
|
|
self.modules = {}
|
|
self.notifs = {}
|
|
self.optional = opts and opts.optional
|
|
if spec then
|
|
self:parse(spec)
|
|
end
|
|
return self
|
|
end
|
|
|
|
function Spec:parse(spec)
|
|
-- spec -> self.plugins
|
|
self:normalize(spec)
|
|
-- self.plugins -> self.optional_only, self.disabled
|
|
self:fix_disabled()
|
|
|
|
if self:has_unused_plugins() then
|
|
self:fix_active()
|
|
end
|
|
|
|
self:calculate_handlers(self.plugins)
|
|
self:calculate_handlers(self.disabled)
|
|
end
|
|
|
|
function Spec:has_unused_plugins()
|
|
return not (vim.tbl_isempty(self.optional_only) and vim.tbl_isempty(self.disabled))
|
|
end
|
|
|
|
function Spec:fix_active()
|
|
---@type table<string,LazyPlugin>
|
|
local unused_plugins = vim.tbl_extend("keep", self.optional_only, self.disabled)
|
|
|
|
---@type string[]
|
|
local candidates = {}
|
|
for _, plugin in pairs(unused_plugins) do
|
|
if plugin.dependencies then
|
|
vim.list_extend(candidates, plugin.dependencies)
|
|
end
|
|
end
|
|
|
|
-- merge candidate again without instances supplied by unused plugins
|
|
for _, candidate in pairs(candidates) do
|
|
local fix = false
|
|
local instances = vim.tbl_filter(function(instance)
|
|
if instance._.parent_name and unused_plugins[instance._.parent_name] then
|
|
fix = true
|
|
return false
|
|
end
|
|
return instance
|
|
end, self.repair_info[candidate])
|
|
if fix then
|
|
self.plugins[candidate] = self:fix_merge(instances)
|
|
end
|
|
end
|
|
end
|
|
|
|
---@param instances_of_plugin LazyPlugin[]
|
|
---@return LazyPlugin
|
|
function Spec:fix_merge(instances_of_plugin)
|
|
local last
|
|
for index, inst in ipairs(instances_of_plugin) do
|
|
if index == 1 then
|
|
last = inst
|
|
else
|
|
self:merge(last, inst)
|
|
last = inst
|
|
end
|
|
end
|
|
return last
|
|
end
|
|
|
|
---@param plugins table<string, LazyPlugin>
|
|
function Spec:calculate_handlers(plugins)
|
|
for _, plugin in pairs(plugins) do
|
|
for _, handler in pairs(Handler.types) do
|
|
if plugin[handler] then
|
|
plugin[handler] = M.values(plugin, handler, true)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- PERF: optimized code to get package name without using lua patterns
|
|
function Spec.get_name(pkg)
|
|
local name = pkg:sub(-4) == ".git" and pkg:sub(1, -5) or pkg
|
|
name = name:sub(-1) == "/" and name:sub(1, -2) or name
|
|
local slash = name:reverse():find("/", 1, true) --[[@as number?]]
|
|
return slash and name:sub(#name - slash + 2) or pkg:gsub("%W+", "_")
|
|
end
|
|
|
|
---@param plugin LazyPlugin
|
|
---@param results? string[]
|
|
---@param parent_name? string
|
|
function Spec:add(plugin, results, parent_name)
|
|
-- 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
|
|
|
|
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
|
|
|
|
if plugin.dir then
|
|
plugin.dir = Util.norm(plugin.dir)
|
|
-- local plugin
|
|
plugin.name = plugin.name or Spec.get_name(plugin.dir)
|
|
elseif plugin.url then
|
|
plugin.name = plugin.name or Spec.get_name(plugin.url)
|
|
-- 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
|
|
-- dev plugins
|
|
if
|
|
plugin.dev
|
|
and (not Config.options.dev.fallback or vim.fn.isdirectory(Config.options.dev.path .. "/" .. plugin.name) == 1)
|
|
then
|
|
plugin.dir = Config.options.dev.path .. "/" .. plugin.name
|
|
else
|
|
-- remote plugin
|
|
plugin.dir = Config.options.root .. "/" .. plugin.name
|
|
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
|
|
|
|
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
|
|
|
|
plugin._ = {}
|
|
plugin._.dep = (parent_name ~= nil) or nil
|
|
|
|
plugin.dependencies = plugin.dependencies and self:normalize(plugin.dependencies, {}, plugin.name) or nil
|
|
self:add_to_repair_info(plugin, parent_name)
|
|
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
|
|
|
|
---@param plugin LazyPlugin
|
|
---@param parent_name? string
|
|
function Spec:add_to_repair_info(plugin, parent_name)
|
|
local copy = vim.deepcopy(plugin) -- copy the instance of the plugin
|
|
copy._.parent_name = parent_name
|
|
self.repair_info[copy.name] = self.repair_info[copy.name] or {}
|
|
table.insert(self.repair_info[copy.name], copy)
|
|
end
|
|
|
|
function Spec:error(msg)
|
|
self:log(msg, vim.log.levels.ERROR)
|
|
end
|
|
|
|
function Spec:warn(msg)
|
|
self:log(msg, vim.log.levels.WARN)
|
|
end
|
|
|
|
---@param gathered_deps string[]
|
|
---@param dep_of table<string,string[]>
|
|
---@param on_disable fun(string):nil
|
|
function Spec:fix_dependencies(gathered_deps, dep_of, on_disable)
|
|
local function should_disable(dep_name)
|
|
for _, parent in ipairs(dep_of[dep_name] or {}) do
|
|
if self.plugins[parent] then
|
|
return false
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
for _, dep_name in ipairs(gathered_deps) do
|
|
-- only check if the plugin is still enabled and it is a dep
|
|
if self.plugins[dep_name] and self.plugins[dep_name]._.dep then
|
|
-- check if the dep is still used by another plugin
|
|
if should_disable(dep_name) then
|
|
-- disable the dep when no longer needed
|
|
on_disable(dep_name)
|
|
end
|
|
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
|
|
plugin.enabled = false
|
|
end
|
|
end
|
|
end
|
|
|
|
---@return string[]
|
|
function Spec:fix_optional()
|
|
local all_optional_deps = {}
|
|
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
|
|
self.plugins[plugin.name] = nil
|
|
self.optional_only[plugin.name] = plugin
|
|
if plugin.dependencies then
|
|
vim.list_extend(all_optional_deps, plugin.dependencies)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return all_optional_deps
|
|
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
|
|
|
|
---@type table<string,string[]> plugin to parent plugin
|
|
local dep_of = {}
|
|
|
|
---@type string[] dependencies of disabled plugins
|
|
local disabled_deps = {}
|
|
|
|
---@type string[] dependencies of plugins that are completely optional
|
|
local all_optional_deps = self:fix_optional()
|
|
self:fix_cond()
|
|
|
|
for _, plugin in pairs(self.plugins) do
|
|
local enabled = not (plugin.enabled == false or (type(plugin.enabled) == "function" and not plugin.enabled()))
|
|
if enabled then
|
|
for _, dep in ipairs(plugin.dependencies or {}) do
|
|
dep_of[dep] = dep_of[dep] or {}
|
|
table.insert(dep_of[dep], plugin.name)
|
|
end
|
|
else
|
|
plugin._.kind = "disabled"
|
|
self.plugins[plugin.name] = nil
|
|
self.disabled[plugin.name] = plugin
|
|
if plugin.dependencies then
|
|
vim.list_extend(disabled_deps, plugin.dependencies)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- fix deps of plugins that are completely optional
|
|
self:fix_dependencies(all_optional_deps, dep_of, function(dep_name)
|
|
local plugin = self.plugins[dep_name]
|
|
self.plugins[dep_name] = nil
|
|
self.optional_only[dep_name] = plugin
|
|
end)
|
|
-- fix deps of disabled plugins
|
|
self:fix_dependencies(disabled_deps, dep_of, function(dep_name)
|
|
local plugin = self.plugins[dep_name]
|
|
plugin._.kind = "disabled"
|
|
self.plugins[plugin.name] = nil
|
|
self.disabled[plugin.name] = plugin
|
|
end)
|
|
end
|
|
|
|
---@param msg string
|
|
---@param level number
|
|
function Spec:log(msg, level)
|
|
self.notifs[#self.notifs + 1] = { msg = msg, level = level, file = self.importing }
|
|
end
|
|
|
|
function Spec:report(level)
|
|
level = level or vim.log.levels.ERROR
|
|
for _, notif in ipairs(self.notifs) do
|
|
if notif.level >= level then
|
|
Util.notify(notif.msg, { level = notif.level })
|
|
end
|
|
end
|
|
end
|
|
|
|
---@param spec LazySpec|LazySpecImport
|
|
---@param results? string[]
|
|
---@param parent_name? string
|
|
function Spec:normalize(spec, results, parent_name)
|
|
local is_dep = parent_name ~= nil
|
|
if type(spec) == "string" then
|
|
if is_dep and not spec:find("/", 1, true) then
|
|
-- spec is a plugin name
|
|
if results then
|
|
table.insert(results, spec)
|
|
end
|
|
else
|
|
self:add({ spec }, results, parent_name)
|
|
end
|
|
elseif #spec > 1 or Util.is_list(spec) then
|
|
---@cast spec LazySpec[]
|
|
for _, s in ipairs(spec) do
|
|
self:normalize(s, results, parent_name)
|
|
end
|
|
elseif spec[1] or spec.dir or spec.url then
|
|
---@cast spec LazyPlugin
|
|
local plugin = self:add(spec, results, parent_name)
|
|
---@diagnostic disable-next-line: cast-type-mismatch
|
|
---@cast plugin LazySpecImport
|
|
if plugin and plugin.import then
|
|
self:import(plugin)
|
|
end
|
|
elseif spec.import then
|
|
---@cast spec LazySpecImport
|
|
self:import(spec)
|
|
else
|
|
self:error("Invalid plugin spec " .. vim.inspect(spec))
|
|
end
|
|
return results
|
|
end
|
|
|
|
---@param spec LazySpecImport
|
|
function Spec:import(spec)
|
|
if spec.import == "lazy" then
|
|
return self:error("You can't name your plugins module `lazy`.")
|
|
end
|
|
if type(spec.import) ~= "string" then
|
|
return self:error("Invalid import spec. `import` should be a string: " .. vim.inspect(spec))
|
|
end
|
|
if vim.tbl_contains(self.modules, spec.import) then
|
|
return
|
|
end
|
|
if spec.enabled == false or (type(spec.enabled) == "function" and not spec.enabled()) then
|
|
return
|
|
end
|
|
|
|
self.modules[#self.modules + 1] = spec.import
|
|
|
|
local imported = 0
|
|
|
|
---@type string[]
|
|
local modnames = {}
|
|
Util.lsmod(spec.import, function(modname)
|
|
modnames[#modnames + 1] = modname
|
|
end)
|
|
table.sort(modnames)
|
|
|
|
for _, modname in ipairs(modnames) do
|
|
imported = imported + 1
|
|
Util.track({ import = modname })
|
|
self.importing = modname
|
|
-- unload the module so we get a clean slate
|
|
---@diagnostic disable-next-line: no-unknown
|
|
package.loaded[modname] = nil
|
|
Util.try(function()
|
|
local mod = require(modname)
|
|
if type(mod) ~= "table" then
|
|
self.importing = nil
|
|
return self:error(
|
|
"Invalid spec module: `"
|
|
.. modname
|
|
.. "`\nExpected a `table` of specs, but a `"
|
|
.. type(mod)
|
|
.. "` was returned instead"
|
|
)
|
|
end
|
|
self:normalize(mod)
|
|
self.importing = nil
|
|
Util.track()
|
|
end, {
|
|
msg = "Failed to load `" .. modname .. "`",
|
|
on_error = function(msg)
|
|
self:error(msg)
|
|
self.importing = nil
|
|
Util.track()
|
|
end,
|
|
})
|
|
end
|
|
if imported == 0 then
|
|
self:error("No specs found for module " .. spec.import)
|
|
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:error("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
|
|
|
|
new._.super = old
|
|
setmetatable(new, { __index = old })
|
|
|
|
return new
|
|
end
|
|
|
|
function M.update_state()
|
|
---@type string[]
|
|
local cloning = {}
|
|
|
|
---@type table<string,FileType>
|
|
local installed = {}
|
|
Util.ls(Config.options.root, function(_, name, type)
|
|
if type == "directory" and name ~= "readme" then
|
|
installed[name] = type
|
|
elseif type == "file" and name:sub(-8) == ".cloning" then
|
|
name = name:sub(1, -9)
|
|
cloning[#cloning + 1] = name
|
|
end
|
|
end)
|
|
|
|
for _, failed in ipairs(cloning) do
|
|
installed[failed] = nil
|
|
end
|
|
|
|
for _, plugin in pairs(Config.plugins) do
|
|
plugin._ = plugin._ or {}
|
|
if plugin.lazy == nil then
|
|
local lazy = plugin._.dep
|
|
or Config.options.defaults.lazy
|
|
or plugin.event
|
|
or plugin.keys
|
|
or plugin.ft
|
|
or plugin.cmd
|
|
plugin.lazy = lazy and true or false
|
|
end
|
|
if plugin.dir:find(Config.options.root, 1, true) == 1 then
|
|
plugin._.installed = installed[plugin.name] ~= nil
|
|
installed[plugin.name] = nil
|
|
else
|
|
plugin._.is_local = true
|
|
plugin._.installed = true -- local plugins are managed by the user
|
|
end
|
|
end
|
|
|
|
for _, plugin in pairs(Config.spec.disabled) do
|
|
if plugin._.cond == false then
|
|
installed[plugin.name] = nil
|
|
end
|
|
end
|
|
|
|
Config.to_clean = {}
|
|
for pack, dir_type in pairs(installed) do
|
|
table.insert(Config.to_clean, {
|
|
name = pack,
|
|
dir = Config.options.root .. "/" .. pack,
|
|
_ = {
|
|
kind = "clean",
|
|
installed = true,
|
|
is_symlink = dir_type == "link",
|
|
is_local = dir_type == "link",
|
|
},
|
|
})
|
|
end
|
|
end
|
|
|
|
function M.load()
|
|
M.loading = true
|
|
-- load specs
|
|
Util.track("spec")
|
|
Config.spec = Spec.new()
|
|
Config.spec:parse({ vim.deepcopy(Config.options.spec), { "folke/lazy.nvim" } })
|
|
|
|
-- override some lazy props
|
|
local lazy = Config.spec.plugins["lazy.nvim"]
|
|
if lazy then
|
|
lazy.lazy = true
|
|
lazy.dir = Config.me
|
|
lazy.config = function()
|
|
error("lazy config should not be called")
|
|
end
|
|
lazy._.loaded = {}
|
|
end
|
|
|
|
local existing = Config.plugins
|
|
Config.plugins = Config.spec.plugins
|
|
-- copy state. This wont do anything during startup
|
|
for name, plugin in pairs(existing) do
|
|
if Config.plugins[name] then
|
|
local dep = Config.plugins[name]._.dep
|
|
local super = Config.plugins[name]._.super
|
|
Config.plugins[name]._ = plugin._
|
|
Config.plugins[name]._.dep = dep
|
|
Config.plugins[name]._.super = super
|
|
end
|
|
end
|
|
Util.track()
|
|
|
|
Util.track("state")
|
|
M.update_state()
|
|
Util.track()
|
|
M.loading = false
|
|
vim.api.nvim_exec_autocmds("User", { pattern = "LazyPlugins", modeline = false })
|
|
end
|
|
|
|
-- Finds the plugin that has this path
|
|
---@param path string
|
|
function M.find(path)
|
|
if not Config.spec then
|
|
return
|
|
end
|
|
local lua = path:find("/lua/", 1, true)
|
|
if lua then
|
|
local name = path:sub(1, lua - 1)
|
|
local slash = name:reverse():find("/", 1, true)
|
|
if slash then
|
|
name = name:sub(#name - slash + 2)
|
|
return name and Config.plugins[name] or Config.spec.plugins[name] or nil
|
|
end
|
|
end
|
|
end
|
|
|
|
---@param plugin LazyPlugin
|
|
function M.has_errors(plugin)
|
|
for _, task in ipairs(plugin._.tasks or {}) do
|
|
if task.error then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Merges super values or runs the values function to override values or return new ones
|
|
-- Used for opts, cmd, event, ft and keys
|
|
---@param plugin LazyPlugin
|
|
---@param prop string
|
|
---@param is_list? boolean
|
|
function M.values(plugin, prop, is_list)
|
|
---@type table
|
|
local ret = plugin._.super and M.values(plugin._.super, prop, is_list) or {}
|
|
local values = rawget(plugin, prop)
|
|
|
|
if not values then
|
|
return ret
|
|
elseif type(values) == "function" then
|
|
ret = values(plugin, ret) or ret
|
|
return type(ret) == "table" and ret or { ret }
|
|
end
|
|
|
|
values = type(values) == "table" and values or { values }
|
|
return is_list and Util.extend(ret, values) or Util.merge(ret, values)
|
|
end
|
|
|
|
return M
|