neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

commit 3c7b698d61c97c63858089e23879f1849e938769
parent 67fede0fc94bff89bb266b649b8d0a57858622d1
Author: Justin M. Keyes <justinkz@gmail.com>
Date:   Mon, 11 Aug 2025 18:20:25 -0400

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


Diffstat:
Mruntime/doc/pack.txt | 16+++++++++++-----
Mruntime/lua/vim/pack.lua | 38++++++++++++++++++++++++++------------
Mtest/functional/plugin/pack_spec.lua | 103+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 140 insertions(+), 17 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,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 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 @@ -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 diff --git a/test/functional/plugin/pack_spec.lua b/test/functional/plugin/pack_spec.lua @@ -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' } })