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:
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()