Merge #35270 vim.pack: more control over "load" behavior

This commit is contained in:
Justin M. Keyes
2025-08-11 18:20:25 -04:00
committed by GitHub
3 changed files with 140 additions and 17 deletions

View File

@@ -315,11 +315,12 @@ add({specs}, {opts}) *vim.pack.add()*
Add plugin to current session
• For each specification check that plugin exists on disk in
|vim.pack-directory|:
• If exists, do nothin in this step.
• If exists, do nothing in this step.
• If doesn't exist, install it by downloading from `src` into `name`
subdirectory (via `git clone`) and update state to match `version`
(via `git checkout`).
• For each plugin execute |:packadd| making them reachable by Nvim.
• For each plugin execute |:packadd| (or customizable `load` function)
making it reachable by Nvim.
Notes:
• Installation is done in parallel, but waits for all to finish before
@@ -334,9 +335,14 @@ add({specs}, {opts}) *vim.pack.add()*
• {specs} (`(string|vim.pack.Spec)[]`) List of plugin specifications.
String item is treated as `src`.
• {opts} (`table?`) A table with the following fields:
• {load}? (`boolean`) Load `plugin/` files and `ftdetect/`
scripts. If `false`, works like `:packadd!`. Default
`true`.
• {load}?
(`boolean|fun(plug_data: {spec: vim.pack.Spec, path: string})`)
Load `plugin/` files and `ftdetect/` scripts. If `false`,
works like `:packadd!`. If function, called with plugin
data and is fully responsible for loading plugin. Default
`false` during startup and `true` afterwards.
• {confirm}? (`boolean`) Whether to ask user to confirm
initial install. Default `true`.
del({names}) *vim.pack.del()*
Remove plugins from disk

View File

@@ -352,7 +352,13 @@ end
--- @param event_name 'PackChangedPre'|'PackChanged'
--- @param kind 'install'|'update'|'delete'
local function trigger_event(p, event_name, kind)
local data = { kind = kind, spec = vim.deepcopy(p.spec), path = p.path }
local spec = vim.deepcopy(p.spec)
-- Infer default branch for fuller `event-data` (if possible)
-- Doing it only on event trigger level allows keeping `spec` close to what
-- user supplied without performance issues during startup.
spec.version = spec.version or (uv.fs_stat(p.path) and git_get_default_branch(p.path))
local data = { kind = kind, spec = spec, path = p.path }
vim.api.nvim_exec_autocmds(event_name, { pattern = p.path, data = data })
end
@@ -556,9 +562,9 @@ local function checkout(p, timestamp, skip_same_sha)
end
--- @param plug_list vim.pack.Plug[]
local function install_list(plug_list)
local function install_list(plug_list, confirm)
-- Get user confirmation to install plugins
if not confirm_install(plug_list) then
if confirm and not confirm_install(plug_list) then
for _, p in ipairs(plug_list) do
p.info.err = 'Installation was not confirmed'
end
@@ -574,9 +580,6 @@ local function install_list(plug_list)
git_clone(p.spec.src, p.path)
p.info.installed = true
-- Infer default branch for fuller `event-data`
p.spec.version = p.spec.version or git_get_default_branch(p.path)
-- Do not skip checkout even if HEAD and target have same commit hash to
-- have new repo in expected detached HEAD state and generated help files.
checkout(p, timestamp, false)
@@ -640,7 +643,7 @@ local active_plugins = {}
local n_active_plugins = 0
--- @param plug vim.pack.Plug
--- @param load boolean
--- @param load boolean|fun(plug_data: {spec: vim.pack.Spec, path: string})
local function pack_add(plug, load)
-- Add plugin only once, i.e. no overriding of spec. This allows users to put
-- plugin first to fully control its spec.
@@ -651,6 +654,11 @@ local function pack_add(plug, load)
n_active_plugins = n_active_plugins + 1
active_plugins[plug.path] = { plug = plug, id = n_active_plugins }
if vim.is_callable(load) then
load({ spec = vim.deepcopy(plug.spec), path = plug.path })
return
end
-- NOTE: The `:packadd` specifically seems to not handle spaces in dir name
vim.cmd.packadd({ vim.fn.escape(plug.spec.name, ' '), bang = not load, magic = { file = false } })
@@ -669,15 +677,21 @@ end
--- @class vim.pack.keyset.add
--- @inlinedoc
--- @field load? boolean Load `plugin/` files and `ftdetect/` scripts. If `false`, works like `:packadd!`. Default `true`.
--- Load `plugin/` files and `ftdetect/` scripts. If `false`, works like `:packadd!`.
--- If function, called with plugin data and is fully responsible for loading plugin.
--- Default `false` during startup and `true` afterwards.
--- @field load? boolean|fun(plug_data: {spec: vim.pack.Spec, path: string})
---
--- @field confirm? boolean Whether to ask user to confirm initial install. Default `true`.
--- Add plugin to current session
---
--- - For each specification check that plugin exists on disk in |vim.pack-directory|:
--- - If exists, do nothin in this step.
--- - If exists, do nothing in this step.
--- - If doesn't exist, install it by downloading from `src` into `name`
--- subdirectory (via `git clone`) and update state to match `version` (via `git checkout`).
--- - For each plugin execute |:packadd| making them reachable by Nvim.
--- - For each plugin execute |:packadd| (or customizable `load` function) making
--- it reachable by Nvim.
---
--- Notes:
--- - Installation is done in parallel, but waits for all to finish before
@@ -693,7 +707,7 @@ end
--- @param opts? vim.pack.keyset.add
function M.add(specs, opts)
vim.validate('specs', specs, vim.islist, false, 'list')
opts = vim.tbl_extend('force', { load = true }, opts or {})
opts = vim.tbl_extend('force', { load = vim.v.vim_did_enter == 1, confirm = true }, opts or {})
vim.validate('opts', opts, 'table')
--- @type vim.pack.Plug[]
@@ -708,7 +722,7 @@ function M.add(specs, opts)
if #plugs_to_install > 0 then
git_ensure_exec()
install_list(plugs_to_install)
install_list(plugs_to_install, opts.confirm)
end
-- Register and load those actually on disk while collecting errors

