neovim

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

commit f5707a9c42a6f0efd0d7abaa1e5caf62eab21aea
parent c53fb58c15c365b41db893416483b3e38f3db79c
Author: Evgeni Chasnovski <evgeni.chasnovski@gmail.com>
Date:   Sun, 28 Dec 2025 16:00:40 +0200

feat(pack)!: make `del()` only remove non-active plugins by default

Problem: Using `vim.pack.del()` to delete active plugin can lead to
  a situation when this plugin is reinstalled after restart. Removing
  plugin from 'init.lua' is documented, but can be missed.

Solution: Make `del()` only remove non-active plugins by default and
  throw an informative error if there is an active plugin.

  Add a way to force delete any plugin by adding `opts.force`. This also
  makes `del()` signature be the same as other functions, which is nice.

Diffstat:
Mruntime/doc/pack.txt | 12++++++++----
Mruntime/lua/vim/pack.lua | 38+++++++++++++++++++++++++++++---------
Mruntime/lua/vim/pack/_lsp.lua | 2+-
Mtest/functional/plugin/pack_spec.lua | 70++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
4 files changed, 88 insertions(+), 34 deletions(-)

diff --git a/runtime/doc/pack.txt b/runtime/doc/pack.txt @@ -321,9 +321,10 @@ Revert plugin after an update ~ • 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. +• Remove plugin specs from |vim.pack.add()| calls in 'init.lua' or they will + be reinstalled later. +• |:restart|. +• Use |vim.pack.del()| with a list of plugin names to remove. *vim.pack-events* • *PackChangedPre* - before trying to change plugin's state. @@ -416,13 +417,16 @@ add({specs}, {opts}) *vim.pack.add()* • {confirm}? (`boolean`) Whether to ask user to confirm initial install. Default `true`. -del({names}) *vim.pack.del()* +del({names}, {opts}) *vim.pack.del()* Remove plugins from disk Parameters: ~ • {names} (`string[]`) List of plugin names to remove from disk. Must be managed by |vim.pack|, not necessarily already added to current session. + • {opts} (`table?`) A table with the following fields: + • {force}? (`boolean`) Whether to allow deleting an active + plugin. Default `false`. get({names}, {opts}) *vim.pack.get()* Gets |vim.pack| plugin info, optionally filtered by `names`. diff --git a/runtime/lua/vim/pack.lua b/runtime/lua/vim/pack.lua @@ -126,8 +126,10 @@ --- ---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. +---- Remove plugin specs from |vim.pack.add()| calls in 'init.lua' or they will be +--- reinstalled later. +---- |:restart|. +---- Use |vim.pack.del()| with a list of plugin names to remove. --- ---[vim.pack-events]() --- @@ -1256,12 +1258,18 @@ function M.update(names, opts) end) end +--- @class vim.pack.keyset.del +--- @inlinedoc +--- @field force? boolean Whether to allow deleting an active plugin. Default `false`. + --- Remove plugins from disk --- --- @param names string[] List of plugin names to remove from disk. Must be managed --- by |vim.pack|, not necessarily already added to current session. -function M.del(names) +--- @param opts? vim.pack.keyset.del +function M.del(names, opts) vim.validate('names', names, vim.islist, false, 'list') + opts = vim.tbl_extend('force', { force = false }, opts or {}) local plug_list = plug_list_from_names(names) if #plug_list == 0 then @@ -1271,19 +1279,31 @@ function M.del(names) lock_read() + local fail_to_delete = {} --- @type string[] for _, p in ipairs(plug_list) do - trigger_event(p, 'PackChangedPre', 'delete') + if not active_plugins[p.path] or opts.force then + trigger_event(p, 'PackChangedPre', 'delete') - vim.fs.rm(p.path, { recursive = true, force = true }) - active_plugins[p.path] = nil - notify(("Removed plugin '%s'"):format(p.spec.name), 'INFO') + vim.fs.rm(p.path, { recursive = true, force = true }) + active_plugins[p.path] = nil + notify(("Removed plugin '%s'"):format(p.spec.name), 'INFO') - plugin_lock.plugins[p.spec.name] = nil + plugin_lock.plugins[p.spec.name] = nil - trigger_event(p, 'PackChanged', 'delete') + trigger_event(p, 'PackChanged', 'delete') + else + fail_to_delete[#fail_to_delete + 1] = p.spec.name + end end lock_write() + + if #fail_to_delete > 0 then + local plugs = table.concat(fail_to_delete, ', ') + local msg = ('Some plugins are active and were not deleted: %s.'):format(plugs) + .. ' Remove them from init.lua, restart, and try again.' + error(msg) + end end --- @inlinedoc diff --git a/runtime/lua/vim/pack/_lsp.lua b/runtime/lua/vim/pack/_lsp.lua @@ -161,7 +161,7 @@ local commands = { end, skip_update_plugin = function(_) end, delete_plugin = function(plug_data) - vim.pack.del({ plug_data.name }) + vim.pack.del({ plug_data.name }, { force = true }) end, } diff --git a/test/functional/plugin/pack_spec.lua b/test/functional/plugin/pack_spec.lua @@ -1980,7 +1980,7 @@ describe('vim.pack', function() -- Should not include removed plugins immediately after they are removed, -- while still returning list without holes - exec_lua('vim.pack.del({ "defbranch" })') + exec_lua('vim.pack.del({ "defbranch" }, { force = true })') local defbranch_data = make_defbranch_data(true, true) local basic_data = make_basic_data(true, true) eq({ { defbranch_data, basic_data }, { basic_data } }, exec_lua('return _G.get_log')) @@ -2013,40 +2013,70 @@ describe('vim.pack', function() describe('del()', function() it('works', function() exec_lua(function() - vim.pack.add({ repos_src.plugindirs, { src = repos_src.basic, version = 'feat-branch' } }) + local basic_spec = { src = repos_src.basic, version = 'feat-branch' } + vim.pack.add({ repos_src.plugindirs, repos_src.defbranch, basic_spec }) end) - eq(true, pack_exists('basic')) - eq(true, pack_exists('plugindirs')) - local locked_plugins = vim.tbl_keys(get_lock_tbl().plugins) - table.sort(locked_plugins) - eq({ 'basic', 'plugindirs' }, locked_plugins) + local assert_on_disk = function(installed_map) + local installed = {} + for p_name, is_installed in pairs(installed_map) do + eq(is_installed, pack_exists(p_name)) + if is_installed then + installed[#installed + 1] = p_name + end + end - watch_events({ 'PackChangedPre', 'PackChanged' }) + table.sort(installed) + local locked = vim.tbl_keys(get_lock_tbl().plugins) + table.sort(locked) + eq(installed, locked) + end - n.exec('messages clear') + assert_on_disk({ basic = true, defbranch = true, plugindirs = true }) + + -- By default should delete only non-active plugins, even if + -- there is active one among input plugin names + n.clear() exec_lua(function() - vim.pack.del({ 'basic', 'plugindirs' }) + vim.pack.add({ repos_src.defbranch }) end) - eq(false, pack_exists('basic')) - eq(false, pack_exists('plugindirs')) + watch_events({ 'PackChangedPre', 'PackChanged' }) - eq( - "vim.pack: Removed plugin 'basic'\nvim.pack: Removed plugin 'plugindirs'", - n.exec_capture('messages') - ) + local err = pcall_err(exec_lua, function() + vim.pack.del({ 'basic', 'defbranch', 'plugindirs' }) + end) + matches('Some plugins are active and were not deleted: defbranch', err) + + assert_on_disk({ basic = false, defbranch = true, plugindirs = false }) + + local msg = "vim.pack: Removed plugin 'basic'\nvim.pack: Removed plugin 'plugindirs'" + eq(msg, n.exec_capture('messages')) -- Should trigger relevant events in order as specified in `vim.pack.add()` local log = exec_lua('return _G.event_log') local find_event = make_find_packchanged(log) - eq(1, find_event('Pre', 'delete', 'basic', 'feat-branch', true)) + eq(1, find_event('Pre', 'delete', 'basic', 'feat-branch', false)) eq(2, find_event('', 'delete', 'basic', 'feat-branch', false)) - eq(3, find_event('Pre', 'delete', 'plugindirs', nil, true)) + eq(3, find_event('Pre', 'delete', 'plugindirs', nil, false)) eq(4, find_event('', 'delete', 'plugindirs', nil, false)) eq(4, #log) - -- Should update lockfile - eq({ plugins = {} }, get_lock_tbl()) + -- Should be possible to force delete active plugins + n.exec('messages clear') + exec_lua('_G.event_log = {}') + exec_lua(function() + vim.pack.del({ 'defbranch' }, { force = true }) + end) + + assert_on_disk({ basic = false, defbranch = false, plugindirs = false }) + + eq("vim.pack: Removed plugin 'defbranch'", n.exec_capture('messages')) + + log = exec_lua('return _G.event_log') + find_event = make_find_packchanged(log) + eq(1, find_event('Pre', 'delete', 'defbranch', nil, true)) + eq(2, find_event('', 'delete', 'defbranch', nil, false)) + eq(2, #log) end) it('works without prior `add()`', function()