commit dd828690c747d0adf19b9f455e0b4cfe2d1ae954
parent b337c6e0fc0a5c0c2e8125babded24ffcd544796
Author: Evgeni Chasnovski <evgeni.chasnovski@gmail.com>
Date: Sat, 9 Aug 2025 16:37:13 +0300
feat(pack): allow function `opts.load` in `add()`
Problem: No way to have full control over how plugin is loaded.
Although `:packadd!` has small side effects (only adds plugin
directory to 'runtimepath'; and maybe its 'after/' subdirectory), it
still has side effects. For example, 'plugin/' directories are still
loaded during startup (as part of `:h load-plugins`).
Solution: Allow function `opts.load` that has full control over how
plugin is loaded.
Diffstat:
3 files changed, 72 insertions(+), 12 deletions(-)
diff --git a/runtime/doc/pack.txt b/runtime/doc/pack.txt
@@ -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,8 +335,11 @@ 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
+ • {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.
del({names}) *vim.pack.del()*
diff --git a/runtime/lua/vim/pack.lua b/runtime/lua/vim/pack.lua
@@ -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
@@ -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 } })
@@ -670,16 +678,18 @@ end
--- @class vim.pack.keyset.add
--- @inlinedoc
--- 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
+--- @field load? boolean|fun(plug_data: {spec: vim.pack.Spec, path: string})
--- 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
diff --git a/test/functional/plugin/pack_spec.lua b/test/functional/plugin/pack_spec.lua
@@ -484,6 +484,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' } })