8000 fix(module_loader)!: load only the plugin holding a required module by zuqini · Pull Request #53 · zuqini/zpack.nvim · GitHub
[go: up one dir, main page]

Skip to content
Merged
Diff view
Diff view
11 changes: 9 additions & 2 deletions doc/zpack.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1048,8 +1048,15 @@ module (boolean, optional)
loaded when its modules are `require()`d.

zpack automatically loads lazy plugins when their modules are
required: the first plugin holding the module, or failing that
the only pending plugin with its top-level name under `/lua`.
required: a pending plugin holding the module's file under
`/lua` (`.lua`, `/init.lua`, or a compiled `.so` or `.dll`).
Of several such plugins, the one named like the top-level
module loads, as in lazy.nvim, so `require('mini.ai')` loads
`mini.nvim` rather than a standalone `mini.ai`. Otherwise the
first listed loads, higher `priority` first. A plugin that has
only the module's top-level name, such as `mini.pairs` for a
`require` of `mini.icons`, stays lazy, and the `require` fails
as usual.

Default: true (module loading enabled)

Expand Down
8 changes: 4 additions & 4 deletions docs/tips.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,9 +170,9 @@ A dependency whose function-form `opts` throws used to stop a lazy plugin listin
{ 'u/p', enabled = false, dependencies = { 'u/d' } }
```

### A disabled plugin's `dependencies` and `specs` no longer count
### A disabled or `optional` plugin's `dependencies` and `specs` no longer count

`opts` in a disabled plugin's `dependencies` spec used to reach that dependency when another spec kept it. Now that spec goes with the plugin listing it. Move what should stay to a spec that stays:
`opts` in a `dependencies` spec of a plugin that is disabled, or left out as `optional`, used to reach that dependency when another spec kept it. Now that spec goes with the plugin listing it. LazyVim's lang extras have this shape but lose nothing: an `optional` nvim-lint or nvim-dap carrying `{ 'mason-org/mason.nvim', opts = { ensure_installed = ... } }` never added to mason's list in 2.x, which ignored `opts_extend`, so LazyVim's own mason spec replaced the list. 3.0 adds it when another spec lists that plugin without `optional`. Move what should stay to a spec that stays:

```lua
-- 2.x: u/d gets { a = 1, b = 2 }. 3.0: only { b = 2 }
Expand Down Expand Up @@ -272,10 +272,10 @@ A helper in an imported directory, such as `lua/plugins/util.lua` returning `{ o
- `lua/plugins/*.lua` import by sorted module name, where it used to be file system order, and later specs win: `foo` comes before `foo-extra`, so `foo-extra`'s `opts` now override `foo`'s.
- A table listed both at the top level and under a lazy plugin's `dependencies` used to stay lazy. Now it's a plugin of its own and loads at startup, in `local d = { 'u/d' }` with `{ d, { 'u/p', cmd = 'P', dependencies = { d } } }`. `lazy = true` on the table keeps it lazy.
- A `nil` in a spec list used to end the list. `{ { 'u/x' }, flag and { 'u/y' } or nil, { 'u/d' } }` now installs `u/d` when `flag` is false.
- A function-form `event`, `cmd`, `ft` or `keys` that throws used to leave the plugin loading at startup. The error is still reported, but the plugin now stays lazy, and only a `require` of its modules loads it.
- A function-form `event`, `cmd`, `ft` or `keys` that throws used to leave the plugin loading at startup. The error is still reported, but the plugin now stays lazy, and only its other triggers or a `require` of its modules load it.
- `opts_extend` used to be ignored. Now it applies, so lists that used to replace each other are joined: `{ 'u/x', opts_extend = { 'l' }, opts = { l = { 'a' } } }` beside `{ 'u/x', opts = { l = { 'b' } } }` gave `{ 'b' }` and now gives `{ 'a', 'b' }`.
- A bare string in a spec list used to be reported as an invalid spec and skipped. `{ 'u/a', { 'u/b' } }` now installs both. A string without an owner, as in a module returning `{ 'lua_ls', 'rust_analyzer' }`, is still skipped with a warning.
- A `require` used to load a lazy plugin whose name or `main` looked like the module's name, even with no such module. As in lazy.nvim, it now takes that top-level name under the plugin's `lua/`.
- A `require` used to load a lazy plugin whose name or `main` looked like the module's name, or whose `lua/` had only the module's top-level name. So an optional `pcall(require, 'mini.icons')` at startup loaded and configured a lazy `mini.pairs`, and `pcall(require, 'nvim-treesitter.configs')` loaded nvim-treesitter's main branch, which has no such module. As in lazy.nvim, a `require` now loads only the plugin holding the module's file under `lua/`, and fails as usual when none does. When two do, the one named like the top-level module loads, so `require('mini.ai')` loads `mini.nvim`, not a standalone `mini.ai` listed beside it. A plugin that makes a module at load time, from `plugin/` or `package.preload`, needs a trigger of its own.
- `setup({ profiling = ... })` is ignored without a word, and `print_profile` and `get_profile` are gone from `zpack.module_loader`.

## Upgrading from 1.x to 2.0
Expand Down
1 change: 0 additions & 1 deletion lua/zpack/merge.lua
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ function M.resolve_all(graph, enabled_results, notify)

for src, entry in pairs(state.spec_registry) do
fields.resolve_enabled(enabled_results, entry, src)
entry.has_opts = fields.contributes_opts(entry)
-- cond_result is intentionally left nil here. It is written later by
-- registration.register_all's vim.pack.add load callback (which needs
-- the live plugin arg for function-form conds). Readers must treat
Expand Down
14 changes: 6 additions & 8 deletions lua/zpack/merge_fields.lua
Original file line number Diff line number Diff line change
Expand Up @@ -159,21 +159,19 @@ function M.merge_spec_array(specs)
return result
end

---`opts` is deliberately not stored on merged_spec; `has_opts` lets
---plugin_loader / startup answer "does any spec contribute opts?" without
---re-scanning.
function M.merge_entry(entry)
entry.sorted_specs = M.sort_specs(entry.specs)
entry.merged_spec = M.merge_spec_array(entry.sorted_specs)
end

---`opts` is deliberately not stored on merged_spec; this boolean lets
---plugin_loader / startup answer "does any spec contribute opts?" without
---re-scanning.
function M.contributes_opts(entry)
entry.has_opts = false
for _, s in ipairs(entry.sorted_specs) do
if s.opts ~= nil then
return true
entry.has_opts = true
break
end
end
return false
end

---lazy.nvim parity: the last spec to set `enabled`, in merge order, decides, so
Expand Down
32 changes: 19 additions & 13 deletions lua/zpack/module_loader.lua
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ local module_lookup_cache = {}
---@type vim.pack.Spec[]
local lazy_pack_specs = {}

---What require leaves in package.loaded while a module loads, and keeps there if the load throws.
local LOADING = package.loaded[...]

---@param path string A plugin's directory
---@param modname string
---@return string? file
Expand All @@ -35,29 +38,31 @@ local function claims_topmod(path, topmod)
return false
end

---Find the pending plugin to load for a module: the first claimant of the top-level
---name holding its file, or failing that a sole pending claimant
---Find the pending plugin to load for a module: of the claimants of the top-level name
---holding its file, the one named like that name as in lazy.nvim, else the first.
---Once the first is found, only plugins named like it are scanned.
---@param modname string
---@return string? src plugin source if found
local function find_src_by_scanning(modname)
local topmod = modname:match("^[^.]+")
local first_pending, pending_count = nil, 0
local topmod = modname:match("^[^.]*")
local norm = util.normalize_name(topmod)
local first

for _, pack_spec in ipairs(lazy_pack_specs) do
local src = pack_spec.src
local registry_entry = state.spec_registry[src]
local plugin = registry_entry and registry_entry.plugin
if plugin and registry_entry.load_status == "pending" and registry_entry.merged_spec.module ~= false
and claims_topmod(plugin.path, topmod) then
if module_file(plugin.path, modname) then
return src
if plugin and registry_entry.load_status == "pending" and registry_entry.merged_spec.module ~= false then
local named = util.normalize_name(plugin.spec.name) == norm
if (named or not first) and claims_topmod(plugin.path, topmod) and module_file(plugin.path, modname) then
if named then
return src
end
first = src
end
first_pending = first_pending or src
pending_count = pending_count + 1
end
end

return pending_count == 1 and first_pending or nil
return first
end

---Initialize module loader with lazy plugins
Expand Down Expand Up @@ -107,6 +112,7 @@ end
---@param modname string
---@return function|string|nil
function M.loader(modname)
modname = modname:gsub("/", "."):gsub("^%.+", "")
local src = find_src_for_module(modname)
if not src then
return nil
Expand All @@ -125,7 +131,7 @@ function M.loader(modname)
require('zpack.plugin_loader').process_spec(pack_spec, { bang = true })

local mod = package.loaded[modname]
if mod ~= nil then
if mod ~= nil and mod ~= LOADING then
return function()
return mod
end
Expand Down
1 change: 0 additions & 1 deletion lua/zpack/prune.lua
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ local function refresh_entry(prune, src, entry)
fields.merge_entry(entry)
entry.merged_spec.name = name
fields.resolve_enabled(prune.enabled_results, entry, src)
entry.has_opts = fields.contributes_opts(entry)
end
end

Expand Down
140 changes: 133 additions & 7 deletions tests/startup_process_test.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1399,6 +1399,37 @@ describe("sessions over one data directory", function()
assert.is_false(loads(result, 'bb'))
end)

describe("when both hold the file", function()
local names = { mininvim = 'mini.nvim', miniai = 'mini.ai' }
local function both_args(order)
return ("{ spec = { { url = %s, name = '%s', lazy = true }, { url = %s, name = '%s', lazy = true } }, defaults = { confirm = false } }")
:format(order[1], names[order[1]], order[2], names[order[2]])
end

before_each(function()
for repo, plugin in pairs(names) do
make_repo(repo, {
['lua/mini/ai.lua'] = { ("return { from = '%s' }"):format(plugin) },
['lua/other/x.lua'] = { ("return { from = '%s' }"):format(plugin) },
})
end
end)

for name, order in pairs({ ['listed first'] = { 'mininvim', 'miniai' }, ['listed second'] = { 'miniai', 'mininvim' } }) do
it("loads the one named like the top-level module when it's " .. name, function()
local result = session(both_args(order), report('mini.ai'))
assert.is_true(vim.list_contains(result.log, 'true=mini.nvim'), vim.inspect(result.log))
assert.is_false(loads(result, 'miniai'))
end)

it("loads the first listed when neither is named like it, with " .. names[order[1]] .. ' first', function()
local result = session(both_args(order), report('other.x'))
assert.is_true(vim.list_contains(result.log, 'true=' .. names[order[1]]), vim.inspect(result.log))
assert.is_false(loads(result, order[2]))
end)
end
end)

it("loads neither for a module neither holds, and a later require still works", function()
local result = session(setup_args('aa', 'bb'), report('shared.nope') .. report('shared.nope') .. report('shared.inb'))
assert.is_true(vim.list_contains(result.log, 'false=false'))
Expand All @@ -1416,17 +1447,77 @@ describe("sessions over one data directory", function()
assert.is_false(loads(result, 'cc'))
end)

it("loads what's left once another trigger has loaded the other claimant", function()
local result = session(setup_args('aa', 'bb'), report('shared.nope') .. "vim.cmd('ZPack load bb'); " .. report('shared.nope2'))
assert.is_true(loads(result, 'aa'))
it("loads neither for such a module once another trigger has loaded the other claimant", function()
local result = session(setup_args('aa', 'bb'), report('shared.nope') .. "vim.cmd('ZPack load bb'); "
.. report('shared.nope2') .. "table.insert(_G.LOG, 'aa=' .. require('zpack.api').get_plugin('aa').status); "
.. report('shared.ina'))
assert.are.same({ 'false=false', 'false=false', 'aa=pending', 'true=aa' }, vim.tbl_filter(function(entry)
return entry:find('=', 1, true) ~= nil
end, result.log))
end)
end)

describe("a module no lazy plugin holds, required at startup", function()
local probe = "config = function() local ok, err = pcall(require, %q); table.insert(_G.LOG, 'probe=' .. tostring(ok)); table.insert(_G.LOG, tostring(err)) end"
local status = "table.insert(_G.LOG, 'status=' .. require('zpack.api').get_plugin(%q).status); "

local function setup_args(lazy_spec, module)
return ("{ spec = { %s, { url = ee, name = 'ee', %s } }, defaults = { confirm = false } }"):format(lazy_spec, probe:format(module))
end

local function assert_not_found(result, module)
assert.is_true(vim.list_contains(result.log, 'probe=false'), vim.inspect(result.log))
local err = result.log[vim.fn.index(result.log, 'probe=false') + 2]
assert.are.equal(1, (err:find(("module '%s' not found:"):format(module), 1, true)), err)
end

before_each(function()
make_repo('ee')
make_repo('pairs', { ['lua/mini/pairs.lua'] = { "return { setup = function() table.insert(_G.LOG, 'pairs.setup') end }" } })
make_repo('ts', {
['lua/nvim-treesitter/init.lua'] = { 'return {}' },
['lua/nvim-treesitter/config.lua'] = { 'return {}' },
})
end)

it("leaves a plugin sharing its namespace lazy, as mini.pairs beside a require of mini.icons", function()
local result = session(
setup_args("{ url = pairs, name = 'mini.pairs', event = 'InsertEnter', opts = {} }", 'mini.icons'),
status:format('mini.pairs') .. "vim.api.nvim_exec_autocmds('InsertEnter', {}); " .. status:format('mini.pairs')
)
assert_not_found(result, 'mini.icons')
assert.are.same({ 'status=pending', 'pairs.plugin', 'pairs.setup', 'status=loaded' },
vim.list_slice(result.log, #result.log - 3))
end)

it("still loads a sole claimant for a submodule it doesn't hold", function()
it("leaves a plugin lazy whose old submodule is gone, as nvim-treesitter's main branch without configs", function()
local result = session(
"{ spec = { { url = bb, name = 'bb', lazy = true } }, defaults = { confirm = false } }",
report('only_bb.nope')
setup_args("{ url = ts, name = 'nvim-treesitter', event = 'BufReadPost' }", 'nvim-treesitter.configs'),
status:format('nvim-treesitter')
)
assert.is_true(loads(result, 'bb'))
assert_not_found(result, 'nvim-treesitter.configs')
assert.is_false(vim.list_contains(result.log, 'ts.plugin'))
assert.are.equal('status=pending', result.log[#result.log])
end)

for label, module in pairs({ ['its file'] = 'mini.pairs', ['a directory with init.lua'] = 'nvim-treesitter' }) do
it("still loads the plugin holding " .. label, function()
local result = session(("{ spec = { %s, %s, { url = ee, name = 'ee', %s } }, defaults = { confirm = false } }"):format(
"{ url = pairs, name = 'mini.pairs', event = 'InsertEnter', opts = {} }",
"{ url = ts, name = 'nvim-treesitter', event = 'BufReadPost' }",
probe:format(module)
))
assert.is_true(vim.list_contains(result.log, 'probe=true'), vim.inspect(result.log))
assert.are.equal(module == 'mini.pairs', vim.list_contains(result.log, 'pairs.setup'))
assert.are.equal(module == 'mini.pairs', vim.list_contains(result.log, 'pairs.plugin'))
assert.are.equal(module ~= 'mini.pairs', vim.list_contains(result.log, 'ts.plugin'))
end)
end

it("still loads the plugin holding a submodule", function()
local result = session(setup_args("{ url = ts, name = 'nvim-treesitter', event = 'BufReadPost' }", 'nvim-treesitter.config'))
assert.is_true(vim.list_contains(result.log, 'probe=true'), vim.inspect(result.log))
assert.is_true(vim.list_contains(result.log, 'ts.plugin'))
end)
end)

Expand All @@ -1447,6 +1538,24 @@ describe("sessions over one data directory", function()
end)
end

for label, name in pairs({ ["a slash-form name, as require('lspconfig/util')"] = 'mine/sub', ['a leading dot'] = '.mine.sub' }) do
it("loads for " .. label, function()
make_repo('aa', { ['lua/mine/sub.lua'] = { "return { from = 'aa' }" } })
local result = session(setup_args("{ url = aa, name = 'aa', lazy = true }"), report(name))
assert.are.equal('aa', result.log[#result.log])
end)
end

for _, name in ipairs({ '', '/' }) do
it(("leaves %q to Lua's own searchers"):format(name), function()
make_repo('aa', { ['lua/mine.lua'] = { "return { from = 'aa' }" } })
local result = session(setup_args("{ url = aa, name = 'aa', lazy = true }"), report(name))
local msg = result.log[#result.log]
assert.is_string(msg)
assert.is_falsy(msg:find('/lua/zpack/', 1, true), msg)
end)
end

it("comes from the plugin it loaded, ahead of a dependency holding the same one", function()
make_repo('parent', { ['lua/same.lua'] = { "return { from = 'parent' }" } })
make_repo('dep', { ['lua/same.lua'] = { "return { from = 'dep' }" } })
Expand All @@ -1462,6 +1571,23 @@ describe("sessions over one data directory", function()
assert.is_truthy(result.log[#result.log]:find('aa/lua/mine.lua', 1, true), result.log[#result.log])
end)

it("raises its error when its own auto-setup's require of it threw", function()
make_repo('aa', { ['lua/mine/init.lua'] = { "error('boom in mine')" } })
local probe = "local ok, mod = pcall(require, 'mine'); table.insert(_G.LOG, tostring(ok) .. ' ' .. tostring(mod))"
local result = session(setup_args("{ url = aa, name = 'aa', main = 'mine', lazy = true, opts = {} }"), probe .. '; ' .. probe)
assert.is_truthy(result.log[#result.log - 1]:find('boom in mine', 1, true), result.log[#result.log - 1])
assert.is_truthy(result.log[#result.log]:find("loop or previous error loading module 'mine'", 1, true), result.log[#result.log])
end)

it("is what its config required, run once, when it returns no table", function()
make_repo('aa', { ['lua/mine.lua'] = { "table.insert(_G.LOG, 'mine.run')", 'return function() end' } })
local result = session(
setup_args("{ url = aa, name = 'aa', lazy = true, config = function() _G.FROM_CONFIG = require('mine') end }"),
"table.insert(_G.LOG, tostring(require('mine') == _G.FROM_CONFIG))")
assert.are.same({ 'mine.run', 'true' }, vim.list_slice(result.log, #result.log - 1))
assert.are.equal(1, #vim.tbl_filter(function(l) return l == 'mine.run' end, result.log))
end)

for _, vim_loader in ipairs({ true, false }) do
it("opens a compiled one with vim_loader = " .. tostring(vim_loader), function()
make_repo('aa', { ['lua/nat/compiled.so'] = { "not a real library" } })
Expand Down
Loading
0