|
| 1 | +local Config = require("lazy.core.config") |
| 2 | +local Util = require("lazy.core.util") |
| 3 | +local Module = require("lazy.core.module") |
| 4 | +local Cache = require("lazy.core.cache") |
| 5 | + |
| 6 | +local M = {} |
| 7 | + |
| 8 | +---@alias CachedPlugin LazyPlugin | {_funs: string[]} |
| 9 | +local skip = { installed = true, loaded = true, tasks = true, dirty = true, dir = true } |
| 10 | +local funs = { config = true, init = true, run = true } |
| 11 | + |
| 12 | +---@class LazyPlugin |
| 13 | +---@field [1] string |
| 14 | +---@field name string display name and name used for plugin config files |
| 15 | +---@field uri string |
| 16 | +---@field branch? string |
| 17 | +---@field dir string |
| 18 | +---@field enabled? boolean |
| 19 | +---@field opt? boolean |
| 20 | +---@field init? fun(LazyPlugin) Will always be run |
| 21 | +---@field config? fun(LazyPlugin) Will be executed when loading the plugin |
| 22 | +---@field event? string|string[] |
| 23 | +---@field cmd? string|string[] |
| 24 | +---@field ft? string|string[] |
| 25 | +---@field module? string|string[] |
| 26 | +---@field keys? string|string[] |
| 27 | +---@field requires? string[] |
| 28 | +---@field loaded? {[string]:string, time:number} |
| 29 | +---@field installed? boolean |
| 30 | +---@field run? string|fun() |
| 31 | +---@field tasks? LazyTask[] |
| 32 | +---@field dirty? boolean |
| 33 | +---@field updated? {from:string, to:string} |
| 34 | + |
| 35 | +---@class LazySpec |
| 36 | +---@field modname string |
| 37 | +---@field modpath string |
| 38 | +---@field plugins table<string, LazyPlugin> |
| 39 | +local Spec = {} |
| 40 | + |
| 41 | +---@param modname string |
| 42 | +---@param modpath string |
| 43 | +function Spec.load(modname, modpath) |
| 44 | + local self = setmetatable({}, { __index = Spec }) |
| 45 | + self.plugins = {} |
| 46 | + self.modname = modname |
| 47 | + self.modpath = modpath |
| 48 | + self:normalize(assert(Module.load(modname, modpath))) |
| 49 | + if modname == Config.options.plugins and not self.plugins["lazy.nvim"] then |
| 50 | + self:add({ "folke/lazy.nvim", opt = false }) |
| 51 | + end |
| 52 | + return self |
| 53 | +end |
| 54 | + |
| 55 | +---@param plugin LazyPlugin |
| 56 | +function Spec:add(plugin) |
| 57 | + if type(plugin[1]) ~= "string" then |
| 58 | + Util.error("Invalid plugin spec " .. vim.inspect(plugin)) |
| 59 | + end |
| 60 | + plugin.uri = plugin.uri or ("https://github.com/" .. plugin[1] .. ".git") |
| 61 | + if not plugin.name then |
| 62 | + -- PERF: optimized code to get package name without using lua patterns |
| 63 | + local name = plugin[1]:sub(-4) == ".git" and plugin[1]:sub(1, -5) or plugin[1] |
| 64 | + local slash = name:reverse():find("/", 1, true) --[[@as number?]] |
| 65 | + plugin.name = slash and name:sub(#name - slash + 2) or plugin[1]:gsub("%W+", "_") |
| 66 | + end |
| 67 | + |
| 68 | + M.process_local(plugin) |
| 69 | + local other = self.plugins[plugin.name] |
| 70 | + self.plugins[plugin.name] = other and vim.tbl_extend("force", self.plugins[plugin.name], plugin) or plugin |
| 71 | + return self.plugins[plugin.name] |
| 72 | +end |
| 73 | + |
| 74 | +---@param spec table |
| 75 | +---@param results? string[] |
| 76 | +function Spec:normalize(spec, results) |
| 77 | + results = results or {} |
| 78 | + if type(spec) == "string" then |
| 79 | + table.insert(results, self:add({ spec }).name) |
| 80 | + elseif #spec > 1 or Util.is_list(spec) then |
| 81 | + ---@cast spec table[] |
| 82 | + for _, s in ipairs(spec) do |
| 83 | + self:normalize(s, results) |
| 84 | + end |
| 85 | + elseif spec.enabled ~= false then |
| 86 | + local plugin = self:add(spec) |
| 87 | + plugin.requires = plugin.requires and self:normalize(plugin.requires, {}) or nil |
| 88 | + table.insert(results, plugin.name) |
| 89 | + end |
| 90 | + return results |
| 91 | +end |
| 92 | + |
| 93 | +---@param spec CachedSpec |
| 94 | +function Spec.revive(spec) |
| 95 | + if spec.funs then |
| 96 | + ---@type LazySpec |
| 97 | + local loaded = nil |
| 98 | + for fun, plugins in pairs(spec.funs) do |
| 99 | + for _, name in pairs(plugins) do |
| 100 | + spec.plugins[name][fun] = function(...) |
| 101 | + loaded = loaded or Spec.load(spec.modname, spec.modpath) |
| 102 | + return loaded.plugins[name][fun](...) |
| 103 | + end |
| 104 | + end |
| 105 | + end |
| 106 | + end |
| 107 | + return spec |
| 108 | +end |
| 109 | + |
| 110 | +function M.update_state(check_clean) |
| 111 | + ---@type table<"opt"|"start", table<string,boolean>> |
| 112 | + local installed = { opt = {}, start = {} } |
| 113 | + for opt, packs in pairs(installed) do |
| 114 | + Util.scandir(Config.options.package_path .. "/" .. opt, function(_, name, type) |
| 115 | + if type == "directory" or type == "link" then |
| 116 | + packs[name] = true |
| 117 | + end |
| 118 | + end) |
| 119 | + end |
| 120 | + |
| 121 | + for _, plugin in pairs(Config.plugins) do |
| 122 | + plugin[1] = plugin["1"] or plugin[1] |
| 123 | + plugin.opt = plugin.opt == nil and Config.options.opt or plugin.opt |
| 124 | + local opt = plugin.opt and "opt" or "start" |
| 125 | + plugin.dir = Config.options.package_path .. "/" .. opt .. "/" .. plugin.name |
| 126 | + plugin.installed = installed[opt][plugin.name] == true |
| 127 | + installed[opt][plugin.name] = nil |
| 128 | + end |
| 129 | + |
| 130 | + if check_clean then |
| 131 | + Config.to_clean = {} |
| 132 | + for opt, packs in pairs(installed) do |
| 133 | + for pack in pairs(packs) do |
| 134 | + table.insert(Config.to_clean, { |
| 135 | + name = pack, |
| 136 | + pack = pack, |
| 137 | + dir = Config.options.package_path .. "/" .. opt .. "/" .. pack, |
| 138 | + opt = opt == "opt", |
| 139 | + installed = true, |
| 140 | + }) |
| 141 | + end |
| 142 | + end |
| 143 | + end |
| 144 | +end |
| 145 | + |
| 146 | +---@param plugin LazyPlugin |
| 147 | +function M.process_local(plugin) |
| 148 | + for _, pattern in ipairs(Config.options.plugins_local.patterns) do |
| 149 | + if plugin[1]:find(pattern, 1, true) then |
| 150 | + plugin.uri = Config.options.plugins_local.path .. "/" .. plugin.name |
| 151 | + return |
| 152 | + end |
| 153 | + end |
| 154 | +end |
| 155 | + |
| 156 | +---@alias LazySpecLoader fun(modname:string, modpath:string):LazySpec |
| 157 | +---@param loader? LazySpecLoader |
| 158 | +function M.specs(loader) |
| 159 | + loader = loader or Spec.load |
| 160 | + ---@type LazySpec[] |
| 161 | + local specs = {} |
| 162 | + |
| 163 | + local function _load(modname, modpath) |
| 164 | + local spec = Util.try(function() |
| 165 | + return loader(modname, modpath) |
| 166 | + end, "Failed to load **" .. modname .. "**") |
| 167 | + if spec then |
| 168 | + table.insert(specs, spec) |
| 169 | + end |
| 170 | + end |
| 171 | + |
| 172 | + _load(Config.options.plugins, Config.paths.main) |
| 173 | + Util.lsmod(Config.paths.plugins, function(name, modpath) |
| 174 | + _load(Config.options.plugins .. "." .. name, modpath) |
| 175 | + end) |
| 176 | + return specs |
| 177 | +end |
| 178 | + |
| 179 | +function M.load() |
| 180 | + local dirty = false |
| 181 | + |
| 182 | + ---@type boolean, LazyState? |
| 183 | + local ok, state = pcall(vim.json.decode, Cache.get("cache.state")) |
| 184 | + if not (ok and state and vim.deep_equal(Config.options, state.config)) then |
| 185 | + dirty = true |
| 186 | + state = nil |
| 187 | + end |
| 188 | + |
| 189 | + local loader = state |
| 190 | + and function(modname, modpath) |
| 191 | + local spec = state and state.specs[modname] |
| 192 | + if (not spec) or Module.is_dirty(modname, modpath) then |
| 193 | + dirty = true |
| 194 | + vim.schedule(function() |
| 195 | + vim.notify("Reloading " .. modname) |
| 196 | + end) |
| 197 | + return Spec.load(modname, modpath) |
| 198 | + end |
| 199 | + return Spec.revive(spec) |
| 200 | + end |
| 201 | + |
| 202 | + -- load specs |
| 203 | + Util.track("specs") |
| 204 | + local specs = M.specs(loader) |
| 205 | + Util.track() |
| 206 | + |
| 207 | + -- merge |
| 208 | + Config.plugins = {} |
| 209 | + for _, spec in ipairs(specs) do |
| 210 | + for _, plugin in pairs(spec.plugins) do |
| 211 | + local other = Config.plugins[plugin.name] |
| 212 | + Config.plugins[plugin.name] = other and vim.tbl_extend("force", other, plugin) or plugin |
| 213 | + end |
| 214 | + end |
| 215 | + |
| 216 | + Util.track("state") |
| 217 | + M.update_state() |
| 218 | + Util.track() |
| 219 | + |
| 220 | + if dirty then |
| 221 | + Cache.dirty = true |
| 222 | + elseif state then |
| 223 | + require("lazy.core.loader").loaders = state.loaders |
| 224 | + end |
| 225 | +end |
| 226 | + |
| 227 | +function M.save() |
| 228 | + if not M.dirty then |
| 229 | + return |
| 230 | + end |
| 231 | + |
| 232 | + ---@alias CachedSpec LazySpec|{funs:table<string, string[]>} |
| 233 | + ---@class LazyState |
| 234 | + local state = { |
| 235 | + ---@type table<string, CachedSpec> |
| 236 | + specs = {}, |
| 237 | + loaders = require("lazy.core.loader").loaders, |
| 238 | + config = Config.options, |
| 239 | + } |
| 240 | + |
| 241 | + for _, spec in ipairs(M.specs()) do |
| 242 | + ---@cast spec CachedSpec |
| 243 | + spec.funs = {} |
| 244 | + state.specs[spec.modname] = spec |
| 245 | + for _, plugin in pairs(spec.plugins) do |
| 246 | + ---@cast plugin CachedPlugin |
| 247 | + for k, v in pairs(plugin) do |
| 248 | + if type(v) == "function" then |
| 249 | + if funs[k] then |
| 250 | + spec.funs[k] = spec.funs[k] or {} |
| 251 | + table.insert(spec.funs[k], plugin.name) |
| 252 | + end |
| 253 | + plugin[k] = nil |
| 254 | + elseif skip[k] then |
| 255 | + plugin[k] = nil |
| 256 | + end |
| 257 | + end |
| 258 | + end |
| 259 | + end |
| 260 | + Cache.set("cache.state", vim.json.encode(state)) |
| 261 | +end |
| 262 | + |
| 263 | +return M |
0 commit comments