From 6d50f7d86fdb479310b970968535c25931293b86 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Fri, 8 May 2026 18:15:28 +0545 Subject: [PATCH] feat(lua):autoinstall missing plugins on startup and enhance vim.pack UI keymaps --- lua/autocmd.lua | 20 ++++ lua/keys.lua | 176 +++++++++++++++------------- lua/pack_helper.lua | 2 +- lua/packui.lua | 280 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 393 insertions(+), 85 deletions(-) create mode 100644 lua/packui.lua diff --git a/lua/autocmd.lua b/lua/autocmd.lua index 9c13888..4299337 100644 --- a/lua/autocmd.lua +++ b/lua/autocmd.lua @@ -198,3 +198,23 @@ vim.api.nvim_create_autocmd({ "TextChanged", "InsertLeave", "BufLeave", "FocusLo end, delay) end, }) + +-- Autoinstall missing plugins on startup +vim.api.nvim_create_autocmd('VimEnter', { + group = augroup('autoinstall'), + callback = function() + local ok, items = pcall(vim.pack.get, nil, { info = true }) + if ok then + local missing = {} + for _, item in ipairs(items) do + if not item.path or vim.fn.isdirectory(item.path) == 0 then + table.insert(missing, item.spec.name or item.spec.src:match('([^/]+)/*$')) + end + end + if #missing > 0 then + vim.notify('Installing missing plugins...', vim.log.levels.INFO, { title = 'vim.pack' }) + vim.pack.update(missing, { force = true }) + end + end + end, +}) diff --git a/lua/keys.lua b/lua/keys.lua index 83d4afa..61e733b 100644 --- a/lua/keys.lua +++ b/lua/keys.lua @@ -1,143 +1,151 @@ local function map(mode, lhs, rhs, opts) - opts = opts or {} - opts.silent = opts.silent ~= false - vim.keymap.set(mode, lhs, rhs, opts) + opts = opts or {} + opts.silent = opts.silent ~= false + vim.keymap.set(mode, lhs, rhs, opts) end local opts = { noremap = true, silent = true } + -- Space as leader -map("n", "", "", opts) -vim.g.mapleader = " " -vim.g.maplocalleader = " " +map('n', '', '', opts) +vim.g.mapleader = ' ' +vim.g.maplocalleader = ' ' -- Modes -- normal_mode = "n", insert_mode = "i", visual_mode = "v", -- visual_block_mode = "x", term_mode = "t", command_mode = "c", -- Terminal Mappings -map("t", "jk", "") -map("t", "kj", "") -map("t", "", "") +map('t', 'jk', '') +map('t', 'kj', '') +map('t', '', '') + +-- Insert Mode Mappings +map('i', 'jk', '') +map('n', 'ps', require('packui').open, {desc = "Open Vim Pack UI"}) +-- Comments +map('n', '/', 'gcc', { remap = true, desc = 'Toggle comment' }) +map('v', '/', 'gc', { remap = true, desc = 'Toggle comment' }) -- Clear search, diff update and redraw -map({ "i", "n" }, "", "noh", { desc = "Escape and clear hlsearch" }) +map({ 'i', 'n' }, '', 'noh', { desc = 'Escape and clear hlsearch' }) -map({ "i", "c" }, "", "") -map({ "i", "c" }, "", "") +map({ 'i', 'c' }, '', '') +map({ 'i', 'c' }, '', '') -map("n", "rL", vim.lsp.buf.rename, { desc = "Rename Lsp Symbol" }) -map("n", "`", "'", { noremap = true }) -- better marks +map('n', 'rL', vim.lsp.buf.rename, { desc = 'Rename Lsp Symbol' }) +map('n', '`', "'", { noremap = true }) -- better marks -map({ "n", "v" }, "H", "^", { desc = "Move to the beginning of the line" }) -map({ "n", "v" }, "L", "$", { desc = "Move to the end of the line" }) +map({ 'n', 'v' }, 'H', '^', { desc = 'Move to the beginning of the line' }) +map({ 'n', 'v' }, 'L', '$', { desc = 'Move to the end of the line' }) -- Visual overwrite paste -map({ "v", "x" }, "p", '"_dP', opts) +map({ 'v', 'x' }, 'p', '"_dP', opts) -- Do not copy on x -map({ "v", "x" }, "x", '"_x', opts) -map("n", "X", '"_D', opts) -map("n", "dd", '"_dd', opts) -map({ "v", "x" }, "X", '"_d', opts) +map({ 'v', 'x' }, 'x', '"_x', opts) +map('n', 'X', '"_D', opts) +map('n', 'dd', '"_dd', opts) +map({ 'v', 'x' }, 'X', '"_d', opts) -- Select all -- Select all -map("n", "a", "ggVG", { desc = "Select all" }) -map("n", "", "ggVG", { desc = "Select all" }) -map("i", "", "ggVG", { desc = "Select all" }) +map('n', 'a', 'ggVG', { desc = 'Select all' }) +map('n', '', 'ggVG', { desc = 'Select all' }) +map('i', '', 'ggVG', { desc = 'Select all' }) -- Move text up and down -map({ "v", "x" }, "K", ":move '<-2gv-gv", opts) -map({ "v", "x" }, "J", ":move '>+1gv-gv", opts) +map({ 'v', 'x' }, 'K', ":move '<-2gv-gv", opts) +map({ 'v', 'x' }, 'J', ":move '>+1gv-gv", opts) -- Line operations -map("n", "", ":m .-2==", { desc = "Move line up" }) -map("n", "", ":m .+1==", { desc = "Move line down" }) -map("i", "", ":m .-2==gi", { desc = "Move line up" }) -map("i", "", ":m .+1==gi", { desc = "Move line down" }) -map("v", "", ":m '<-2gv=gv", { desc = "Move selection up" }) -map("v", "", ":m '>+1gv=gv", { desc = "Move selection down" }) +map('n', '', ':m .-2==', { desc = 'Move line up' }) +map('n', '', ':m .+1==', { desc = 'Move line down' }) +map('i', '', ':m .-2==gi', { desc = 'Move line up' }) +map('i', '', ':m .+1==gi', { desc = 'Move line down' }) +map('v', '', ":m '<-2gv=gv", { desc = 'Move selection up' }) +map('v', '', ":m '>+1gv=gv", { desc = 'Move selection down' }) -- Line manipulation -map("n", "", ">>", { desc = "Indent line" }) -map("n", "", "<<", { desc = "Unindent line" }) -map("i", "", ">>", { desc = "Indent line" }) -map("i", "", "<<", { desc = "Unindent line" }) +map('n', '', '>>', { desc = 'Indent line' }) +map('n', '', '<<', { desc = 'Unindent line' }) +map('i', '', '>>', { desc = 'Indent line' }) +map('i', '', '<<', { desc = 'Unindent line' }) -- Quick word operations -map("n", "", "db", { desc = "Delete word backward" }) -map("i", "", "", { desc = "Delete word backward" }) -map("n", "", "dw", { desc = "Delete word forward" }) -map("i", "", "dw", { desc = "Delete word forward" }) +map('n', '', 'db', { desc = 'Delete word backward' }) +map('i', '', '', { desc = 'Delete word backward' }) +map('n', '', 'dw', { desc = 'Delete word forward' }) +map('i', '', 'dw', { desc = 'Delete word forward' }) -map("n", "d", '"_d', { desc = "Delete without replacing clipboard" }) -map("n", "x", '"_x', { desc = "Cut without replacing clipboard" }) -map("n", "c", '"_c', { desc = "Change without replacing clipboard" }) -map("v", "p", '"_dP', { desc = "Paste without replacing clipboard" }) +map('n', 'd', '"_d', { desc = 'Delete without replacing clipboard' }) +map('n', 'x', '"_x', { desc = 'Cut without replacing clipboard' }) +map('n', 'c', '"_c', { desc = 'Change without replacing clipboard' }) +map('v', 'p', '"_dP', { desc = 'Paste without replacing clipboard' }) --insert the following characters around the visual selection -map("v", "(", "s()Pll", { desc = "wrap selection with parens" }) -map("v", "[", "s[]Pll", { desc = "wrap selection with square braces" }) -map("v", "{", "s{}Pll", { desc = "wrap selection with curly braces" }) -map("v", "'", "s''Pll", { desc = "wrap selection with single quotes" }) -map("v", '"', 's""Pll', { desc = "wrap selection with double quotes" }) +map('v', '(', 's()Pll', { desc = 'wrap selection with parens' }) +map('v', '[', 's[]Pll', { desc = 'wrap selection with square braces' }) +map('v', '{', 's{}Pll', { desc = 'wrap selection with curly braces' }) +map('v', "'", "s''Pll", { desc = 'wrap selection with single quotes' }) +map('v', '"', 's""Pll', { desc = 'wrap selection with double quotes' }) -- Insert mode cursor movement with Ctrl+hjkl -map({ "i", "c" }, "", "", { noremap = true, silent = true }) -map({ "i", "c" }, "", "", { noremap = true, silent = true }) -map({ "i", "c" }, "", "", { noremap = true, silent = true }) -map({ "i", "c" }, "", "", { noremap = true, silent = true }) +map({ 'i', 'c' }, '', '', { noremap = true, silent = true }) +map({ 'i', 'c' }, '', '', { noremap = true, silent = true }) +map({ 'i', 'c' }, '', '', { noremap = true, silent = true }) +map({ 'i', 'c' }, '', '', { noremap = true, silent = true }) -map("n", "re", ":%s//", { desc = "Shortcut to replace current word under cursor" }) +map('n', 're', ':%s//', { desc = 'Shortcut to replace current word under cursor' }) -- toggle inlay hints -vim.keymap.set("n", "th", function() - vim.lsp.inlay_hint.enable(not vim.lsp.inlay_hint.is_enabled()) +vim.keymap.set('n', 'th', function() + vim.lsp.inlay_hint.enable(not vim.lsp.inlay_hint.is_enabled()) end) -- No arrow navigation -local modes = { "n", "v", "i" } -local arrows = { "", "", "", "" } +local modes = { 'n', 'v', 'i' } +local arrows = { '', '', '', '' } for _, mode in ipairs(modes) do - for _, key in ipairs(arrows) do - vim.api.nvim_set_keymap(mode, key, "", opts) - end + for _, key in ipairs(arrows) do + vim.api.nvim_set_keymap(mode, key, '', opts) + end end -- Go to Beginning or End of line -map({ "n", "v" }, "H", "_^", { desc = "Move to the beginning of the line" }) -map({ "n", "v" }, "L", "$", { desc = "Move to the end of the line" }) +map({ 'n', 'v' }, 'H', '_^', { desc = 'Move to the beginning of the line' }) +map({ 'n', 'v' }, 'L', '$', { desc = 'Move to the end of the line' }) -- Search inside visually highlighted text. -map("x", "g/", "/\\%V", { silent = false, desc = "Search inside visual selection" }) +map('x', 'g/', '/\\%V', { silent = false, desc = 'Search inside visual selection' }) -- Search & replace in current file/line -map("v", "rw", '"9y:%s@9@9@g', { desc = "Search & replace selection", silent = false }) -map("n", "rw", 'viw"9y:%s@9@9@g', { desc = "Search & replace word", silent = false }) +map('v', 'rw', '"9y:%s@9@9@g', { desc = 'Search & replace selection', silent = false }) +map('n', 'rw', 'viw"9y:%s@9@9@g', { desc = 'Search & replace word', silent = false }) map( - "v", - "rl", - '"9y:s@9@9@g', - { desc = "Search & replace in current line", silent = false } + 'v', + 'rl', + '"9y:s@9@9@g', + { desc = 'Search & replace in current line', silent = false } ) map( - "n", - "rl", - 'viw"9y:s@9@9@g', - { desc = "Search & replace word in current line", silent = false } + 'n', + 'rl', + 'viw"9y:s@9@9@g', + { desc = 'Search & replace word in current line', silent = false } ) -- Folds -map("n", "<", function() - if not vim.opt.foldmethod then - vim.opt.foldmethod = "indent" - end - vim.cmd("normal! zM") -end, { desc = "Fold all" }) +map('n', '<', function() + if not vim.opt.foldmethod then + vim.opt.foldmethod = 'indent' + end + vim.cmd('normal! zM') +end, { desc = 'Fold all' }) -map("n", ">", function() - vim.opt.foldenable = false - vim.cmd("normal! zR") -end, { desc = "Open all folds" }) +map('n', '>', function() + vim.opt.foldenable = false + vim.cmd('normal! zR') +end, { desc = 'Open all folds' }) diff --git a/lua/pack_helper.lua b/lua/pack_helper.lua index 16acbe8..67a0919 100644 --- a/lua/pack_helper.lua +++ b/lua/pack_helper.lua @@ -27,7 +27,7 @@ _G.add = function(specs) end)() or s) table.insert(res, n) end - if #res > 0 then pcall(vim.pack.add, res) end + if #res > 0 then pcall(vim.pack.add, res, { confirm = false }) end end return M diff --git a/lua/packui.lua b/lua/packui.lua new file mode 100644 index 0000000..5d06089 --- /dev/null +++ b/lua/packui.lua @@ -0,0 +1,280 @@ +local M = {} + +local state = { + buf = nil, + win = nil, + items = {}, + line_to_item = {}, +} + +local function notify(message, level) + vim.notify(message, level or vim.log.levels.INFO, { title = 'vim.pack' }) +end + +local function close() + if state.win and vim.api.nvim_win_is_valid(state.win) then + vim.api.nvim_win_close(state.win, true) + end + state.win = nil + state.buf = nil +end + +local function plugin_name(item) + return item.spec.name or item.spec.src:match('([^/]+)/*$') or item.spec.src +end + +local function plugin_version(item) + local version = item.spec.version + if version == nil then + return '' + end + return type(version) == 'string' and version or tostring(version) +end + +local function short_rev(rev) + if not rev or rev == '' then + return '' + end + return rev:sub(1, 7) +end + +local function get_items(info) + local ok, items = pcall(vim.pack.get, nil, { info = info or false }) + if not ok then + notify(items, vim.log.levels.ERROR) + return {} + end + + table.sort(items, function(a, b) + return plugin_name(a) < plugin_name(b) + end) + + return items +end + +local function item_at_cursor() + if not state.win or not vim.api.nvim_win_is_valid(state.win) then + return nil + end + + local line = vim.api.nvim_win_get_cursor(state.win)[1] + return state.line_to_item[line] +end + +local function render() + if not state.buf or not vim.api.nvim_buf_is_valid(state.buf) then + return + end + + state.items = get_items(false) + state.line_to_item = {} + + local lines = { + ' 📦 vim.pack managed plugins', + ' [u] update [U] update all [d] delete [X] clean inactive [i] inspect [o] open [q] quit', + ' ─────────────────────────────────────────────────────────────────────────────────', + '', + string.format(' %-3s %-30s %-10s %-15s %s', 'Act', 'Plugin', 'Rev', 'Version', 'Source'), + string.format(' %-3s %-30s %-10s %-15s %s', '───', '──────', '───', '───────', '──────'), + } + + local ns_id = vim.api.nvim_create_namespace('packui') + vim.api.nvim_buf_clear_namespace(state.buf, ns_id, 0, -1) + + for _, item in ipairs(state.items) do + local name = plugin_name(item) + local status = item.active and ' ● ' or ' ' + local line = string.format( + ' %-3s %-30s %-10s %-15s %s', + status, + name, + short_rev(item.rev), + plugin_version(item), + item.spec.src + ) + lines[#lines + 1] = line + state.line_to_item[#lines] = item + end + + if #state.items == 0 then + lines[#lines + 1] = ' No plugins managed by vim.pack.' + end + + vim.bo[state.buf].modifiable = true + vim.api.nvim_buf_set_lines(state.buf, 0, -1, false, lines) + vim.bo[state.buf].modifiable = false + + -- Highlights + vim.api.nvim_buf_add_highlight(state.buf, ns_id, 'Title', 0, 0, -1) + vim.api.nvim_buf_add_highlight(state.buf, ns_id, 'NonText', 1, 0, -1) + vim.api.nvim_buf_add_highlight(state.buf, ns_id, 'Comment', 2, 0, -1) + vim.api.nvim_buf_add_highlight(state.buf, ns_id, 'Label', 4, 0, -1) + + for i = 6, #lines do + local item = state.line_to_item[i] + if item then + if item.active then + vim.api.nvim_buf_add_highlight(state.buf, ns_id, 'DiagnosticOk', i - 1, 2, 5) + end + vim.api.nvim_buf_add_highlight(state.buf, ns_id, 'Directory', i - 1, 6, 36) + vim.api.nvim_buf_add_highlight(state.buf, ns_id, 'Constant', i - 1, 37, 47) + vim.api.nvim_buf_add_highlight(state.buf, ns_id, 'Special', i - 1, 48, 63) + vim.api.nvim_buf_add_highlight(state.buf, ns_id, 'Comment', i - 1, 64, -1) + end + end +end + +local function run_for_item(action) + local item = item_at_cursor() + if not item then + notify('No plugin under cursor', vim.log.levels.WARN) + return + end + + action(item, plugin_name(item)) +end + +local function update_item() + run_for_item(function(_, name) + notify('Updating ' .. name .. '...') + vim.pack.update({ name }) + vim.defer_fn(render, 500) + end) +end + +local function update_all() + notify('Updating all plugins...') + vim.pack.update() + vim.defer_fn(render, 1000) +end + +local function delete_item(force) + run_for_item(function(_, name) + local choice = vim.fn.confirm('Delete ' .. name .. '?', '&Yes\n&No', 2) + if choice ~= 1 then + return + end + + local ok, err = pcall(vim.pack.del, { name }, { force = force }) + if not ok then + notify(err, vim.log.levels.ERROR) + return + end + + notify('Deleted ' .. name) + render() + end) +end + +local function delete_inactive() + local inactive_names = {} + for _, item in ipairs(state.items) do + if not item.active then + table.insert(inactive_names, plugin_name(item)) + end + end + + if #inactive_names == 0 then + notify('No inactive plugins to delete') + return + end + + local choice = vim.fn.confirm('Delete ' .. #inactive_names .. ' inactive plugins?', '&Yes\n&No', 2) + if choice ~= 1 then + return + end + + notify('Deleting inactive plugins...') + local ok, err = pcall(vim.pack.del, inactive_names) + if not ok then + notify(err, vim.log.levels.ERROR) + return + end + + notify('Cleaned ' .. #inactive_names .. ' plugins') + render() +end + +local function inspect_item() + run_for_item(function(_, name) + local ok, items = pcall(vim.pack.get, { name }, { info = true }) + if not ok then + notify(items, vim.log.levels.ERROR) + return + end + + vim.print(items[1]) + end) +end + +local function open_path() + run_for_item(function(item) + close() + vim.cmd.edit(vim.fn.fnameescape(item.path)) + end) +end + +local function yank_source() + run_for_item(function(item, name) + vim.fn.setreg('+', item.spec.src) + notify('Yanked source for ' .. name) + end) +end + +local function map(lhs, rhs, desc) + vim.keymap.set('n', lhs, rhs, { buffer = state.buf, silent = true, nowait = true, desc = desc }) +end + +function M.open() + if state.win and vim.api.nvim_win_is_valid(state.win) then + vim.api.nvim_set_current_win(state.win) + render() + return + end + + local width = math.min(120, vim.o.columns - 10) + local height = math.min(30, vim.o.lines - 10) + + state.buf = vim.api.nvim_create_buf(false, true) + state.win = vim.api.nvim_open_win(state.buf, true, { + relative = 'editor', + style = 'minimal', + border = 'rounded', + title = ' 📦 vim.pack ', + title_pos = 'center', + width = width, + height = height, + row = math.floor((vim.o.lines - height) / 2), + col = math.floor((vim.o.columns - width) / 2), + }) + + vim.bo[state.buf].bufhidden = 'wipe' + vim.bo[state.buf].buftype = 'nofile' + vim.bo[state.buf].filetype = 'packui' + vim.bo[state.buf].swapfile = false + vim.wo[state.win].cursorline = true + vim.wo[state.win].wrap = false + + map('q', close, 'Close UI') + map('', close, 'Close UI') + map('r', render, 'Refresh') + map('u', update_item, 'Update plugin') + map('U', update_all, 'Update all') + map('d', function() delete_item(false) end, 'Delete plugin') + map('D', function() delete_item(true) end, 'Force delete') + map('i', inspect_item, 'Inspect plugin') + map('o', open_path, 'Open path') + map('', open_path, 'Open path') + map('y', yank_source, 'Yank source') + map('X', delete_inactive, 'Delete inactive plugins') + + render() + vim.api.nvim_win_set_cursor(state.win, { 6, 2 }) +end + +function M.setup() + vim.api.nvim_create_user_command('Pack', M.open, { desc = 'Open vim.pack plugin UI' }) + vim.api.nvim_create_user_command('PackUI', M.open, { desc = 'Open vim.pack plugin UI' }) +end + +return M