pack.lua
1 --- Pack aggregator: collect vim.pack.Spec from lua/plugins/*.lua and register lazy triggers. 2 --- Requires Neovim 0.12+ (vim.pack). Preserves keybindings; plugins in pack/core/opt. 3 --- Each lua/plugins/*.lua returns { specs = {...}, lazy = {...} } (vim.pack.Spec table(s)). 4 5 local M = {} 6 7 local PACK_PLUGIN_DIR = 'plugins' 8 9 local function collect_plugin_modules() 10 local config = vim.fn.stdpath('config') 11 local plugin_dir = config .. '/lua/' .. PACK_PLUGIN_DIR 12 local pattern = plugin_dir .. '/*.lua' 13 local files = vim.fn.glob(pattern, true, true) or {} 14 local modules = {} 15 for _, path in ipairs(files) do 16 local basename = vim.fn.fnamemodify(path, ':t:r') 17 if basename and basename ~= '' then 18 table.insert(modules, PACK_PLUGIN_DIR .. '.' .. basename) 19 end 20 end 21 return modules 22 end 23 24 local function load_module_specs(module_name) 25 local ok, mod = pcall(require, module_name) 26 if not ok or type(mod) ~= 'table' then 27 return nil, nil 28 end 29 local raw = mod.specs or mod 30 if type(raw) ~= 'table' then 31 raw = {} 32 end 33 -- Allow single spec: { src = '...', name = '...' } 34 if raw.src then 35 raw = { raw } 36 end 37 -- Only include vim.pack.Spec entries (must have .src); ignore old lazy.nvim format 38 local specs = {} 39 for _, s in ipairs(raw) do 40 if type(s) == 'table' and s.src then 41 table.insert(specs, s) 42 end 43 end 44 local lazy = type(mod.lazy) == 'table' and mod.lazy or nil 45 return specs, lazy 46 end 47 48 local function register_lazy_trigger(name, trigger) 49 if trigger.ft and #trigger.ft > 0 then 50 vim.api.nvim_create_autocmd('FileType', { 51 pattern = trigger.ft, 52 callback = function() 53 vim.cmd.packadd(name) 54 end, 55 once = true, 56 }) 57 end 58 if trigger.cmd and #trigger.cmd > 0 then 59 for _, cmd in ipairs(trigger.cmd) do 60 -- When user runs :Foo, load plugin then re-dispatch (once: after packadd, command exists) 61 vim.api.nvim_create_autocmd('CmdUndefined', { 62 pattern = cmd, 63 callback = function(info) 64 vim.cmd.packadd(name) 65 vim.schedule(function() 66 vim.cmd(('%s'):format(info.match)) 67 end) 68 end, 69 once = true, 70 }) 71 end 72 end 73 if trigger.keys and #trigger.keys > 0 then 74 for _, key in ipairs(trigger.keys) do 75 local mode = 'n' 76 local key_str = key 77 if type(key) == 'table' then 78 mode = key[1] or 'n' 79 key_str = key[2] or key[1] 80 end 81 vim.keymap.set(mode, key_str, function() 82 vim.cmd.packadd(name) 83 vim.keymap.del(mode, key_str) 84 vim.schedule(function() 85 vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes(key_str, true, false, true), mode, false) 86 end) 87 end, { desc = 'packadd ' .. name }) 88 end 89 end 90 end 91 92 function M.setup() 93 if not vim.pack or not vim.pack.add then 94 vim.notify('vim.pack not available (Neovim 0.12+ required)', vim.log.levels.WARN) 95 return 96 end 97 98 local all_specs = {} 99 local lazy_specs = {} -- { { names = { ... }, lazy = {...} }, ... } 100 101 for _, module_name in ipairs(collect_plugin_modules()) do 102 local specs, lazy = load_module_specs(module_name) 103 if specs and #specs > 0 then 104 if lazy and (lazy.ft or lazy.cmd or lazy.keys) then 105 local names = {} 106 for _, s in ipairs(specs) do 107 local n = s.name or (s.src and vim.fn.fnamemodify(s.src:gsub('%.git$', ''), ':t') or nil) 108 if n then 109 table.insert(names, n) 110 end 111 end 112 if #names > 0 then 113 table.insert(lazy_specs, { names = names, lazy = lazy }) 114 for _, s in ipairs(specs) do 115 table.insert(all_specs, s) 116 end 117 else 118 for _, s in ipairs(specs) do 119 table.insert(all_specs, s) 120 end 121 end 122 else 123 for _, s in ipairs(specs) do 124 table.insert(all_specs, s) 125 end 126 end 127 end 128 end 129 130 -- Install all; load eager (no lazy) now; lazy ones stay opt until trigger 131 local eager_specs = {} 132 for _, s in ipairs(all_specs) do 133 local is_lazy = false 134 for _, entry in ipairs(lazy_specs) do 135 local n = s.name or (s.src and vim.fn.fnamemodify(s.src:gsub('%.git$', ''), ':t') or nil) 136 for _, name in ipairs(entry.names) do 137 if name == n then 138 is_lazy = true 139 break 140 end 141 end 142 if is_lazy then 143 break 144 end 145 end 146 if not is_lazy then 147 table.insert(eager_specs, s) 148 end 149 end 150 151 if #eager_specs > 0 then 152 vim.pack.add(eager_specs, { load = true, confirm = false }) 153 end 154 155 local lazy_names_set = {} 156 for _, entry in ipairs(lazy_specs) do 157 for _, name in ipairs(entry.names) do 158 lazy_names_set[name] = true 159 end 160 end 161 local lazy_only_specs = {} 162 for _, spec in ipairs(all_specs) do 163 local n = spec.name or (spec.src and vim.fn.fnamemodify(spec.src:gsub('%.git$', ''), ':t') or nil) 164 if n and lazy_names_set[n] then 165 table.insert(lazy_only_specs, spec) 166 end 167 end 168 if #lazy_only_specs > 0 then 169 vim.pack.add(lazy_only_specs, { load = false, confirm = false }) 170 end 171 for _, entry in ipairs(lazy_specs) do 172 for _, name in ipairs(entry.names) do 173 register_lazy_trigger(name, entry.lazy) 174 end 175 end 176 177 -- PackChanged: notify on update for critical plugins (optional hook) 178 local critical_plugins = { ['noice.nvim'] = true, ['yazi.nvim'] = true, ['nvim-treesitter'] = true } 179 vim.api.nvim_create_autocmd('User', { 180 pattern = 'PackChanged', 181 callback = function(data) 182 local ev = type(data) == 'table' and data or {} 183 if ev.kind == 'update' and ev.spec and critical_plugins[ev.spec.name] then 184 vim.notify(('Pack updated: %s'):format(ev.spec.name), vim.log.levels.INFO) 185 end 186 end, 187 }) 188 end 189 190 return M