263 lines
6.7 KiB
Lua
263 lines
6.7 KiB
Lua
local logger = require('dep.log')
|
|
local git = require('dep.git')
|
|
local packager = require('dep.package')
|
|
|
|
---all functions for convenience
|
|
---@type table
|
|
local M = {}
|
|
|
|
---@type boolean
|
|
local initialized
|
|
|
|
---performance logging
|
|
---@type table
|
|
local perf = {}
|
|
|
|
--- get execution time of a function
|
|
---@param name string name of performance output
|
|
---@param code function function to run
|
|
---@vararg any arguments for code
|
|
function M.benchmark(name, code, ...)
|
|
local start = os.clock()
|
|
code(...)
|
|
perf[name] = os.clock() - start
|
|
end
|
|
|
|
--- recurse over all packages and register them
|
|
---@param speclist spec[] table of specs
|
|
---@param overrides spec? a package spec that is used to override options
|
|
function M.registertree(speclist, overrides)
|
|
overrides = overrides or {}
|
|
|
|
-- recurse the packages
|
|
for _, spec in pairs(speclist) do
|
|
|
|
-- make sure the overrides override and take into account the packages spec
|
|
---@diagnostic disable-next-line: missing-fields
|
|
overrides = {
|
|
pin = overrides.pin or spec.pin,
|
|
disable = overrides.disable or spec.disable
|
|
}
|
|
|
|
local ok = packager:new(spec, overrides)
|
|
|
|
-- if erroring print out the spec and the error
|
|
if not ok then
|
|
error(string.format("%s (spec=%s)", err, vim.inspect(spec)))
|
|
end
|
|
end
|
|
end
|
|
|
|
--- reload all packages in package table spec
|
|
---@param force boolean|nil force all packages to load
|
|
function M.reload(force)
|
|
local reloaded = packager.get_root():loadtree(force)
|
|
|
|
if reloaded then
|
|
local ok, err
|
|
M.benchmark("reload", function()
|
|
ok, err = pcall(vim.cmd,
|
|
[[
|
|
silent! helptags ALL
|
|
silent! UpdateRemotePlugins
|
|
]])
|
|
end)
|
|
|
|
if ok then
|
|
logger:log("vim", "reloaded helptags and remote plugins")
|
|
else
|
|
logger:log("error",
|
|
"failed to reload helptags and remote plugins; reason: %s", err)
|
|
end
|
|
end
|
|
end
|
|
|
|
--- check if there's a circular dependency in the package tree
|
|
function M.findcycle()
|
|
local index = 0
|
|
local indexes = {}
|
|
local lowlink = {}
|
|
local stack = {}
|
|
|
|
-- use tarjan algorithm to find circular dependencies (strongly connected
|
|
-- components)
|
|
local function connect(package)
|
|
indexes[package.id], lowlink[package.id] = index, index
|
|
stack[#stack + 1], stack[package.id] = package, true
|
|
index = index + 1
|
|
|
|
for i = 1, #package.dependents do
|
|
local dependent = package.dependents[i]
|
|
|
|
if not indexes[dependent.id] then
|
|
local cycle = connect(dependent)
|
|
if cycle then
|
|
return cycle
|
|
else
|
|
lowlink[package.id] = math.min(lowlink[package.id], lowlink[dependent.id])
|
|
end
|
|
elseif stack[dependent.id] then
|
|
lowlink[package.id] = math.min(lowlink[package.id], indexes[dependent.id])
|
|
end
|
|
end
|
|
|
|
if lowlink[package.id] == indexes[package.id] then
|
|
local cycle = { package }
|
|
local node
|
|
|
|
repeat
|
|
node = stack[#stack]
|
|
stack[#stack], stack[node.id] = nil, nil
|
|
cycle[#cycle + 1] = node
|
|
until node == package
|
|
|
|
-- a node is by definition strongly connected to itself ignore single-node
|
|
-- components unless it explicitly specified itself as a dependency
|
|
if #cycle > 2 or package.dependents[package.id] then
|
|
return cycle
|
|
end
|
|
end
|
|
end
|
|
|
|
for _, package in pairs(packager.get_packages()) do
|
|
if not indexes[package.id] then
|
|
local cycle = connect(package)
|
|
if cycle then
|
|
return cycle
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
--- sync a tree of plugins
|
|
---@param tree package[] tree of plugins
|
|
---@param cb function? callback
|
|
function M.synctree(tree, cb)
|
|
local progress = 0
|
|
local has_errors = false
|
|
|
|
local function done(err)
|
|
progress = progress + 1
|
|
has_errors = has_errors or err
|
|
|
|
if progress == #tree then
|
|
-- TODO: implement clean
|
|
-- clean()
|
|
M.reload()
|
|
|
|
if has_errors then
|
|
logger:log("error", "there were errors during sync; see :messages or :DepLog for more information")
|
|
else
|
|
logger:log("update", "synchronized %s %s", #tree, #tree == 1 and "package" or "packages")
|
|
end
|
|
|
|
if cb then
|
|
cb()
|
|
end
|
|
end
|
|
end
|
|
|
|
for _, package in pairs(tree) do
|
|
git.sync(package, done)
|
|
end
|
|
end
|
|
|
|
-- basically the main function of our program
|
|
return function(opts)
|
|
logger.pipe = logger:setup()
|
|
|
|
--- make comparison for table.sort
|
|
---@param a table package spec a
|
|
---@param b table package spec b
|
|
---@return boolean
|
|
local function comp(a, b)
|
|
-- NOTE: this doesn't have to be in any real order, it just has to be
|
|
-- consistant, thus we can just check if the unicode value of one package
|
|
-- id is less than the other
|
|
return a.id < b.id
|
|
end
|
|
|
|
initialized, err = pcall(function()
|
|
packager.set_base_dir(opts.base_dir or vim.fn.stdpath("data").."/site/pack/deps/opt/")
|
|
M.benchmark("load", function()
|
|
-- register all packages
|
|
local root = packager:new({
|
|
"squibid/dep",
|
|
url = "https://git.squi.bid/dep",
|
|
pin = true
|
|
})
|
|
if not root then
|
|
logger:log("error", "couldn't register root package")
|
|
return
|
|
end
|
|
M.registertree(opts)
|
|
|
|
-- sort package dependencies
|
|
for _, package in pairs(packager.get_packages()) do
|
|
table.sort(package.requirements, comp)
|
|
table.sort(package.dependents, comp)
|
|
end
|
|
|
|
-- make sure there arent any circular dependencies
|
|
M.findcycle()
|
|
end)
|
|
|
|
-- load packages
|
|
M.reload()
|
|
|
|
--- check if a package should be synced
|
|
---@param package table package table spec
|
|
---@return boolean sync
|
|
local function shouldsync(package)
|
|
if opts.sync == "new" or opts.sync == nil then
|
|
return not package.exists
|
|
else
|
|
return opts.sync == "always"
|
|
end
|
|
end
|
|
|
|
-- get all package that need syncing
|
|
local targets = {}
|
|
for i, package in pairs(packager.get_packages()) do
|
|
if shouldsync(package) then
|
|
targets[i] = package
|
|
end
|
|
end
|
|
|
|
M.synctree(targets)
|
|
end)
|
|
|
|
if not initialized then
|
|
logger:log("error", err)
|
|
end
|
|
|
|
vim.api.nvim_create_user_command("DepLog", function()
|
|
vim.cmd('vsp '..logger.path)
|
|
vim.opt_local.readonly = true
|
|
|
|
local w = vim.uv.new_fs_event()
|
|
local function watch_file(fname)
|
|
local fullpath = vim.api.nvim_call_function(
|
|
'fnamemodify', { fname, ':p' })
|
|
w:start(fullpath, {}, vim.schedule_wrap(function(...)
|
|
vim.api.nvim_command('checktime')
|
|
-- Debounce: stop/start.
|
|
w:stop()
|
|
watch_file(fname)
|
|
end))
|
|
end
|
|
|
|
watch_file(logger.path)
|
|
end, {})
|
|
|
|
vim.api.nvim_create_user_command("DepSync", function()
|
|
M.synctree(packager.get_packages())
|
|
end, {})
|
|
|
|
vim.api.nvim_create_user_command("DepReload", function()
|
|
M.reload()
|
|
end, {})
|
|
|
|
logger:cleanup()
|
|
end
|