View File

@@ -137,10 +137,12 @@ function repos_setup.plugindirs()
repo_write_file('plugindirs', 'lua/plugindirs.lua', 'return "plugindirs main"')
repo_write_file('plugindirs', 'plugin/dirs.lua', 'vim.g._plugin = true')
repo_write_file('plugindirs', 'plugin/dirs_log.lua', '_G.DL = _G.DL or {}; DL[#DL+1] = "p"')
repo_write_file('plugindirs', 'plugin/dirs.vim', 'let g:_plugin_vim=v:true')
repo_write_file('plugindirs', 'plugin/sub/dirs.lua', 'vim.g._plugin_sub = true')
repo_write_file('plugindirs', 'plugin/bad % name.lua', 'vim.g._plugin_bad = true')
repo_write_file('plugindirs', 'after/plugin/dirs.lua', 'vim.g._after_plugin = true')
repo_write_file('plugindirs', 'after/plugin/dirs_log.lua', '_G.DL = _G.DL or {}; DL[#DL+1] = "a"')
repo_write_file('plugindirs', 'after/plugin/dirs.vim', 'let g:_after_plugin_vim=v:true')
repo_write_file('plugindirs', 'after/plugin/sub/dirs.lua', 'vim.g._after_plugin_sub = true')
repo_write_file('plugindirs', 'after/plugin/bad % name.lua', 'vim.g._after_plugin_bad = true')
@@ -330,6 +332,22 @@ describe('vim.pack', function()
eq({ confirm_msg, 'Proceed? &Yes\n&No', 1, 'Question' }, exec_lua('return _G.confirm_args'))
end)
it('respects `opts.confirm`', function()
exec_lua(function()
_G.confirm_used = false
---@diagnostic disable-next-line: duplicate-set-field
vim.fn.confirm = function()
_G.confirm_used = true
return 1
end
vim.pack.add({ repos_src.basic }, { confirm = false })
end)
eq(false, exec_lua('return _G.confirm_used'))
eq('basic main', exec_lua('return require("basic")'))
end)
it('installs at proper version', function()
local out = exec_lua(function()
vim.pack.add({
@@ -357,6 +375,45 @@ describe('vim.pack', function()
eq(true, exec_lua('return pcall(require, "lspconfig")'))
end)
describe('startup', function()
local init_lua = ''
before_each(function()
init_lua = vim.fs.joinpath(fn.stdpath('config'), 'init.lua')
fn.mkdir(vim.fs.dirname(init_lua), 'p')
end)
after_each(function()
pcall(vim.fs.rm, init_lua, { force = true })
end)
it('works in init.lua', function()
local pack_add_cmd = ('vim.pack.add({ %s })'):format(vim.inspect(repos_src.plugindirs))
fn.writefile({ pack_add_cmd, '_G.done = true' }, init_lua)
local validate_loaded = function()
eq('plugindirs main', exec_lua('return require("plugindirs")'))
-- Should source 'plugin/' and 'after/plugin/' exactly once
eq({ true, true }, n.exec_lua('return { vim.g._plugin, vim.g._after_plugin }'))
eq({ 'p', 'a' }, n.exec_lua('return _G.DL'))
end
-- Should auto-install but wait before executing code after it
n.clear({ args_rm = { '-u' } })
n.exec_lua('vim.wait(500, function() return _G.done end, 50)')
validate_loaded()
-- Should only `:packadd!` already installed plugin
n.clear({ args_rm = { '-u' } })
validate_loaded()
-- Should not load plugins if `--noplugin`, only adjust 'runtimepath'
n.clear({ args = { '--noplugin' }, args_rm = { '-u' } })
eq('plugindirs main', exec_lua('return require("plugindirs")'))
eq({}, n.exec_lua('return { vim.g._plugin, vim.g._after_plugin }'))
eq(vim.NIL, n.exec_lua('return _G.DL'))
end)
end)
it('shows progress report during installation', function()
exec_lua(function()
vim.pack.add({ repos_src.basic, repos_src.defbranch })
@@ -443,6 +500,52 @@ describe('vim.pack', function()
validate(false, {})
end)
it('can use function `opts.load`', function()
local validate = function()
n.exec_lua(function()
_G.load_log = {}
local load = function(...)
table.insert(_G.load_log, { ... })
end
vim.pack.add({ repos_src.plugindirs, repos_src.basic }, { load = load })
end)
-- Order of execution should be the same as supplied in `add()`
local plugindirs_data = {
spec = { src = repos_src.plugindirs, name = 'plugindirs' },
path = pack_get_plug_path('plugindirs'),
}
local basic_data = {
spec = { src = repos_src.basic, name = 'basic' },
path = pack_get_plug_path('basic'),
}
-- - Only single table argument should be supplied to `load`
local ref_log = { { plugindirs_data }, { basic_data } }
eq(ref_log, n.exec_lua('return _G.load_log'))
-- Should not add plugin to the session in any way
eq(false, exec_lua('return pcall(require, "plugindirs")'))
eq(false, exec_lua('return pcall(require, "basic")'))
-- Should not source 'plugin/'
eq({}, n.exec_lua('return { vim.g._plugin, vim.g._after_plugin }'))
-- Plugins should still be marked as "active", since they were added
plugindirs_data.spec.version = 'main'
plugindirs_data.active = true
basic_data.spec.version = 'main'
basic_data.active = true
eq({ plugindirs_data, basic_data }, n.exec_lua('return vim.pack.get()'))
end
-- Works on initial install
validate()
-- Works when loading already installed plugin
n.clear()
validate()
end)
it('generates help tags', function()
exec_lua(function()
vim.pack.add({ { src = repos_src.helptags, name = 'help tags' } })