/ nvim / lua / pack.lua
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