lazy.nvim/lua/lazy/core/plugin.lua

453 lines
12 KiB
Lua
Raw Normal View History

local Config = require("lazy.core.config")
2024-06-22 21:18:26 +01:00
local Meta = require("lazy.core.meta")
local Pkg = require("lazy.pkg")
2023-10-09 10:25:42 +01:00
local Util = require("lazy.core.util")
---@class LazyCorePlugin
local M = {}
M.loading = false
2022-11-28 06:36:32 +00:00
---@class LazySpecLoader
2024-06-22 21:18:26 +01:00
---@field meta LazyMeta
---@field plugins table<string, LazyPlugin>
---@field disabled table<string, LazyPlugin>
---@field ignore_installed table<string, true>
---@field modules string[]
---@field notifs {msg:string, level:number, file?:string}[]
---@field importing? string
2023-05-23 07:57:53 +01:00
---@field optional? boolean
---@field pkgs table<string, boolean>
local Spec = {}
M.Spec = Spec
M.LOCAL_SPEC = ".lazy.lua"
---@param spec? LazySpec
2023-05-23 07:57:53 +01:00
---@param opts? {optional?:boolean}
function Spec.new(spec, opts)
2024-06-22 21:18:26 +01:00
local self = setmetatable({}, Spec)
self.meta = Meta.new(self)
self.disabled = {}
self.modules = {}
self.notifs = {}
self.ignore_installed = {}
self.pkgs = {}
2023-05-23 07:57:53 +01:00
self.optional = opts and opts.optional
if spec then
self:parse(spec)
end
return self
end
2024-06-22 21:18:26 +01:00
function Spec:__index(key)
if Spec[key] then
return Spec[key]
end
if key == "plugins" then
self.meta:rebuild()
return self.meta.plugins
end
end
function Spec:parse(spec)
self:normalize(spec)
2024-06-22 21:30:20 +01:00
self.meta:resolve()
end
-- PERF: optimized code to get package name without using lua patterns
2024-06-18 18:40:58 +01:00
---@return string
function Spec.get_name(pkg)
local name = pkg:sub(-4) == ".git" and pkg:sub(1, -5) or pkg
name = name:sub(-1) == "/" and name:sub(1, -2) or name
local slash = name:reverse():find("/", 1, true) --[[@as number?]]
return slash and name:sub(#name - slash + 2) or pkg:gsub("%W+", "_")
end
2024-06-22 21:18:26 +01:00
---@param plugin LazyPluginSpec
function Spec:add(plugin)
self.meta:add(plugin)
2024-06-22 21:18:26 +01:00
---@diagnostic disable-next-line: cast-type-mismatch
---@cast plugin LazyPlugin
2024-06-18 18:40:58 +01:00
-- import the plugin's spec
if Config.options.pkg.enabled and plugin.dir and not self.pkgs[plugin.dir] then
self.pkgs[plugin.dir] = true
local pkg = Pkg.get_spec(plugin)
if pkg then
2024-06-22 21:18:26 +01:00
self:normalize(pkg)
2024-06-18 18:40:58 +01:00
end
end
return plugin
end
function Spec:error(msg)
self:log(msg, vim.log.levels.ERROR)
end
function Spec:warn(msg)
self:log(msg, vim.log.levels.WARN)
end
---@param msg string
---@param level number
function Spec:log(msg, level)
self.notifs[#self.notifs + 1] = { msg = msg, level = level, file = self.importing }
end
function Spec:report(level)
level = level or vim.log.levels.ERROR
local count = 0
for _, notif in ipairs(self.notifs) do
if notif.level >= level then
Util.notify(notif.msg, { level = notif.level })
count = count + 1
end
end
return count
end
---@param spec LazySpec|LazySpecImport
2024-06-22 21:18:26 +01:00
function Spec:normalize(spec)
if type(spec) == "string" then
2024-06-22 21:18:26 +01:00
self:add({ spec })
elseif #spec > 1 or Util.is_list(spec) then
2022-11-28 06:36:32 +00:00
---@cast spec LazySpec[]
for _, s in ipairs(spec) do
2024-06-22 21:18:26 +01:00
self:normalize(s)
end
elseif spec[1] or spec.dir or spec.url then
2024-06-22 21:18:26 +01:00
---@cast spec LazyPluginSpec
local plugin = self:add(spec)
---@diagnostic disable-next-line: cast-type-mismatch
---@cast plugin LazySpecImport
if plugin and plugin.import then
self:import(plugin)
end
elseif spec.import then
---@cast spec LazySpecImport
self:import(spec)
else
self:error("Invalid plugin spec " .. vim.inspect(spec))
end
end
---@param spec LazySpecImport
function Spec:import(spec)
if spec.import == "lazy" then
return self:error("You can't name your plugins module `lazy`.")
end
if type(spec.import) == "function" then
if not spec.name then
return self:error("Invalid import spec. Missing name: " .. vim.inspect(spec))
end
elseif type(spec.import) ~= "string" then
return self:error("Invalid import spec. `import` should be a string: " .. vim.inspect(spec))
end
local import_name = spec.name or spec.import
---@cast import_name string
if vim.tbl_contains(self.modules, import_name) then
return
end
if spec.cond == false or (type(spec.cond) == "function" and not spec.cond()) then
return
end
if spec.enabled == false or (type(spec.enabled) == "function" and not spec.enabled()) then
return
end
self.modules[#self.modules + 1] = import_name
local import = spec.import
local imported = 0
---@type (string|(fun():LazyPluginSpec))[]
local modspecs = {}
if type(import) == "string" then
Util.lsmod(import, function(modname)
modspecs[#modspecs + 1] = modname
end)
table.sort(modspecs)
else
modspecs = { spec.import }
end
for _, modspec in ipairs(modspecs) do
imported = imported + 1
local modname = type(modspec) == "string" and modspec or import_name
Util.track({ import = modname })
self.importing = modname
-- unload the module so we get a clean slate
---@diagnostic disable-next-line: no-unknown
package.loaded[modname] = nil
Util.try(function()
local mod = type(modspec) == "function" and modspec() or require(modspec)
if type(mod) ~= "table" then
self.importing = nil
return self:error(
"Invalid spec module: `"
.. modname
.. "`\nExpected a `table` of specs, but a `"
.. type(mod)
.. "` was returned instead"
)
end
self:normalize(mod)
self.importing = nil
Util.track()
end, {
msg = "Failed to load `" .. modname .. "`",
on_error = function(msg)
self:error(msg)
self.importing = nil
Util.track()
end,
})
end
if imported == 0 then
self:error("No specs found for module " .. spec.import)
end
end
function M.update_state()
---@type string[]
local cloning = {}
---@type table<string,FileType>
local installed = {}
Util.ls(Config.options.root, function(_, name, type)
2022-12-20 06:19:55 +00:00
if type == "directory" and name ~= "readme" then
installed[name] = type
elseif type == "file" and name:sub(-8) == ".cloning" then
name = name:sub(1, -9)
cloning[#cloning + 1] = name
end
end)
for _, failed in ipairs(cloning) do
installed[failed] = nil
end
for _, plugin in pairs(Config.plugins) do
plugin._ = plugin._ or {}
if plugin.lazy == nil then
2022-12-02 16:09:40 +00:00
local lazy = plugin._.dep
or Config.options.defaults.lazy
or plugin.event
or plugin.keys
or plugin.ft
or plugin.cmd
plugin.lazy = lazy and true or false
end
if plugin.dir:find(Config.options.root, 1, true) == 1 then
plugin._.installed = installed[plugin.name] ~= nil
installed[plugin.name] = nil
else
plugin._.is_local = true
plugin._.installed = true -- local plugins are managed by the user
end
end
for name in pairs(Config.spec.ignore_installed) do
installed[name] = nil
end
2024-06-18 18:39:47 +01:00
require("lazy.manage.rocks").update_state()
Config.to_clean = {}
for pack, dir_type in pairs(installed) do
table.insert(Config.to_clean, {
name = pack,
dir = Config.options.root .. "/" .. pack,
_ = {
kind = "clean",
installed = true,
is_symlink = dir_type == "link",
is_local = dir_type == "link",
},
})
end
end
---@param path string
function M.local_spec(path)
local file = vim.secure.read(path)
if file then
return loadstring(file)()
end
return {}
end
---@return LazySpecImport?
function M.find_local_spec()
if not Config.options.local_spec then
return
end
local path = vim.uv.cwd()
while path ~= "" do
local file = path .. "/" .. M.LOCAL_SPEC
if vim.fn.filereadable(file) == 1 then
return {
name = vim.fn.fnamemodify(file, ":~:."),
import = function()
local data = vim.secure.read(file)
if data then
return loadstring(data)()
end
return {}
end,
}
end
local p = vim.fn.fnamemodify(path, ":h")
if p == path then
break
end
path = p
end
end
function M.load()
M.loading = true
-- load specs
Util.track("spec")
Config.spec = Spec.new()
local specs = {
2024-06-22 21:18:26 +01:00
---@diagnostic disable-next-line: param-type-mismatch
vim.deepcopy(Config.options.spec),
}
specs[#specs + 1] = M.find_local_spec()
specs[#specs + 1] = { "folke/lazy.nvim" }
Config.spec:parse(specs)
-- override some lazy props
local lazy = Config.spec.plugins["lazy.nvim"]
if lazy then
lazy.lazy = true
lazy.dir = Config.me
lazy.config = function()
error("lazy config should not be called")
end
lazy._.loaded = {}
end
local existing = Config.plugins
Config.plugins = Config.spec.plugins
-- copy state. This wont do anything during startup
for name, plugin in pairs(existing) do
if Config.plugins[name] then
local dep = Config.plugins[name]._.dep
2024-06-22 21:18:26 +01:00
local frags = Config.plugins[name]._.frags
Config.plugins[name]._ = plugin._
Config.plugins[name]._.dep = dep
2024-06-22 21:18:26 +01:00
Config.plugins[name]._.frags = frags
end
end
Util.track()
Util.track("state")
M.update_state()
Util.track()
M.loading = false
vim.api.nvim_exec_autocmds("User", { pattern = "LazyPlugins", modeline = false })
end
-- Finds the plugin that has this path
---@param path string
function M.find(path)
2023-01-02 13:35:36 +00:00
if not Config.spec then
return
end
local lua = path:find("/lua/", 1, true)
if lua then
local name = path:sub(1, lua - 1)
local slash = name:reverse():find("/", 1, true)
if slash then
name = name:sub(#name - slash + 2)
return name and Config.plugins[name] or Config.spec.plugins[name] or nil
end
end
end
---@param plugin LazyPlugin
function M.has_errors(plugin)
for _, task in ipairs(plugin._.tasks or {}) do
if task.error then
return true
end
end
return false
end
-- Merges super values or runs the values function to override values or return new ones.
-- Values are cached for performance.
-- Used for opts, cmd, event, ft and keys
---@param plugin LazyPlugin
---@param prop string
---@param is_list? boolean
function M.values(plugin, prop, is_list)
if not plugin[prop] then
return {}
end
plugin._.cache = plugin._.cache or {}
local key = prop .. (is_list and "_list" or "")
if plugin._.cache[key] == nil then
plugin._.cache[key] = M._values(plugin, plugin, prop, is_list)
end
return plugin._.cache[key]
end
-- Merges super values or runs the values function to override values or return new ones
-- Used for opts, cmd, event, ft and keys
---@param root LazyPlugin
---@param plugin LazyPlugin
---@param prop string
---@param is_list? boolean
function M._values(root, plugin, prop, is_list)
if not plugin[prop] then
return {}
end
2024-06-22 21:18:26 +01:00
local super = getmetatable(plugin)
---@type table
2024-06-22 21:18:26 +01:00
local ret = super and M._values(root, super.__index, prop, is_list) or {}
local values = rawget(plugin, prop)
if not values then
return ret
elseif type(values) == "function" then
ret = values(root, ret) or ret
return type(ret) == "table" and ret or { ret }
end
values = type(values) == "table" and values or { values }
if is_list then
return Util.extend(ret, values)
else
---@type {path:string[], list:any[]}[]
local lists = {}
2024-06-22 21:18:26 +01:00
---@diagnostic disable-next-line: no-unknown
for _, key in ipairs(plugin[prop .. "_extend"] or {}) do
local path = vim.split(key, ".", { plain = true })
local r = Util.key_get(ret, path)
local v = Util.key_get(values, path)
if type(r) == "table" and type(v) == "table" then
lists[key] = { path = path, list = {} }
vim.list_extend(lists[key].list, r)
vim.list_extend(lists[key].list, v)
end
end
local t = Util.merge(ret, values)
for _, list in pairs(lists) do
Util.key_set(t, list.path, list.list)
end
return t
end
end
return M