diff --git a/lua/lazy/manage/init.lua b/lua/lazy/manage/init.lua index e10693b..76f070c 100644 --- a/lua/lazy/manage/init.lua +++ b/lua/lazy/manage/init.lua @@ -1,109 +1,86 @@ local Config = require("lazy.core.config") -local Task = require("lazy.manage.task") local Runner = require("lazy.manage.runner") local Plugin = require("lazy.core.plugin") local M = {} ----@alias ManagerOpts {wait?: boolean, plugins?: LazyPlugin[], clear?: boolean, show?: boolean} +---@class ManagerOpts +---@field wait? boolean +---@field clear? boolean +---@field interactive? boolean ----@param operation TaskType +---@param ropts RunnerOpts ---@param opts? ManagerOpts ----@param filter? fun(plugin:LazyPlugin):boolean? -function M.run(operation, opts, filter) +function M.run(ropts, opts) opts = opts or {} - local plugins = opts.plugins or Config.plugins + if opts.interactive == nil then + opts.interactive = Config.options.interactive + end + if ropts.interactive == nil then + ropts.interactive = opts.interactive + end if opts.clear then M.clear() end - if opts.show then + if opts.interactive then require("lazy.view").show() end ---@type Runner - local runner = Runner.new() - - -- install missing plugins - for _, plugin in pairs(plugins) do - if filter == nil or filter(plugin) then - runner:add(Task.new(plugin, operation)) - end - end + local runner = Runner.new(ropts) + runner:start() vim.cmd([[do User LazyRender]]) - -- wait for install to finish + -- wait for post-install to finish runner:wait(function() - -- check if we need to do any post-install hooks - for _, plugin in ipairs(runner:plugins()) do - if plugin.dirty then - runner:add(Task.new(plugin, "docs")) - if plugin.opt == false or plugin.run then - runner:add(Task.new(plugin, "run")) - end - end - plugin.dirty = false - if opts.show and operation == "update" and plugin.updated and plugin.updated.from ~= plugin.updated.to then - runner:add(Task.new(plugin, "log", { - log = { - from = plugin.updated.from, - to = plugin.updated.to, - }, - })) - end - end - -- wait for post-install to finish - runner:wait(function() - vim.cmd([[do User LazyRender]]) - end) + vim.cmd([[do User LazyRender]]) end) if opts.wait then runner:wait() end - return runner end ---@param opts? ManagerOpts function M.install(opts) - ---@param plugin LazyPlugin - M.run("install", opts, function(plugin) - return plugin.uri and not plugin.installed - end) + M.run({ + pipeline = { "git.install", { "plugin.docs", "plugin.run" } }, + plugins = function(plugin) + return plugin.uri and not plugin.installed + end, + }, opts) end ---@param opts? ManagerOpts function M.update(opts) - ---@param plugin LazyPlugin - M.run("update", opts, function(plugin) - return plugin.uri and plugin.installed - end) + M.run({ + pipeline = { "git.update", { "plugin.docs", "plugin.run" }, "git.log" }, + plugins = function(plugin) + return plugin.uri and plugin.installed + end, + }, opts) end ---@param opts? ManagerOpts function M.log(opts) - ---@param plugin LazyPlugin - M.run("log", opts, function(plugin) - return plugin.uri and plugin.installed - end) -end - ----@param opts? ManagerOpts -function M.docs(opts) - ---@param plugin LazyPlugin - M.run("docs", opts, function(plugin) - return plugin.installed - end) + M.run({ + pipeline = { "git.log" }, + plugins = function(plugin) + return plugin.uri and plugin.installed + end, + }, opts) end ---@param opts? ManagerOpts function M.clean(opts) - opts = opts or {} Plugin.update_state(true) - opts.plugins = vim.tbl_values(Config.to_clean) - M.run("clean", opts) + M.run({ + pipeline = { "plugin.clean" }, + plugins = Config.to_clean, + }, opts) end function M.clear() @@ -114,7 +91,7 @@ function M.clear() if plugin.tasks then ---@param task LazyTask plugin.tasks = vim.tbl_filter(function(task) - return task.running + return task:is_running() end, plugin.tasks) end end diff --git a/lua/lazy/manage/runner.lua b/lua/lazy/manage/runner.lua index 75e402d..d70e094 100644 --- a/lua/lazy/manage/runner.lua +++ b/lua/lazy/manage/runner.lua @@ -1,24 +1,124 @@ +local Task = require("lazy.manage.task") +local Config = require("lazy.core.config") + +---@alias LazyPipeline (TaskType|TaskType[])[] + +---@class RunnerOpts +---@field pipeline LazyPipeline +---@field interactive? boolean +---@field plugins? LazyPlugin[]|fun(plugin:LazyPlugin):any? + ---@class Runner ---@field _tasks LazyTask[] +---@field _plugins LazyPlugin[] +---@field _running boolean +---@field _on_done fun()[] +---@field _waiting fun()[] +---@field _opts RunnerOpts local Runner = {} -function Runner.new() - local self = setmetatable({}, { - __index = Runner, - }) +---@param opts RunnerOpts +function Runner.new(opts) + local self = setmetatable({}, { __index = Runner }) + self._opts = opts or {} self._tasks = {} + local plugins = self._opts.plugins + if type(plugins) == "function" then + self._plugins = vim.tbl_filter(plugins, Config.plugins) + else + self._plugins = plugins or Config.plugins + end + self._running = false + self._on_done = {} + self._waiting = {} return self end ----@param task LazyTask -function Runner:add(task) - table.insert(self._tasks, task) - task:start() +---@param plugin LazyPlugin +---@param pipeline LazyPipeline +function Runner:_run(plugin, pipeline) + if #pipeline == 0 then + return + end + local ops = table.remove(pipeline, 1) + if ops == "wait" then + return table.insert(self._waiting, function() + self:_run(plugin, pipeline) + end) + end + + ops = type(ops) == "string" and { ops } or ops + ---@cast ops TaskType[] + + ---@type LazyTask[] + local tasks = {} + + local function on_done() + for _, task in ipairs(tasks) do + if task.error or not task:is_done() then + return + end + end + self:_run(plugin, pipeline) + end + + for _, op in ipairs(ops) do + local task = self:queue(plugin, op, { on_done = on_done }) + if task then + table.insert(tasks, task) + end + end + + for _, task in ipairs(tasks) do + task:start() + end end -function Runner:is_empty() - return #self._tasks == 0 +---@param plugin LazyPlugin +---@param task_type TaskType +---@param opts? TaskOptions +---@return LazyTask? +function Runner:queue(plugin, task_type, opts) + local def = vim.split(task_type, ".", { plain = true }) + assert(#def == 2) + ---@type LazyTaskDef + local task_def = require("lazy.manage.task." .. def[1])[def[2]] + assert(task_def) + if not task_def.needed or task_def.needed(plugin, self._opts) then + local task = Task.new(plugin, def[2], task_def.run, opts) + table.insert(self._tasks, task) + return task + end +end + +function Runner:start() + for _, plugin in pairs(self._plugins) do + self:_run(plugin, vim.deepcopy(self._opts.pipeline)) + end + self._running = true + local check = vim.loop.new_check() + + check:start(function() + for _, task in ipairs(self._tasks) do + if task:is_running() then + return + end + end + if #self._waiting > 0 then + for _, cb in ipairs(self._waiting) do + cb() + end + self._waiting = {} + return + end + check:stop() + self._running = false + for _, cb in ipairs(self._on_done) do + vim.schedule(cb) + end + self._on_done = {} + end) end ---@return LazyPlugin[] @@ -33,33 +133,19 @@ function Runner:tasks() return self._tasks end +-- Execute the callback async when done. +-- When no callback is specified, this will wait sync ---@param cb? fun() function Runner:wait(cb) - if #self._tasks == 0 then + if #self._tasks == 0 or not self._running then return cb and cb() end - local done = false - local check = vim.loop.new_check() - - check:start(function() - for _, task in ipairs(self._tasks) do - if task.running then - return - end - end - - check:stop() - - done = true - - if cb then - vim.schedule(cb) - end - end) - - if not cb then - while not done do + if cb then + table.insert(self._on_done, cb) + else + -- sync wait + while self._running do vim.wait(100) end end diff --git a/lua/lazy/manage/task/git.lua b/lua/lazy/manage/task/git.lua new file mode 100644 index 0000000..78c1add --- /dev/null +++ b/lua/lazy/manage/task/git.lua @@ -0,0 +1,114 @@ +local Util = require("lazy.util") + +---@type table +local M = {} + +M.log = { + needed = function(plugin, opts) + if opts.interactive ~= true or not Util.file_exists(plugin.dir .. "/.git") then + return false + end + return plugin.updated == nil or plugin.updated.from ~= plugin.updated.to + end, + run = function(self) + local args = { + "log", + "--pretty=format:%h %s (%cr)", + "--abbrev-commit", + "--decorate", + "--date=short", + "--color=never", + } + + if self.plugin.updated then + table.insert(args, self.plugin.updated.from .. ".." .. (self.plugin.updated.to or "HEAD")) + else + table.insert(args, "--since=7 days ago") + end + + self:spawn("git", { + args = args, + cwd = self.plugin.dir, + }) + end, +} + +M.update = { + run = function(self) + if Util.file_exists(self.plugin.uri) then + if vim.loop.fs_realpath(self.plugin.uri) ~= vim.loop.fs_realpath(self.plugin.dir) then + vim.loop.fs_unlink(self.plugin.dir) + vim.loop.fs_symlink(self.plugin.uri, self.plugin.dir, { + dir = true, + }) + vim.opt.runtimepath:append(self.plugin.uri) + end + else + local args = { + "pull", + "--tags", + "--recurse-submodules", + "--update-shallow", + "--progress", + } + local git = assert(Util.git_info(self.plugin.dir)) + + self:spawn("git", { + args = args, + cwd = self.plugin.dir, + on_exit = function(ok) + if ok then + local git_new = assert(Util.git_info(self.plugin.dir)) + self.plugin.updated = { + from = git.hash, + to = git_new.hash, + } + self.plugin.dirty = not vim.deep_equal(git, git_new) + end + end, + }) + end + end, +} + +M.install = { + run = function(self) + if Util.file_exists(self.plugin.uri) then + vim.loop.fs_symlink(self.plugin.uri, self.plugin.dir, { + dir = true, + }) + vim.opt.runtimepath:append(self.plugin.uri) + else + local args = { + "clone", + self.plugin.uri, + -- "--depth=1", + "--filter=blob:none", + -- "--filter=tree:0", + "--recurse-submodules", + "--single-branch", + "--shallow-submodules", + "--progress", + } + + if self.plugin.branch then + vim.list_extend(args, { + "-b", + self.plugin.branch, + }) + end + + table.insert(args, self.plugin.dir) + self:spawn("git", { + args = args, + on_exit = function(ok) + if ok then + self.plugin.installed = true + self.plugin.dirty = true + end + end, + }) + end + end, +} +return M diff --git a/lua/lazy/manage/task/init.lua b/lua/lazy/manage/task/init.lua index 4270472..71dbbb3 100644 --- a/lua/lazy/manage/task/init.lua +++ b/lua/lazy/manage/task/init.lua @@ -1,36 +1,40 @@ local Process = require("lazy.manage.process") -local Loader = require("lazy.core.loader") -local Util = require("lazy.util") + +---@class LazyTaskDef +---@field needed? fun(plugin:LazyPlugin, opts:RunnerOpts):any? +---@field run fun(task:LazyTask) + +---@alias LazyTaskState fun():boolean? ---@class LazyTask ---@field plugin LazyPlugin ---@field type TaskType ----@field running boolean ----@field opts TaskOptions +---@field output string +---@field status string +---@field error? string +---@field private _task fun(task:LazyTask) +---@field private _running LazyPluginState[] +---@field private _started boolean +---@field private _opts TaskOptions local Task = {} ---@alias TaskType "update"|"install"|"run"|"clean"|"log"|"docs" ---@class TaskOptions ---@field on_done? fun(task:LazyTask) -local options = { - log = { - since = "7 days ago", - ---@type string - from = nil, - ---@type string - to = nil, - }, -} ---@param plugin LazyPlugin ---@param type TaskType ---@param opts? TaskOptions -function Task.new(plugin, type, opts) +---@param task fun(task:LazyTask) +function Task.new(plugin, type, task, opts) local self = setmetatable({}, { __index = Task, }) - self.opts = vim.tbl_deep_extend("force", {}, options, opts or {}) + self._opts = opts or {} + self._running = {} + self._task = task + self._started = false self.plugin = plugin self.type = type self.output = "" @@ -40,10 +44,40 @@ function Task.new(plugin, type, opts) return self end -function Task:_done() - self.running = false - if self.opts.on_done then - self.opts.on_done(self) +function Task:has_started() + return self._started +end + +function Task:is_done() + return self:has_started() and not self:is_running() +end + +function Task:is_running() + for _, state in ipairs(self._running) do + if state() then + return true + end + end + return false +end + +function Task:start() + self._started = true + ---@type boolean, string|any + local ok, err = pcall(self._task, self) + if not ok then + self.error = err or "failed" + end + self:_check() +end + +---@private +function Task:_check() + if self:is_running() then + return + end + if self._opts.on_done then + self._opts.on_done(self) end vim.cmd("do User LazyRender") vim.api.nvim_exec_autocmds("User", { @@ -52,99 +86,25 @@ function Task:_done() }) end -function Task:clean() - local dir = self.plugin.dir:gsub("/+$", "") - local stat = vim.loop.fs_lstat(dir) - - if stat.type == "directory" then - Util.walk(dir, function(path, _, type) - if type == "directory" then - vim.loop.fs_rmdir(path) - else - vim.loop.fs_unlink(path) - end - end) - vim.loop.fs_rmdir(dir) - else - vim.loop.fs_unlink(dir) - end - - self.plugin.installed = false - self:_done() -end - -function Task:install() - if Util.file_exists(self.plugin.uri) then - vim.loop.fs_symlink(self.plugin.uri, self.plugin.dir, { - dir = true, - }) - vim.opt.runtimepath:append(self.plugin.uri) - self:_done() - else - local args = { - "clone", - self.plugin.uri, - -- "--depth=1", - "--filter=blob:none", - -- "--filter=tree:0", - "--recurse-submodules", - "--single-branch", - "--shallow-submodules", - "--progress", - } - - if self.plugin.branch then - vim.list_extend(args, { - "-b", - self.plugin.branch, - }) +---@param fn fun() +function Task:schedule(fn) + local done = false + table.insert(self._running, function() + return not done + end) + vim.schedule(function() + ---@type boolean, string|any + local ok, err = pcall(fn) + if not ok then + self.error = err or "failed" end - - table.insert(args, self.plugin.dir) - self:spawn("git", { - args = args, - on_exit = function(ok) - if ok then - self.plugin.installed = true - self.plugin.dirty = true - end - end, - }) - end -end - -function Task:run() - Loader.load(self.plugin, { task = "run" }, { load_start = true }) - - local run = self.plugin.run - if run then - if type(run) == "string" and run:sub(1, 1) == ":" then - local cmd = vim.api.nvim_parse_cmd(run:sub(2), {}) - self.output = vim.api.nvim_cmd(cmd, { output = true }) - elseif type(run) == "function" then - run() - else - local args = vim.split(run, "%s+") - return self:spawn(table.remove(args, 1), { - args = args, - cwd = self.plugin.dir, - }) - end - end - -- FIXME: the spawn above wont be finished yet - self:_done() -end - -function Task:docs() - local docs = self.plugin.dir .. "/doc/" - if Util.file_exists(docs) then - self.output = vim.api.nvim_cmd({ cmd = "helptags", args = { docs } }, { output = true }) - end - self:_done() + done = true + self:_check() + end) end ---@param cmd string ----@param opts ProcessOpts +---@param opts? ProcessOpts function Task:spawn(cmd, opts) opts = opts or {} local on_line = opts.on_line @@ -152,116 +112,42 @@ function Task:spawn(cmd, opts) function opts.on_line(line) self.status = line - if on_line then pcall(on_line, line) end - vim.cmd("do User LazyRender") end + ---@param output string function opts.on_exit(ok, output) - self.output = output - + self.output = self.output .. output if not ok then - self.error = output + self.error = self.error and (self.error .. "\n" .. output) or output end - if on_exit then pcall(on_exit, ok, output) end - - self:_done() + self:_check() end - - Process.spawn(cmd, opts) -end - -function Task:start() - self.running = true - local ok, err = pcall(function() - if self.type == "update" then - self:update() - elseif self.type == "install" then - self:install() - elseif self.type == "run" then - self:run() - elseif self.type == "clean" then - self:clean() - elseif self.type == "log" then - self:log() - elseif self.type == "docs" then - self:docs() - end + local proc = Process.spawn(cmd, opts) + table.insert(self._running, function() + return proc and not proc:is_closing() end) - - if not ok then - self.error = err or "failed" - self:_done() - end end -function Task:log() - if not Util.file_exists(self.plugin.dir .. "/.git") then - self:_done() - return - end - - local args = { - "log", - "--pretty=format:%h %s (%cr)", - "--abbrev-commit", - "--decorate", - "--date=short", - "--color=never", - } - - if self.opts.log.from then - table.insert(args, self.opts.log.from .. ".." .. (self.opts.log.to or "HEAD")) - else - table.insert(args, "--since=" .. self.opts.log.since) - end - - self:spawn("git", { - args = args, - cwd = self.plugin.dir, - }) -end - -function Task:update() - if Util.file_exists(self.plugin.uri) then - if vim.loop.fs_realpath(self.plugin.uri) ~= vim.loop.fs_realpath(self.plugin.dir) then - vim.loop.fs_unlink(self.plugin.dir) - vim.loop.fs_symlink(self.plugin.uri, self.plugin.dir, { - dir = true, - }) - vim.opt.runtimepath:append(self.plugin.uri) +---@param tasks (LazyTask?)[] +function Task.all_done(tasks) + for _, task in ipairs(tasks) do + if task and not task:is_done() then + return false end - self:_done() - else - local args = { - "pull", - "--tags", - "--recurse-submodules", - "--update-shallow", - "--progress", - } - local git = assert(Util.git_info(self.plugin.dir)) + end + return true +end - self:spawn("git", { - args = args, - cwd = self.plugin.dir, - on_exit = function(ok) - if ok then - local git_new = assert(Util.git_info(self.plugin.dir)) - self.plugin.updated = { - from = git.hash, - to = git_new.hash, - } - self.plugin.dirty = not vim.deep_equal(git, git_new) - end - end, - }) +function Task:wait() + while self:is_running() do + vim.wait(10) end end diff --git a/lua/lazy/manage/task/plugin.lua b/lua/lazy/manage/task/plugin.lua new file mode 100644 index 0000000..3d29275 --- /dev/null +++ b/lua/lazy/manage/task/plugin.lua @@ -0,0 +1,66 @@ +local Util = require("lazy.util") +local Loader = require("lazy.core.loader") + +---@type table +local M = {} + +M.run = { + needed = function(plugin) + return plugin.dirty and (plugin.opt == false or plugin.run) + end, + run = function(self) + Loader.load(self.plugin, { task = "run" }, { load_start = true }) + + local run = self.plugin.run + if run then + if type(run) == "string" and run:sub(1, 1) == ":" then + local cmd = vim.api.nvim_parse_cmd(run:sub(2), {}) + self.output = vim.api.nvim_cmd(cmd, { output = true }) + elseif type(run) == "function" then + run() + else + local args = vim.split(run, "%s+") + return self:spawn(table.remove(args, 1), { + args = args, + cwd = self.plugin.dir, + }) + end + end + end, +} + +M.clean = { + run = function(self) + local dir = self.plugin.dir:gsub("/+$", "") + local stat = vim.loop.fs_lstat(dir) + + if stat.type == "directory" then + Util.walk(dir, function(path, _, type) + if type == "directory" then + vim.loop.fs_rmdir(path) + else + vim.loop.fs_unlink(path) + end + end) + vim.loop.fs_rmdir(dir) + else + vim.loop.fs_unlink(dir) + end + + self.plugin.installed = false + end, +} + +M.docs = { + needed = function(plugin) + return plugin.dirty + end, + run = function(self) + local docs = self.plugin.dir .. "/doc/" + if Util.file_exists(docs) then + self.output = vim.api.nvim_cmd({ cmd = "helptags", args = { docs } }, { output = true }) + end + end, +} + +return M diff --git a/lua/lazy/view/render.lua b/lua/lazy/view/render.lua index 79ddbc9..7fc8075 100644 --- a/lua/lazy/view/render.lua +++ b/lua/lazy/view/render.lua @@ -50,7 +50,7 @@ function M:update() if plugin.tasks then for _, task in ipairs(plugin.tasks) do self.progress.total = self.progress.total + 1 - if not task.running then + if not task:is_running() then self.progress.done = self.progress.done + 1 end end @@ -215,7 +215,7 @@ function M:diagnostics(plugin) end end for _, task in ipairs(plugin.tasks or {}) do - if task.running then + if task:is_running() then self:diagnostic({ severity = vim.diagnostic.severity.WARN, message = task.type .. (task.status == "" and "" or (": " .. task.status)), diff --git a/lua/lazy/view/sections.lua b/lua/lazy/view/sections.lua index 0b6035c..22cd545 100644 --- a/lua/lazy/view/sections.lua +++ b/lua/lazy/view/sections.lua @@ -25,7 +25,7 @@ return { { filter = function(plugin) return has_task(plugin, function(task) - return task.running + return task:is_running() end) end, title = "Working",