From 17335308eba91283937f166a9e3d49d6f14a22c4 Mon Sep 17 00:00:00 2001 From: Evgeni Chasnovski Date: Fri, 8 May 2026 15:26:40 +0300 Subject: [PATCH] fix(pack): suggest "delete" code action for active plugins #39678 Problem: Trying to execute code action on an active plugin without updates leads to nothing. It is more useful if code actions "do something" on a bigger portion of the confirm buffer. Solution: Suggest "delete" code action even for active plugins. Trying to execute it will first show a confirmation buffer with relevant warning of why this might be not a good idea. Confirming will delete a plugin. --- runtime/doc/pack.txt | 4 +-- runtime/lua/vim/pack.lua | 4 +-- runtime/lua/vim/pack/_lsp.lua | 30 +++++++++++------ test/functional/plugin/pack_spec.lua | 49 ++++++++++++++++++++-------- 4 files changed, 60 insertions(+), 27 deletions(-) diff --git a/runtime/doc/pack.txt b/runtime/doc/pack.txt index 299b93e99f..86db228f87 100644 --- a/runtime/doc/pack.txt +++ b/runtime/doc/pack.txt @@ -520,8 +520,8 @@ update({names}, {opts}) *vim.pack.update()* change or newer tag. • 'textDocument/codeAction' (`gra` via |lsp-defaults| or |vim.lsp.buf.code_action()|) - show code actions relevant for "plugin at - cursor". Like "delete" (if plugin is not active), "update" or "skip - updating" (if there are pending updates). + cursor". Like "delete" (after extra confirmation for active plugins), + "update" or "skip updating" (if there are pending updates). Parameters: ~ • {names} (`string[]?`) List of plugin names to update. Must be managed diff --git a/runtime/lua/vim/pack.lua b/runtime/lua/vim/pack.lua index 38f7493e02..85a4814540 100644 --- a/runtime/lua/vim/pack.lua +++ b/runtime/lua/vim/pack.lua @@ -1255,8 +1255,8 @@ end --- - 'textDocument/hover' (`K` via |lsp-defaults| or |vim.lsp.buf.hover()|) - 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 relevant for "plugin at cursor". Like "delete" (if plugin is not active), ---- "update" or "skip updating" (if there are pending updates). +--- code actions relevant for "plugin at cursor". Like "delete" (after extra confirmation for +--- active plugins), "update" or "skip updating" (if there are pending updates). --- --- @param names? string[] List of plugin names to update. Must be managed --- by |vim.pack|, not necessarily already added to current session. diff --git a/runtime/lua/vim/pack/_lsp.lua b/runtime/lua/vim/pack/_lsp.lua index b560183492..1c32076b7e 100644 --- a/runtime/lua/vim/pack/_lsp.lua +++ b/runtime/lua/vim/pack/_lsp.lua @@ -35,7 +35,7 @@ end local group_header_pattern = '^# (%S+)' local plugin_header_pattern = '^## (.+)$' ---- @return { group: string?, name: string?, from: integer?, to: integer? } +--- @return { group: string?, name: string?, from: integer?, to: integer?, active: boolean? } local get_plug_data_at_lnum = function(bufnr, lnum) local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) --- @type string, string, integer, integer @@ -223,9 +223,8 @@ methods['textDocument/codeAction'] = function(params, callback) new_action('Skip updating', 'skip_update_plugin'), }, 0) end - if not vim.pack.get({ plug_data.name })[1].active then - vim.list_extend(res, { new_action('Delete', 'delete_plugin') }) - end + plug_data.active = vim.pack.get({ plug_data.name })[1].active + vim.list_extend(res, { new_action('Delete', 'delete_plugin') }) callback(nil, res) end @@ -235,7 +234,15 @@ local commands = { end, skip_update_plugin = function(_) end, delete_plugin = function(plug_data) - vim.pack.del({ plug_data.name }) + if plug_data.active then + local confirm_msg = ('Plugin `%s` is active.'):format(plug_data.name) + .. ' Make sure its `vim.pack.add` call is removed from config.' + local choice = vim.fn.confirm(confirm_msg, 'Delete? &Yes\n&No', 1, 'Question') + if choice ~= 1 then + return 'skip_line_update' + end + end + vim.pack.del({ plug_data.name }, { force = true }) end, } @@ -246,15 +253,18 @@ local commands = { methods['workspace/executeCommand'] = vim.schedule_wrap(function(params, callback) --- @type integer, table local bufnr, plug_data = unpack(params.arguments) - local ok, err = pcall(commands[params.command], plug_data) + local ok, res = pcall(commands[params.command], plug_data) if not ok then - return callback({ code = 1, message = err }, {}) + return callback({ code = 1, message = res }, {}) end -- Remove plugin lines (including blank line) to not later act on plugin - vim.bo[bufnr].modifiable = true - vim.api.nvim_buf_set_lines(bufnr, plug_data.from - 2, plug_data.to, false, {}) - vim.bo[bufnr].modifiable, vim.bo[bufnr].modified = false, false + if res ~= 'skip_line_update' then + vim.bo[bufnr].modifiable = true + vim.api.nvim_buf_set_lines(bufnr, plug_data.from - 2, plug_data.to, false, {}) + vim.bo[bufnr].modifiable, vim.bo[bufnr].modified = false, false + end + callback(nil, {}) end) diff --git a/test/functional/plugin/pack_spec.lua b/test/functional/plugin/pack_spec.lua index ddc06d2b3d..8d11f96f25 100644 --- a/test/functional/plugin/pack_spec.lua +++ b/test/functional/plugin/pack_spec.lua @@ -1592,10 +1592,10 @@ describe('vim.pack', function() local ref_lockfile = get_lock_tbl() --- @type vim.pack.Lock - local function assert_action(pos, action_titles, select_idx) + local function assert_action(pos, action_titles, select_idx, should_preserve_lines) api.nvim_win_set_cursor(0, pos) - local lines = api.nvim_buf_get_lines(0, 0, -1, false) + local buf_lines = api.nvim_buf_get_lines(0, 0, -1, false) n.exec_lua(function() _G.select_items = nil _G.select_idx = select_idx @@ -1607,33 +1607,35 @@ describe('vim.pack', function() eq(titles, action_titles) -- If no action is asked (like via cancel), should not delete lines - if select_idx <= 0 then - eq(lines, api.nvim_buf_get_lines(0, 0, -1, false)) + if select_idx <= 0 or should_preserve_lines then + eq(buf_lines, api.nvim_buf_get_lines(0, 0, -1, false)) end end -- - Should not include "namespace" header as "plugin at cursor" assert_action({ 1, 1 }, {}, 0) assert_action({ 2, 0 }, {}, 0) - -- - No actions for `defbranch` since it is active and has no updates - assert_action({ 3, 1 }, {}, 0) - assert_action({ 7, 0 }, {}, 0) + -- - "Delete" action should be present for active plugins + local defbranch_actions = { 'Delete `defbranch`' } + assert_action({ 3, 1 }, defbranch_actions, 0) + assert_action({ 7, 0 }, defbranch_actions, 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 suggest updating related actions if updates available - local fetch_actions = { 'Update `fetch`', 'Skip updating `fetch`' } + local fetch_actions = { 'Update `fetch`', 'Skip updating `fetch`', 'Delete `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 for not active plugins - assert_action({ 24, 0 }, { 'Delete `semver`' }, 0) - assert_action({ 28, 0 }, { 'Delete `semver`' }, 0) - assert_action({ 31, 0 }, { 'Delete `semver`' }, 0) + -- - "Delete" action should be present for not active plugin + local semver_actions = { 'Delete `semver`' } + assert_action({ 24, 0 }, semver_actions, 0) + assert_action({ 28, 0 }, semver_actions, 0) + assert_action({ 31, 0 }, semver_actions, 0) -- - Should correctly perform action and remove plugin's lines local function line_match(lnum, pattern) @@ -1641,7 +1643,7 @@ describe('vim.pack', function() end -- - Delete not active plugin. Should remove from disk and update lockfile. - assert_action({ 24, 0 }, { 'Delete `semver`' }, 1) + assert_action({ 24, 0 }, semver_actions, 1) eq(false, pack_exists('semver')) line_match(22, '^# Same') eq(22, api.nvim_buf_line_count(0)) @@ -1656,6 +1658,27 @@ describe('vim.pack', function() line_match(10, '^$') line_match(11, '^# Same') + -- - Delete active plugin. Should ask for confirmation before force deleting + -- with possibility to not confirm. + mock_confirm(2) -- No + assert_action({ 3, 0 }, defbranch_actions, 1, true) + eq(true, pack_exists('defbranch')) + eq(ref_lockfile, get_lock_tbl()) + local confirm_msg = 'Plugin `defbranch` is active.' + .. ' Make sure its `vim.pack.add` call is removed from config.' + local ref_confirm_log = { { confirm_msg, 'Delete? &Yes\n&No', 1, 'Question' } } + eq(ref_confirm_log, exec_lua('return _G.confirm_log')) + + mock_confirm(1) -- Yes + assert_action({ 3, 0 }, defbranch_actions, 1) + eq(false, pack_exists('defbranch')) + ref_lockfile.plugins.defbranch = nil + eq(ref_lockfile, get_lock_tbl()) + eq(5, api.nvim_buf_line_count(0)) + line_match(1, '^# Error') + line_match(3, '^# Update') + line_match(5, '^# Same') + -- - Update plugin. Should not re-fetch new data and update lockfile. n.exec('quit') n.exec_lua(function()