Merge #37142 from echasnovski/pack-safer-del

This commit is contained in:
Justin M. Keyes
2025-12-30 04:20:33 -05:00
committed by GitHub
5 changed files with 146 additions and 64 deletions

View File

@@ -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`.
@@ -464,7 +468,8 @@ update({names}, {opts}) *vim.pack.update()*
details of particular pending change or newer tag.
• 'textDocument/codeAction' (`gra` via |lsp-defaults| or
|vim.lsp.buf.code_action()|) - show code actions available for
"plugin at cursor". Like "delete", "update", or "skip updating".
"plugin at cursor". Like "delete" (if plugin is not active),
"update" or "skip updating" (if there are pending updates).
Execute |:write| to confirm update, execute |:quit| to discard the
update.
• If `true`, make updates right away.

View File

@@ -25,8 +25,10 @@ for i, l in ipairs(lines) do
cur_header_hl_group = header_hl_groups[cur_group]
hi_range(i, 0, l:len(), cur_header_hl_group)
elseif l:find('^## (.+)$') ~= nil then
-- Header 2
-- Header 2 with possibly "(not active)" suffix
hi_range(i, 0, l:len(), cur_header_hl_group)
local col = l:match('() %(not active%)$') or l:len()
hi_range(i, col, l:len(), 'DiagnosticError', priority + 1)
elseif cur_info ~= nil then
-- Plugin info
local end_col = l:match('(). +%b()$') or l:len()

View File

@@ -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]()
---
@@ -988,11 +990,12 @@ end
--- @param p vim.pack.Plug
--- @return string
local function compute_feedback_lines_single(p)
local active_suffix = active_plugins[p.path] ~= nil and '' or ' (not active)'
if p.info.err ~= '' then
return ('## %s\n\n %s'):format(p.spec.name, p.info.err:gsub('\n', '\n '))
return ('## %s%s\n\n %s'):format(p.spec.name, active_suffix, p.info.err:gsub('\n', '\n '))
end
local parts = { '## ' .. p.spec.name .. '\n' }
local parts = { ('## %s%s\n'):format(p.spec.name, active_suffix) }
local version_suffix = p.info.version_str == '' and '' or (' (%s)'):format(p.info.version_str)
if p.info.sha_head == p.info.sha_target then
@@ -1125,7 +1128,7 @@ local function get_update_map(bufnr)
for _, l in ipairs(lines) do
local name = l:match('^## (.+)$')
if name and is_in_update then
res[name] = true
res[name:gsub(' %(not active%)$', '')] = true
end
local group = l:match('^# (%S+)')
@@ -1158,8 +1161,9 @@ end
--- show more information at cursor. Like details of particular pending
--- change or newer tag.
--- - 'textDocument/codeAction' (`gra` via |lsp-defaults| or |vim.lsp.buf.code_action()|) -
--- show code actions available for "plugin at cursor". Like "delete", "update",
--- or "skip updating".
--- show code actions available for "plugin at cursor".
--- Like "delete" (if plugin is not active), "update" or "skip updating"
--- (if there are pending updates).
---
--- Execute |:write| to confirm update, execute |:quit| to discard the update.
--- - If `true`, make updates right away.
@@ -1256,12 +1260,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 +1281,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

View File

@@ -59,7 +59,7 @@ local get_plug_data_at_lnum = function(bufnr, lnum)
if not (from <= lnum and lnum <= to) then
return {}
end
return { group = group, name = name, from = from, to = to }
return { group = group, name = name:gsub(' %(not active%)$', ''), from = from, to = to }
end
--- @alias vim.pack.lsp.Position { line: integer, character: integer }
@@ -151,7 +151,9 @@ methods['textDocument/codeAction'] = function(params, callback)
new_action('Skip updating', 'skip_update_plugin'),
}, 0)
end
vim.list_extend(res, { new_action('Delete', 'delete_plugin') })
if not vim.pack.get({ plug_data.name })[1].active then
vim.list_extend(res, { new_action('Delete', 'delete_plugin') })
end
callback(nil, res)
end

View File

@@ -1384,7 +1384,7 @@ describe('vim.pack', function()
exec_lua(function()
vim.pack.add({
repos_src.fetch,
{ src = repos_src.semver, version = 'v0.3.0' },
-- No `semver` to test with non-active plugins
{ src = repos_src.defbranch, version = 'does-not-exist' },
})
vim.pack.update()
@@ -1409,7 +1409,7 @@ describe('vim.pack', function()
{ lnum = 9, col = 1, end_lnum = 22, end_col = 1, text = '[Namespace] Update' },
{ lnum = 11, col = 1, end_lnum = 22, end_col = 1, text = '[Module] fetch' },
{ lnum = 22, col = 1, end_lnum = 32, end_col = 1, text = '[Namespace] Same' },
{ lnum = 24, col = 1, end_lnum = 32, end_col = 1, text = '[Module] semver' },
{ lnum = 24, col = 1, end_lnum = 32, end_col = 1, text = '[Module] semver (not active)' },
}
eq(ref_loclist, loclist)
@@ -1491,22 +1491,22 @@ describe('vim.pack', function()
-- - Should not include "namespace" header as "plugin at cursor"
assert_action({ 1, 1 }, {}, 0)
assert_action({ 2, 0 }, {}, 0)
-- - Only deletion should be available on errored plugin
assert_action({ 3, 1 }, { 'Delete `defbranch`' }, 0)
assert_action({ 7, 0 }, { 'Delete `defbranch`' }, 0)
-- - No actions for `defbranch` since it is active and has no updates
assert_action({ 3, 1 }, {}, 0)
assert_action({ 7, 0 }, {}, 0)
-- - Should not include separator blank line as "plugin at cursor"
assert_action({ 8, 0 }, {}, 0)
assert_action({ 9, 0 }, {}, 0)
assert_action({ 10, 0 }, {}, 0)
-- - Should also suggest updating related actions if updates available
local fetch_actions = { 'Update `fetch`', 'Skip updating `fetch`', 'Delete `fetch`' }
-- - Should suggest updating related actions if updates available
local fetch_actions = { 'Update `fetch`', 'Skip updating `fetch`' }
assert_action({ 11, 0 }, fetch_actions, 0)
assert_action({ 14, 0 }, fetch_actions, 0)
assert_action({ 20, 0 }, fetch_actions, 0)
assert_action({ 21, 0 }, {}, 0)
assert_action({ 22, 0 }, {}, 0)
assert_action({ 23, 0 }, {}, 0)
-- - Only deletion should be available on plugins without update
-- - Only deletion should be available for not active plugins
assert_action({ 24, 0 }, { 'Delete `semver`' }, 0)
assert_action({ 28, 0 }, { 'Delete `semver`' }, 0)
assert_action({ 32, 0 }, { 'Delete `semver`' }, 0)
@@ -1516,27 +1516,26 @@ describe('vim.pack', function()
matches(pattern, api.nvim_buf_get_lines(0, lnum - 1, lnum, false)[1])
end
-- - Delete. Should remove from disk and update lockfile.
assert_action({ 3, 0 }, { 'Delete `defbranch`' }, 1)
eq(false, pack_exists('defbranch'))
line_match(1, '^# Error')
line_match(2, '^$')
line_match(3, '^# Update')
-- - Delete not active plugin. Should remove from disk and update lockfile.
assert_action({ 24, 0 }, { 'Delete `semver`' }, 1)
eq(false, pack_exists('semver'))
line_match(22, '^# Same')
eq(22, api.nvim_buf_line_count(0))
ref_lockfile.plugins.defbranch = nil
ref_lockfile.plugins.semver = nil
eq(ref_lockfile, get_lock_tbl())
-- - Skip udating
assert_action({ 5, 0 }, fetch_actions, 2)
assert_action({ 11, 0 }, fetch_actions, 2)
eq('return "fetch main"', fn.readblob(fetch_lua_file))
line_match(3, '^# Update')
line_match(4, '^$')
line_match(5, '^# Same')
line_match(9, '^# Update')
line_match(10, '^$')
line_match(11, '^# Same')
-- - Update plugin. Should not re-fetch new data and update lockfile.
n.exec('quit')
n.exec_lua(function()
vim.pack.update({ 'fetch', 'semver' })
vim.pack.update({ 'fetch' })
end)
exec_lua('_G.echo_log = {}')
@@ -1549,8 +1548,7 @@ describe('vim.pack', function()
eq('return "fetch new 2"', fn.readblob(fetch_lua_file))
assert_progress_report('Applying updates', { 'fetch' })
line_match(1, '^# Update')
line_match(2, '^$')
line_match(3, '^# Same')
eq(1, api.nvim_buf_line_count(0))
eq(ref_lockfile, get_lock_tbl())
@@ -1622,6 +1620,29 @@ describe('vim.pack', function()
ref_fetch_lock.rev = git_get_hash('main', 'fetch')
eq(ref_fetch_lock, get_lock_tbl().plugins.fetch)
end)
it('hints about not active plugins', function()
exec_lua(function()
vim.pack.update()
end)
for _, l in ipairs(api.nvim_buf_get_lines(0, 0, -1, false)) do
if l:match('^## ') then
matches(' %(not active%)$', l)
end
end
-- Should also hint in `textDocument/documentSymbol` of in-process LSP,
-- yet still work for navigation
exec_lua('vim.lsp.buf.document_symbol()')
local loclist = fn.getloclist(0)
matches(' %(not active%)$', loclist[2].text)
matches(' %(not active%)$', loclist[4].text)
matches(' %(not active%)$', loclist[5].text)
n.exec('llast')
eq(21, api.nvim_win_get_cursor(0)[1])
end)
end)
it('works with not active plugins', function()
@@ -1980,7 +2001,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 +2034,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
table.sort(installed)
local locked = vim.tbl_keys(get_lock_tbl().plugins)
table.sort(locked)
eq(installed, locked)
end
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.add({ repos_src.defbranch })
end)
watch_events({ 'PackChangedPre', 'PackChanged' })
n.exec('messages clear')
exec_lua(function()
vim.pack.del({ 'basic', 'plugindirs' })
local err = pcall_err(exec_lua, function()
vim.pack.del({ 'basic', 'defbranch', 'plugindirs' })
end)
eq(false, pack_exists('basic'))
eq(false, pack_exists('plugindirs'))
matches('Some plugins are active and were not deleted: defbranch', err)
eq(
"vim.pack: Removed plugin 'basic'\nvim.pack: Removed plugin 'plugindirs'",
n.exec_capture('messages')
)
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()