feat(plugin): dont include plugin spec fragments for disabled or optional plugins (#1058)

* feat(plugin): dont include plugin spec fragments for disabled or optional plugins

* test: fixed tests

* fix(plugin): calculate handlers after disabling plugins

* fix(plugin): clear Plugin._.super when rebuilding

* fix(ui): dont process handlers for disabled plugins

* test: added tests for disabling fragments

* fix(plugin): ignore any installed deps of a disabled conditional plugin. Fixes #1053
This commit is contained in:
Folke Lemaitre 2023-09-29 16:11:56 +02:00 committed by GitHub
parent 6b55e4695a
commit f3c7169dd6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 181 additions and 80 deletions

View File

@ -8,22 +8,30 @@ M.loading = false
---@class LazySpecLoader ---@class LazySpecLoader
---@field plugins table<string, LazyPlugin> ---@field plugins table<string, LazyPlugin>
---@field fragments table<number, LazyPlugin>
---@field disabled table<string, LazyPlugin> ---@field disabled table<string, LazyPlugin>
---@field dirty table<string, true>
---@field ignore_installed table<string, true>
---@field modules string[] ---@field modules string[]
---@field notifs {msg:string, level:number, file?:string}[] ---@field notifs {msg:string, level:number, file?:string}[]
---@field importing? string ---@field importing? string
---@field optional? boolean ---@field optional? boolean
local Spec = {} local Spec = {}
M.Spec = Spec M.Spec = Spec
M.last_fid = 0
M.fid_stack = {} ---@type number[]
---@param spec? LazySpec ---@param spec? LazySpec
---@param opts? {optional?:boolean} ---@param opts? {optional?:boolean}
function Spec.new(spec, opts) function Spec.new(spec, opts)
local self = setmetatable({}, { __index = Spec }) local self = setmetatable({}, { __index = Spec })
self.plugins = {} self.plugins = {}
self.fragments = {}
self.disabled = {} self.disabled = {}
self.modules = {} self.modules = {}
self.dirty = {}
self.notifs = {} self.notifs = {}
self.ignore_installed = {}
self.optional = opts and opts.optional self.optional = opts and opts.optional
if spec then if spec then
self:parse(spec) self:parse(spec)
@ -33,6 +41,7 @@ end
function Spec:parse(spec) function Spec:parse(spec)
self:normalize(spec) self:normalize(spec)
self:fix_disabled()
-- calculate handlers -- calculate handlers
for _, plugin in pairs(self.plugins) do for _, plugin in pairs(self.plugins) do
@ -42,8 +51,6 @@ function Spec:parse(spec)
end end
end end
end end
self:fix_disabled()
end end
-- PERF: optimized code to get package name without using lua patterns -- PERF: optimized code to get package name without using lua patterns
@ -56,8 +63,7 @@ end
---@param plugin LazyPlugin ---@param plugin LazyPlugin
---@param results? string[] ---@param results? string[]
---@param is_dep? boolean function Spec:add(plugin, results)
function Spec:add(plugin, results, is_dep)
-- check if we already processed this spec. Can happen when a user uses the same instance of a spec in multiple specs -- 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 -- see https://github.com/folke/lazy.nvim/issues/45
if rawget(plugin, "_") then if rawget(plugin, "_") then
@ -124,10 +130,28 @@ function Spec:add(plugin, results, is_dep)
plugin.config = nil plugin.config = nil
end end
plugin._ = {} local fpid = M.fid_stack[#M.fid_stack]
plugin._.dep = is_dep
M.last_fid = M.last_fid + 1
plugin._ = {
fid = M.last_fid,
fpid = fpid,
dep = fpid ~= nil,
}
self.fragments[plugin._.fid] = plugin
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
plugin.dependencies = plugin.dependencies and self:normalize(plugin.dependencies, {}, true) or nil
if self.plugins[plugin.name] then if self.plugins[plugin.name] then
plugin = self:merge(self.plugins[plugin.name], plugin) plugin = self:merge(self.plugins[plugin.name], plugin)
end end
@ -146,29 +170,75 @@ function Spec:warn(msg)
self:log(msg, vim.log.levels.WARN) self:log(msg, vim.log.levels.WARN)
end end
---@param gathered_deps string[] --- Rebuilds a plugin spec excluding any removed fragments
---@param dep_of table<string,string[]> ---@param name string
---@param on_disable fun(string):nil function Spec:rebuild(name)
function Spec:fix_dependencies(gathered_deps, dep_of, on_disable) local plugin = self.plugins[name]
local function should_disable(dep_name) if not plugin then
for _, parent in ipairs(dep_of[dep_name] or {}) do return
if self.plugins[parent] then
return false
end
end
return true
end end
for _, dep_name in ipairs(gathered_deps) do local fragments = {} ---@type LazyPlugin[]
-- 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 repeat
-- check if the dep is still used by another plugin local super = plugin._.super
if should_disable(dep_name) then if self.fragments[plugin._.fid] then
-- disable the dep when no longer needed plugin._.dep = plugin._.fpid ~= nil
on_disable(dep_name) 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 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
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 end
function Spec:fix_cond() function Spec:fix_cond()
@ -179,14 +249,20 @@ function Spec:fix_cond()
end end
if cond == false or (type(cond) == "function" and not cond(plugin)) then if cond == false or (type(cond) == "function" and not cond(plugin)) then
plugin._.cond = false plugin._.cond = false
local stack = { plugin }
while #stack > 0 do
local p = table.remove(stack)
for _, dep in ipairs(p.dependencies or {}) do
table.insert(stack, self.plugins[dep])
end
self.ignore_installed[p.name] = true
end
plugin.enabled = false plugin.enabled = false
end end
end end
end end
---@return string[]
function Spec:fix_optional() function Spec:fix_optional()
local all_optional_deps = {}
if not self.optional then if not self.optional then
---@param plugin LazyPlugin ---@param plugin LazyPlugin
local function all_optional(plugin) local function all_optional(plugin)
@ -196,14 +272,12 @@ function Spec:fix_optional()
-- handle optional plugins -- handle optional plugins
for _, plugin in pairs(self.plugins) do for _, plugin in pairs(self.plugins) do
if plugin.optional and all_optional(plugin) then if plugin.optional and all_optional(plugin) then
-- remove all optional fragments
self:remove_fragments(plugin.name, { self = true })
self.plugins[plugin.name] = nil self.plugins[plugin.name] = nil
if plugin.dependencies then
vim.list_extend(all_optional_deps, plugin.dependencies)
end end
end end
end end
end
return all_optional_deps
end end
function Spec:fix_disabled() function Spec:fix_disabled()
@ -214,44 +288,24 @@ function Spec:fix_disabled()
end end
end end
---@type table<string,string[]> plugin to parent plugin self:fix_optional()
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() self:fix_cond()
for _, plugin in pairs(self.plugins) do for _, plugin in pairs(self.plugins) do
local enabled = not (plugin.enabled == false or (type(plugin.enabled) == "function" and not plugin.enabled())) local disabled = plugin.enabled == false or (type(plugin.enabled) == "function" and not plugin.enabled())
if enabled then if disabled 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" plugin._.kind = "disabled"
-- remove all child fragments
self:remove_fragments(plugin.name, { self = false })
self.plugins[plugin.name] = nil self.plugins[plugin.name] = nil
self.disabled[plugin.name] = plugin self.disabled[plugin.name] = plugin
if plugin.dependencies then
vim.list_extend(disabled_deps, plugin.dependencies)
end
end end
end end
-- fix deps of plugins that are completely optional -- rebuild any plugin specs that were modified
self:fix_dependencies(all_optional_deps, dep_of, function(dep_name) for name, _ in pairs(self.dirty) do
self.plugins[dep_name] = nil self:rebuild(name)
end) 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 end
---@param msg string ---@param msg string
@ -272,24 +326,24 @@ end
---@param spec LazySpec|LazySpecImport ---@param spec LazySpec|LazySpecImport
---@param results? string[] ---@param results? string[]
---@param is_dep? boolean ---@param is_dep? boolean
function Spec:normalize(spec, results, is_dep) function Spec:normalize(spec, results)
if type(spec) == "string" then if type(spec) == "string" then
if is_dep and not spec:find("/", 1, true) then if not spec:find("/", 1, true) then
-- spec is a plugin name -- spec is a plugin name
if results then if results then
table.insert(results, spec) table.insert(results, spec)
end end
else else
self:add({ spec }, results, is_dep) self:add({ spec }, results)
end end
elseif #spec > 1 or Util.is_list(spec) then elseif #spec > 1 or Util.is_list(spec) then
---@cast spec LazySpec[] ---@cast spec LazySpec[]
for _, s in ipairs(spec) do for _, s in ipairs(spec) do
self:normalize(s, results, is_dep) self:normalize(s, results)
end end
elseif spec[1] or spec.dir or spec.url then elseif spec[1] or spec.dir or spec.url then
---@cast spec LazyPlugin ---@cast spec LazyPlugin
local plugin = self:add(spec, results, is_dep) local plugin = self:add(spec, results)
---@diagnostic disable-next-line: cast-type-mismatch ---@diagnostic disable-next-line: cast-type-mismatch
---@cast plugin LazySpecImport ---@cast plugin LazySpecImport
if plugin and plugin.import then if plugin and plugin.import then
@ -425,10 +479,8 @@ function M.update_state()
end end
end end
for _, plugin in pairs(Config.spec.disabled) do for name in pairs(Config.spec.ignore_installed) do
if plugin._.cond == false then installed[name] = nil
installed[plugin.name] = nil
end
end end
Config.to_clean = {} Config.to_clean = {}

View File

@ -2,12 +2,15 @@
---@alias LazyPluginKind "normal"|"clean"|"disabled" ---@alias LazyPluginKind "normal"|"clean"|"disabled"
---@class LazyPluginState ---@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 loaded? {[string]:string}|{time:number}
---@field installed boolean ---@field installed? boolean
---@field tasks? LazyTask[] ---@field tasks? LazyTask[]
---@field dirty? boolean ---@field dirty? boolean
---@field updated? {from:string, to:string} ---@field updated? {from:string, to:string}
---@field is_local boolean ---@field is_local? boolean
---@field updates? {from:GitInfo, to:GitInfo} ---@field updates? {from:GitInfo, to:GitInfo}
---@field cloned? boolean ---@field cloned? boolean
---@field kind? LazyPluginKind ---@field kind? LazyPluginKind

View File

@ -411,6 +411,7 @@ function M:plugin(plugin)
else else
self:append(" ") self:append(" ")
local reason = {} local reason = {}
if plugin._.kind ~= "disabled" then
for handler in pairs(Handler.types) do for handler in pairs(Handler.types) do
if plugin[handler] then if plugin[handler] then
local trigger = {} local trigger = {}
@ -420,6 +421,7 @@ function M:plugin(plugin)
reason[handler] = table.concat(trigger, " ") reason[handler] = table.concat(trigger, " ")
end end
end end
end
for _, other in pairs(Config.plugins) do for _, other in pairs(Config.plugins) do
if vim.tbl_contains(other.dependencies or {}, plugin.name) then if vim.tbl_contains(other.dependencies or {}, plugin.name) then
reason.plugin = other.name reason.plugin = other.name

View File

@ -1,11 +1,25 @@
local Config = require("lazy.core.config") local Config = require("lazy.core.config")
local Plugin = require("lazy.core.plugin") local Plugin = require("lazy.core.plugin")
local Loader = require("lazy.core.loader")
local assert = require("luassert") local assert = require("luassert")
Config.setup() Config.setup()
---@param plugins LazyPlugin[]|LazyPlugin
local function clean(plugins)
local p = plugins
plugins = type(plugins) == "table" and plugins or { plugins }
for _, plugin in pairs(plugins) do
plugin._.fid = nil
plugin._.fpid = nil
plugin._.fdeps = nil
if plugin._.dep == false then
plugin._.dep = nil
end
end
return p
end
describe("plugin spec url/name", function() describe("plugin spec url/name", function()
local tests = { local tests = {
{ { dir = "~/foo" }, { name = "foo", dir = vim.fn.fnamemodify("~/foo", ":p") } }, { { dir = "~/foo" }, { name = "foo", dir = vim.fn.fnamemodify("~/foo", ":p") } },
@ -28,6 +42,7 @@ describe("plugin spec url/name", function()
end end
local spec = Plugin.Spec.new(test[1]) local spec = Plugin.Spec.new(test[1])
local plugins = vim.tbl_values(spec.plugins) local plugins = vim.tbl_values(spec.plugins)
plugins[1]._ = {}
assert(#spec.notifs == 0) assert(#spec.notifs == 0)
assert.equal(1, #plugins) assert.equal(1, #plugins)
assert.same(test[2], plugins[1]) assert.same(test[2], plugins[1])
@ -61,7 +76,7 @@ describe("plugin spec opt", function()
for _, plugin in pairs(spec.plugins) do for _, plugin in pairs(spec.plugins) do
plugin.dir = nil plugin.dir = nil
end end
assert.same(spec.plugins, { assert.same(clean(spec.plugins), {
bar = { bar = {
"foo/bar", "foo/bar",
_ = {}, _ = {},
@ -105,7 +120,7 @@ describe("plugin spec opt", function()
for _, plugin in pairs(spec.plugins) do for _, plugin in pairs(spec.plugins) do
plugin.dir = nil plugin.dir = nil
end end
assert.same(spec.plugins, { assert.same(clean(spec.plugins), {
bar = { bar = {
"foo/bar", "foo/bar",
_ = {}, _ = {},
@ -335,3 +350,32 @@ describe("plugin opts", function()
end end
end) end)
end) end)
describe("plugin spec", function()
it("only includes fragments from enabled plugins", function()
local tests = {
{
spec = {
{ "foo/disabled", enabled = false, dependencies = { "foo/bar", opts = { key_disabled = true } } },
{ "foo/disabled", dependencies = { "foo/bar", opts = { key_disabled_two = true } } },
{ "foo/conditional", cond = false, dependencies = { "foo/bar", opts = { key_cond = true } } },
{ "foo/optional", optional = true, dependencies = { "foo/bar", opts = { key_optional = true } } },
{ "foo/active", dependencies = { "foo/bar", opts = { key_active = true } } },
{
"foo/bar",
opts = { key = true },
},
},
expected_opts = { key = true, key_active = true },
}, -- for now, one test...
}
for _, test in ipairs(tests) do
local spec = Plugin.Spec.new(test.spec)
assert(#spec.notifs == 0)
assert(vim.tbl_count(spec.plugins) == 2)
assert(spec.plugins.active)
assert(spec.plugins.bar)
assert.same(test.expected_opts, Plugin.values(spec.plugins.bar, "opts"))
end
end)
end)