2022-11-30 21:19:50 +08:00
|
|
|
local ffi = require("ffi")
|
|
|
|
---@diagnostic disable-next-line: no-unknown
|
|
|
|
local uv = vim.loop
|
2022-11-23 04:12:33 +08:00
|
|
|
|
|
|
|
local M = {}
|
2022-11-30 21:19:50 +08:00
|
|
|
M.dirty = false
|
2022-12-19 19:21:20 +08:00
|
|
|
M.VERSION = "1"
|
2022-11-23 04:12:33 +08:00
|
|
|
|
2022-12-02 19:43:34 +08:00
|
|
|
---@class LazyCacheConfig
|
|
|
|
M.config = {
|
|
|
|
enabled = true,
|
|
|
|
path = vim.fn.stdpath("state") .. "/lazy.state",
|
2022-12-04 01:59:28 +08:00
|
|
|
-- Once one of the following events triggers, caching will be disabled.
|
|
|
|
-- To cache all modules, set this to `{}`, but that is not recommended.
|
|
|
|
-- The default is to disable on:
|
|
|
|
-- * VimEnter: not useful to cache anything else beyond startup
|
|
|
|
-- * BufReadPre: this will be triggered early when opening a file from the command line directly
|
|
|
|
disable_events = { "VimEnter", "BufReadPre" },
|
2022-12-02 19:43:34 +08:00
|
|
|
}
|
2022-12-02 23:52:22 +08:00
|
|
|
M.debug = false
|
2022-12-02 19:43:34 +08:00
|
|
|
|
2022-11-30 21:19:50 +08:00
|
|
|
---@type CacheHash
|
|
|
|
local cache_hash
|
|
|
|
|
|
|
|
---@alias CacheHash {mtime: {sec:number, nsec:number}, size:number}
|
2022-12-02 16:20:48 +08:00
|
|
|
---@alias CacheEntry {hash:CacheHash, modpath:string, chunk:string, used:number}
|
2022-11-30 21:19:50 +08:00
|
|
|
---@type table<string,CacheEntry?>
|
|
|
|
M.cache = {}
|
2022-12-02 16:20:48 +08:00
|
|
|
M.loader_idx = 2 -- 2 so preload still works
|
|
|
|
M.enabled = true
|
|
|
|
M.ttl = 3600 * 24 * 5 -- keep unused modules for up to 5 days
|
2022-12-19 19:21:20 +08:00
|
|
|
---@type string[]
|
|
|
|
M.rtp = nil
|
|
|
|
|
|
|
|
-- checks wether the cached modpath is still valid
|
|
|
|
function M.check_path(modname, modpath)
|
|
|
|
-- check rtp exlcuding plugins. This is a very small list, so should be fast
|
|
|
|
for _, path in ipairs(M.get_rtp()) do
|
|
|
|
if modpath:find(path, 1, true) == 1 then
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
end
|
2022-11-23 04:12:33 +08:00
|
|
|
|
2022-12-19 19:21:20 +08:00
|
|
|
-- the correct lazy path should be part of rtp.
|
|
|
|
-- so if we get here, this is folke using the local dev instance ;)
|
2022-12-02 16:20:48 +08:00
|
|
|
if modname:sub(1, 4) == "lazy" then
|
2022-12-19 19:21:20 +08:00
|
|
|
return false
|
|
|
|
end
|
|
|
|
|
|
|
|
-- check plugins. Again fast, since we check the plugin name from the path.
|
|
|
|
-- only needed when the plugin mod has been loaded
|
|
|
|
if package.loaded["lazy.core.plugin"] then
|
|
|
|
local plugin = require("lazy.core.plugin").find(modpath)
|
|
|
|
if plugin and modpath:find(plugin.dir, 1, true) == 1 then
|
|
|
|
if not plugin._.loaded then
|
|
|
|
require("lazy.core.loader").load(plugin, { require = modname })
|
|
|
|
end
|
|
|
|
return true
|
|
|
|
end
|
2022-12-02 16:20:48 +08:00
|
|
|
end
|
2022-12-19 19:21:20 +08:00
|
|
|
return false
|
2022-12-02 16:20:48 +08:00
|
|
|
end
|
2022-11-25 05:04:23 +08:00
|
|
|
|
2022-12-04 01:59:28 +08:00
|
|
|
function M.disable()
|
2022-12-02 16:20:48 +08:00
|
|
|
if not M.enabled then
|
2022-12-02 19:43:34 +08:00
|
|
|
return
|
|
|
|
end
|
|
|
|
local idx = M.idx()
|
|
|
|
if idx then
|
|
|
|
table.remove(package.loaders, idx)
|
|
|
|
end
|
|
|
|
M.enabled = false
|
|
|
|
end
|
|
|
|
|
|
|
|
---@param modname string
|
|
|
|
---@return any
|
|
|
|
function M.loader(modname)
|
2022-12-02 16:20:48 +08:00
|
|
|
local entry = M.cache[modname]
|
|
|
|
|
2022-11-30 21:19:50 +08:00
|
|
|
local chunk, err
|
2022-12-19 19:21:20 +08:00
|
|
|
if entry and M.check_path(modname, entry.modpath) then
|
2022-12-02 16:20:48 +08:00
|
|
|
entry.used = os.time()
|
2022-12-03 02:16:21 +08:00
|
|
|
local hash = M.hash(entry.modpath)
|
|
|
|
if not hash then
|
|
|
|
return
|
|
|
|
end
|
2022-12-02 16:20:48 +08:00
|
|
|
if M.eq(entry.hash, hash) then
|
|
|
|
-- found in cache and up to date
|
|
|
|
chunk, err = load(entry.chunk --[[@as string]], "@" .. entry.modpath)
|
|
|
|
return chunk or error(err)
|
|
|
|
end
|
|
|
|
-- reload from file
|
|
|
|
entry.hash = hash
|
|
|
|
chunk, err = loadfile(entry.modpath)
|
2022-11-25 05:04:23 +08:00
|
|
|
else
|
2022-12-02 16:20:48 +08:00
|
|
|
-- load the module and find its modpath
|
|
|
|
local modpath
|
|
|
|
chunk, modpath = M.find(modname)
|
|
|
|
if modpath then
|
|
|
|
entry = { hash = M.hash(modpath), modpath = modpath, used = os.time() }
|
|
|
|
M.cache[modname] = entry
|
2022-11-23 04:12:33 +08:00
|
|
|
end
|
|
|
|
end
|
2022-12-02 23:52:22 +08:00
|
|
|
if M.debug then
|
|
|
|
vim.schedule(function()
|
|
|
|
vim.notify("[cache:load] " .. modname)
|
|
|
|
end)
|
|
|
|
end
|
2022-12-02 16:20:48 +08:00
|
|
|
if entry and chunk then
|
|
|
|
M.dirty = true
|
|
|
|
entry.chunk = string.dump(chunk)
|
|
|
|
end
|
|
|
|
return chunk or error(err)
|
2022-11-23 04:12:33 +08:00
|
|
|
end
|
|
|
|
|
2022-12-16 22:08:09 +08:00
|
|
|
function M.require(modname)
|
|
|
|
return M.loader(modname)()
|
|
|
|
end
|
|
|
|
|
2022-12-02 19:43:34 +08:00
|
|
|
function M.idx()
|
2022-12-02 16:20:48 +08:00
|
|
|
-- update our loader position if needed
|
|
|
|
if package.loaders[M.loader_idx] ~= M.loader then
|
2022-12-02 19:43:34 +08:00
|
|
|
M.loader_idx = nil
|
2022-11-25 05:04:23 +08:00
|
|
|
---@diagnostic disable-next-line: no-unknown
|
2022-12-02 16:20:48 +08:00
|
|
|
for i, loader in ipairs(package.loaders) do
|
|
|
|
if loader == M.loader then
|
|
|
|
M.loader_idx = i
|
|
|
|
break
|
|
|
|
end
|
2022-11-23 04:12:33 +08:00
|
|
|
end
|
2022-11-25 05:04:23 +08:00
|
|
|
end
|
2022-12-02 19:43:34 +08:00
|
|
|
return M.loader_idx
|
|
|
|
end
|
2022-12-02 16:20:48 +08:00
|
|
|
|
2022-12-02 19:43:34 +08:00
|
|
|
---@param modname string
|
|
|
|
function M.find(modname)
|
|
|
|
if M.idx() then
|
|
|
|
-- find the module and its modpath
|
|
|
|
for i = M.loader_idx + 1, #package.loaders do
|
|
|
|
---@diagnostic disable-next-line: no-unknown
|
|
|
|
local chunk = package.loaders[i](modname)
|
|
|
|
if type(chunk) == "function" then
|
|
|
|
local info = debug.getinfo(chunk, "S")
|
|
|
|
return chunk, (info.what ~= "C" and info.source:sub(2))
|
|
|
|
end
|
2022-12-02 16:20:48 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2022-12-19 19:21:20 +08:00
|
|
|
function M.get_rtp()
|
|
|
|
if not M.rtp then
|
|
|
|
M.rtp = {}
|
|
|
|
local skip = {}
|
|
|
|
-- only skip plugins once Config has been setup
|
|
|
|
if package.loaded["lazy.core.config"] then
|
|
|
|
local Config = require("lazy.core.config")
|
|
|
|
for _, plugin in ipairs(Config.plugins) do
|
|
|
|
if plugin.name ~= "lazy.nvim" then
|
|
|
|
skip[plugin.dir] = true
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
for _, path in ipairs(vim.api.nvim_list_runtime_paths()) do
|
|
|
|
if not skip[path] then
|
|
|
|
M.rtp[#M.rtp + 1] = path
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
return M.rtp
|
|
|
|
end
|
|
|
|
|
2022-12-02 19:43:34 +08:00
|
|
|
---@param opts? LazyConfig
|
|
|
|
function M.setup(opts)
|
|
|
|
-- no fancy deep extend here. just set the options
|
|
|
|
if opts and opts.performance and opts.performance.cache then
|
|
|
|
for k, v in pairs(opts.performance.cache) do
|
|
|
|
M.config[k] = v
|
|
|
|
end
|
|
|
|
end
|
2022-12-02 23:52:22 +08:00
|
|
|
M.debug = opts and opts.debug
|
2022-12-02 19:43:34 +08:00
|
|
|
|
2022-12-02 16:20:48 +08:00
|
|
|
M.load_cache()
|
|
|
|
table.insert(package.loaders, M.loader_idx, M.loader)
|
|
|
|
|
2022-12-19 19:21:20 +08:00
|
|
|
-- reset rtp when it changes
|
|
|
|
vim.api.nvim_create_autocmd("OptionSet", {
|
|
|
|
pattern = "runtimepath",
|
|
|
|
callback = function()
|
|
|
|
M.rtp = nil
|
|
|
|
end,
|
|
|
|
})
|
|
|
|
|
2022-12-04 01:59:28 +08:00
|
|
|
if #M.config.disable_events > 0 then
|
|
|
|
vim.api.nvim_create_autocmd(M.config.disable_events, { once = true, callback = M.disable })
|
2022-12-02 19:43:34 +08:00
|
|
|
end
|
|
|
|
return M
|
2022-11-23 04:12:33 +08:00
|
|
|
end
|
|
|
|
|
2022-11-30 21:19:50 +08:00
|
|
|
---@return CacheHash?
|
|
|
|
function M.hash(file)
|
2022-12-03 02:16:21 +08:00
|
|
|
local ok, ret = pcall(uv.fs_stat, file)
|
|
|
|
return ok and ret or nil
|
2022-11-30 21:19:50 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
---@param h1 CacheHash
|
|
|
|
---@param h2 CacheHash
|
|
|
|
function M.eq(h1, h2)
|
|
|
|
return h1 and h2 and h1.size == h2.size and h1.mtime.sec == h2.mtime.sec and h1.mtime.nsec == h2.mtime.nsec
|
|
|
|
end
|
|
|
|
|
|
|
|
function M.save_cache()
|
2022-12-02 19:43:34 +08:00
|
|
|
local f = assert(uv.fs_open(M.config.path, "w", 438))
|
2022-12-19 19:21:20 +08:00
|
|
|
uv.fs_write(f, M.VERSION)
|
2022-12-15 04:38:24 +08:00
|
|
|
uv.fs_write(f, "\0")
|
2022-11-30 21:19:50 +08:00
|
|
|
for modname, entry in pairs(M.cache) do
|
2022-12-02 16:20:48 +08:00
|
|
|
if entry.used > os.time() - M.ttl then
|
2022-11-30 21:19:50 +08:00
|
|
|
entry.modname = modname
|
2022-12-02 16:20:48 +08:00
|
|
|
local header = {
|
|
|
|
entry.hash.size,
|
|
|
|
entry.hash.mtime.sec,
|
|
|
|
entry.hash.mtime.nsec,
|
|
|
|
#modname,
|
|
|
|
#entry.chunk,
|
|
|
|
#entry.modpath,
|
|
|
|
entry.used,
|
|
|
|
}
|
|
|
|
uv.fs_write(f, ffi.string(ffi.new("const uint32_t[7]", header), 28))
|
2022-11-30 21:19:50 +08:00
|
|
|
uv.fs_write(f, modname)
|
|
|
|
uv.fs_write(f, entry.chunk)
|
2022-12-02 16:20:48 +08:00
|
|
|
uv.fs_write(f, entry.modpath)
|
2022-11-30 21:19:50 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
uv.fs_close(f)
|
|
|
|
end
|
|
|
|
|
|
|
|
function M.load_cache()
|
|
|
|
M.cache = {}
|
2022-12-02 19:43:34 +08:00
|
|
|
local f = uv.fs_open(M.config.path, "r", 438)
|
2022-11-30 21:19:50 +08:00
|
|
|
if f then
|
|
|
|
cache_hash = uv.fs_fstat(f) --[[@as CacheHash]]
|
|
|
|
local data = uv.fs_read(f, cache_hash.size, 0) --[[@as string]]
|
|
|
|
uv.fs_close(f)
|
|
|
|
|
2022-12-15 04:38:24 +08:00
|
|
|
local zero = data:find("\0", 1, true)
|
|
|
|
if not zero then
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
2022-12-19 19:21:20 +08:00
|
|
|
if M.VERSION ~= data:sub(1, zero - 1) then
|
2022-12-15 04:38:24 +08:00
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
local offset = zero + 1
|
2022-11-30 21:19:50 +08:00
|
|
|
while offset + 1 < #data do
|
2022-12-02 16:20:48 +08:00
|
|
|
local header = ffi.cast("uint32_t*", ffi.new("const char[28]", data:sub(offset, offset + 27)))
|
|
|
|
offset = offset + 28
|
2022-11-30 21:19:50 +08:00
|
|
|
local modname = data:sub(offset, offset + header[3] - 1)
|
|
|
|
offset = offset + header[3]
|
|
|
|
local chunk = data:sub(offset, offset + header[4] - 1)
|
|
|
|
offset = offset + header[4]
|
2022-12-02 16:20:48 +08:00
|
|
|
local file = data:sub(offset, offset + header[5] - 1)
|
|
|
|
offset = offset + header[5]
|
|
|
|
M.cache[modname] = {
|
|
|
|
hash = { size = header[0], mtime = { sec = header[1], nsec = header[2] } },
|
|
|
|
chunk = chunk,
|
|
|
|
modpath = file,
|
|
|
|
used = header[6],
|
|
|
|
}
|
2022-11-30 21:19:50 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
function M.autosave()
|
|
|
|
vim.api.nvim_create_autocmd("VimLeavePre", {
|
|
|
|
callback = function()
|
|
|
|
if M.dirty then
|
2022-12-02 19:43:34 +08:00
|
|
|
local hash = M.hash(M.config.path)
|
2022-11-30 21:19:50 +08:00
|
|
|
-- abort when the file was changed in the meantime
|
|
|
|
if hash == nil or M.eq(cache_hash, hash) then
|
|
|
|
M.save_cache()
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end,
|
|
|
|
})
|
2022-11-23 04:12:33 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
return M
|