neovim

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

commit f0294418d65d005d885eabf80eecc7255358b95e
parent d464dffd2fe5ceee77944fa9946547548b30d9e1
Author: Justin M. Keyes <justinkz@gmail.com>
Date:   Sun, 16 Nov 2025 13:21:47 -0800

Merge #36435 vim.pack: improve default `opts.load`, handle `src` change


Diffstat:
Mruntime/doc/pack.txt | 26++++++++++++++++++++++----
Mruntime/lua/vim/pack.lua | 47++++++++++++++++++++++++++++++++++++-----------
Mtest/functional/plugin/pack_spec.lua | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
3 files changed, 127 insertions(+), 30 deletions(-)

diff --git a/runtime/doc/pack.txt b/runtime/doc/pack.txt @@ -280,6 +280,10 @@ Switch plugin's version: changes in 'init.lua' as well or you will be prompted again next time you run |vim.pack.update()|. +Switch plugin's source: +• Update 'init.lua' for plugin to have desired `src`. +• |:restart|. This will cleanly reinstall plugin from the new source. + Freeze plugin from being updated: • Update 'init.lua' for plugin to have `version` set to current revision. Get it from |vim.pack-lockfile| (plugin's field `rev`; looks like `abc12345`). @@ -290,6 +294,18 @@ Unfreeze plugin to start receiving updates: want it to be updated. • |:restart|. +Revert plugin after an update: +• Locate plugin's revision at working state. For example: + • If there is a previous version of |vim.pack-lockfile| (like from version + control history), use it to get plugin's `rev` field. + • If there is a log file ("nvim-pack.log" at "log" |stdpath()|), open it and + navigate to latest updates (at the bottom). Locate lines about plugin + update details and use revision from "State before". +• Freeze plugin to target revision (set `version` and |:restart|). +• Run `vim.pack.update({ 'plugin-name' }, { force = true })` to make plugin + state on disk follow target revision. |:restart|. +• When ready to deal with updating plugin, unfreeze it. + Remove plugins from disk: • Use |vim.pack.del()| with a list of plugin names to remove. Make sure their specs are not included in |vim.pack.add()| call in 'init.lua' or they will @@ -354,10 +370,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 nothing in this step. + • If exists, check if its `src` is the same as input. If not - delete + immediately to clean install from the new source. Otherwise do + nothing. • If doesn't exist, install it by downloading from `src` into `name` - subdirectory (via `git clone`) and update revision to follow `version` - (via `git checkout`). + subdirectory (via partial blobless `git clone`) and update revision to + match `version` (via `git checkout`). • For each plugin execute |:packadd| (or customizable `load` function) making it reachable by Nvim. @@ -379,7 +397,7 @@ add({specs}, {opts}) *vim.pack.add()* 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. + `false` during |init.lua| sourcing and `true` afterwards. • {confirm}? (`boolean`) Whether to ask user to confirm initial install. Default `true`. diff --git a/runtime/lua/vim/pack.lua b/runtime/lua/vim/pack.lua @@ -77,6 +77,10 @@ ---any changes in 'init.lua' as well or you will be prompted again next time ---you run |vim.pack.update()|. --- +---Switch plugin's source: +---- Update 'init.lua' for plugin to have desired `src`. +---- |:restart|. This will cleanly reinstall plugin from the new source. +--- ---Freeze plugin from being updated: ---- Update 'init.lua' for plugin to have `version` set to current revision. ---Get it from |vim.pack-lockfile| (plugin's field `rev`; looks like `abc12345`). @@ -87,6 +91,18 @@ ---you want it to be updated. ---- |:restart|. --- +---Revert plugin after an update: +---- Locate plugin's revision at working state. For example: +--- - If there is a previous version of |vim.pack-lockfile| (like from version +--- control history), use it to get plugin's `rev` field. +--- - If there is a log file ("nvim-pack.log" at "log" |stdpath()|), open it +--- and navigate to latest updates (at the bottom). Locate lines about plugin +--- update details and use revision from "State before". +---- Freeze plugin to target revision (set `version` and |:restart|). +---- Run `vim.pack.update({ 'plugin-name' }, { force = true })` to make plugin +--- state on disk follow target revision. |:restart|. +---- When ready to deal with updating plugin, unfreeze it. +--- ---Remove plugins from disk: ---- Use |vim.pack.del()| with a list of plugin names to remove. Make sure their specs ---are not included in |vim.pack.add()| call in 'init.lua' or they will be reinstalled. @@ -165,11 +181,11 @@ end local git_version = vim.version.parse('1') local function git_ensure_exec() - local sys_res = vim.system({ 'git', 'version' }):wait() - git_version = vim.version.parse(sys_res.stdout) - if sys_res.stderr ~= '' then + local ok, sys = pcall(vim.system, { 'git', 'version' }) + if not ok then error('No `git` executable') end + git_version = vim.version.parse(sys:wait().stdout) end --- @async @@ -743,8 +759,9 @@ local function pack_add(plug, load) -- 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 } }) - -- Execute 'after/' scripts if not during startup (when they will be sourced - -- automatically), as `:packadd` only sources plain 'plugin/' files. + -- The `:packadd` only sources plain 'plugin/' files. Execute 'after/' scripts + -- if not during startup (when they will be sourced later, even if + -- `vim.pack.add` is inside user's 'plugin/') -- See https://github.com/vim/vim/issues/15584 -- Deliberately do so after executing all currently known 'plugin/' files. if vim.v.vim_did_enter == 1 and load then @@ -760,7 +777,7 @@ end --- @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. +--- Default `false` during |init.lua| sourcing 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`. @@ -768,9 +785,11 @@ end --- Add plugin to current session --- --- - For each specification check that plugin exists on disk in |vim.pack-directory|: ---- - If exists, do nothing in this step. +--- - If exists, check if its `src` is the same as input. If not - delete +--- immediately to clean install from the new source. Otherwise do nothing. --- - If doesn't exist, install it by downloading from `src` into `name` ---- subdirectory (via `git clone`) and update revision to follow `version` (via `git checkout`). +--- subdirectory (via partial blobless `git clone`) and update revision +--- to match `version` (via `git checkout`). --- - For each plugin execute |:packadd| (or customizable `load` function) making --- it reachable by Nvim. --- @@ -788,7 +807,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 = vim.v.vim_did_enter == 1, confirm = true }, opts or {}) + opts = vim.tbl_extend('force', { load = vim.v.vim_did_init == 1, confirm = true }, opts or {}) vim.validate('opts', opts, 'table') local plug_dir = get_plug_dir() @@ -803,13 +822,19 @@ function M.add(specs, opts) local plugs_to_install = {} --- @type vim.pack.Plug[] local needs_lock_write = false for _, p in ipairs(plugs) do - -- TODO(echasnovski): check that lock's `src` is the same as in spec. - -- If not - cleanly reclone (delete directory and mark as not installed). + -- Allow to cleanly change `src` of an already installed plugin + if p.info.installed and plugin_lock.plugins[p.spec.name].src ~= p.spec.src then + M.del({ p.spec.name }) + p.info.installed = false + end + + -- Detect `version` change local p_lock = plugin_lock.plugins[p.spec.name] or {} needs_lock_write = needs_lock_write or p_lock.version ~= p.spec.version p_lock.version = p.spec.version plugin_lock.plugins[p.spec.name] = p_lock + -- Register for install if not p.info.installed then plugs_to_install[#plugs_to_install + 1] = p needs_lock_write = true diff --git a/test/functional/plugin/pack_spec.lua b/test/functional/plugin/pack_spec.lua @@ -611,6 +611,46 @@ describe('vim.pack', function() eq({ { '.git', 'directory' } }, entries) end) + it('allows changing `src` of installed plugin', function() + local basic_src = repos_src.basic + local defbranch_src = repos_src.defbranch + exec_lua(function() + vim.pack.add({ basic_src }) + end) + eq('basic main', exec_lua('return require("basic")')) + + n.clear() + watch_events({ 'PackChangedPre', 'PackChanged' }) + exec_lua(function() + vim.pack.add({ { src = defbranch_src, name = 'basic' } }) + end) + eq('defbranch dev', exec_lua('return require("defbranch")')) + + -- Should first properly delete and then cleanly install + local log_simple = vim.tbl_map(function(x) --- @param x table + return { x.event, x.data.kind, x.data.spec } + end, exec_lua('return _G.event_log')) + + local ref_log_simple = { + { 'PackChangedPre', 'delete', { name = 'basic', src = basic_src } }, + { 'PackChanged', 'delete', { name = 'basic', src = basic_src } }, + { 'PackChangedPre', 'install', { name = 'basic', src = defbranch_src } }, + { 'PackChanged', 'install', { name = 'basic', src = defbranch_src } }, + } + eq(ref_log_simple, log_simple) + + local ref_messages = table.concat({ + "vim.pack: Removed plugin 'basic'", + 'vim.pack: Installing plugins (0/1)', + 'vim.pack: 100% Installing plugins (1/1)', + }, '\n') + eq(ref_messages, n.exec_capture('messages')) + + local defbranch_rev = git_get_hash('dev', 'defbranch') + local ref_lock_tbl = { plugins = { basic = { rev = defbranch_rev, src = defbranch_src } } } + eq(ref_lock_tbl, get_lock_tbl()) + end) + it('can install from the Internet', function() t.skip(skip_integ, 'NVIM_TEST_INTEG not set: skipping network integration test') exec_lua(function() @@ -620,35 +660,42 @@ describe('vim.pack', function() end) describe('startup', function() - local init_lua = '' + local config_dir, pack_add_cmd = '', '' + before_each(function() - init_lua = vim.fs.joinpath(fn.stdpath('config'), 'init.lua') - fn.mkdir(vim.fs.dirname(init_lua), 'p') + config_dir = fn.stdpath('config') + fn.mkdir(vim.fs.joinpath(config_dir, 'plugin'), 'p') + + pack_add_cmd = ('vim.pack.add({ %s })'):format(vim.inspect(repos_src.plugindirs)) end) + after_each(function() - pcall(vim.fs.rm, init_lua, { force = true }) + vim.fs.rm(config_dir, { recursive = true, 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 function assert_loaded() - eq('plugindirs main', exec_lua('return require("plugindirs")')) + local function assert_loaded() + 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 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 + local function assert_works() -- 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)') assert_loaded() - -- Should only `:packadd!` already installed plugin + -- Should only `:packadd!`/`:packadd` already installed plugin n.clear({ args_rm = { '-u' } }) assert_loaded() + end + + it('works in init.lua', function() + local init_lua = vim.fs.joinpath(config_dir, 'init.lua') + fn.writefile({ pack_add_cmd, '_G.done = true' }, init_lua) + assert_works() -- Should not load plugins if `--noplugin`, only adjust 'runtimepath' n.clear({ args = { '--noplugin' }, args_rm = { '-u' } }) @@ -656,6 +703,13 @@ describe('vim.pack', function() eq({}, n.exec_lua('return { vim.g._plugin, vim.g._after_plugin }')) eq(vim.NIL, n.exec_lua('return _G.DL')) end) + + it('works in plugin/', function() + local plugin_file = vim.fs.joinpath(config_dir, 'plugin', 'mine.lua') + fn.writefile({ pack_add_cmd, '_G.done = true' }, plugin_file) + -- Should source plugin's 'plugin/' files without explicit `load=true` + assert_works() + end) end) it('shows progress report during installation', function()