diff --git a/lua/lazy/process.lua b/lua/lazy/process.lua new file mode 100644 index 0000000..4227e9c --- /dev/null +++ b/lua/lazy/process.lua @@ -0,0 +1,90 @@ +local M = {} + +---@alias ProcessOpts {args: string[], cwd?: string, on_line?:fun(string), on_exit?: fun(ok:boolean, output:string)} + +function M.spawn(cmd, opts) + opts = opts or {} + local env = { + "GIT_TERMINAL_PROMPT=0", + "GIT_SSH_COMMAND=ssh -oBatchMode=yes", + } + + for key, value in + pairs(vim.loop.os_environ() --[[@as string[] ]]) + do + table.insert(env, key .. "=" .. value) + end + + local stdout = vim.loop.new_pipe() + local stderr = vim.loop.new_pipe() + + local output = "" + ---@type vim.loop.Process + local handle = nil + + handle = vim.loop.spawn(cmd, { + stdio = { nil, stdout, stderr }, + args = opts.args, + cwd = opts.cwd, + env = env, + }, function(exit_code) + handle:close() + stdout:close() + stderr:close() + local check = vim.loop.new_check() + check:start(function() + if not stdout:is_closing() or not stderr:is_closing() then + return + end + check:stop() + if opts.on_exit then + output = output:gsub("[^\r\n]+\r", "") + + vim.schedule(function() + opts.on_exit(exit_code == 0, output) + end) + end + end) + end) + + if not handle then + if opts.on_exit then + opts.on_exit(false, "Failed to spawn process " .. cmd .. " " .. vim.inspect(opts)) + end + + return + end + + local function on_output(err, data) + assert(not err, err) + + if data then + output = output .. data:gsub("\r\n", "\n") + local lines = vim.split(vim.trim(output:gsub("\r$", "")):gsub("[^\n\r]+\r", ""), "\n") + + if opts.on_line then + vim.schedule(function() + opts.on_line(lines[#lines]) + end) + end + end + end + + vim.loop.read_start(stdout, on_output) + vim.loop.read_start(stderr, on_output) + + return handle +end + +-- FIXME: can be removed? +function M.all_done(slot0) + for slot4, slot5 in ipairs(slot0) do + if slot5 and not slot5:is_closing() then + return false + end + end + + return true +end + +return M diff --git a/lua/lazy/runner.lua b/lua/lazy/runner.lua new file mode 100644 index 0000000..75e402d --- /dev/null +++ b/lua/lazy/runner.lua @@ -0,0 +1,68 @@ +---@class Runner +---@field _tasks LazyTask[] +local Runner = {} + +function Runner.new() + local self = setmetatable({}, { + __index = Runner, + }) + self._tasks = {} + + return self +end + +---@param task LazyTask +function Runner:add(task) + table.insert(self._tasks, task) + task:start() +end + +function Runner:is_empty() + return #self._tasks == 0 +end + +---@return LazyPlugin[] +function Runner:plugins() + ---@param task LazyTask + return vim.tbl_map(function(task) + return task.plugin + end, self._tasks) +end + +function Runner:tasks() + return self._tasks +end + +---@param cb? fun() +function Runner:wait(cb) + if #self._tasks == 0 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 + vim.wait(100) + end + end +end + +return Runner diff --git a/lua/lazy/task.lua b/lua/lazy/task.lua new file mode 100644 index 0000000..6cb0124 --- /dev/null +++ b/lua/lazy/task.lua @@ -0,0 +1,206 @@ +local Process = require("lazy.process") +local Loader = require("lazy.loader") + +---@class LazyTask +---@field plugin LazyPlugin +---@field type TaskType +---@field running boolean +local Task = {} + +---@alias TaskType "update"|"install"|"run"|"clean" + +---@param plugin LazyPlugin +---@param type TaskType +function Task.new(plugin, type) + local self = setmetatable({}, { + __index = Task, + }) + self.plugin = plugin + self.type = type + self.output = "" + self.status = "" + plugin.tasks = plugin.tasks or {} + table.insert(plugin.tasks, self) + return self +end + +function Task:_done() + self.running = false + + vim.cmd("do User LazyRender") +end + +function Task:clean() + local function rm(path) + for _, entry in ipairs(Util.scandir(path)) do + if entry.type == "directory" then + rm(entry.path) + else + vim.loop.fs_unlink(entry.path) + end + end + + vim.loop.fs_rmdir(path) + end + + local stat = vim.loop.fs_stat(self.plugin.dir) + + if stat.type == "directory" then + rm(self.plugin.dir) + else + vim.loop.fs_unlink(self.plugin.dir) + end + + self.plugin.installed = false + self.running = false +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", + "--recurse-submodules", + "--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 + +function Task:run() + Loader.load(self.plugin) + + local run = self.plugin.run + + if run then + if type(run) == "string" and run:sub(1, 1) == ":" then + vim.cmd(run:sub(2)) + 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 + + self:_done() +end + +---@param cmd string +---@param opts ProcessOpts +function Task:spawn(cmd, opts) + opts = opts or {} + local on_line = opts.on_line + local on_exit = opts.on_exit + + function opts.on_line(line) + self.status = line + + if on_line then + pcall(on_line, line) + end + + vim.cmd("do User LazyRender") + end + + function opts.on_exit(ok, output) + self.output = output + + if not ok then + self.error = output + end + + if on_exit then + pcall(on_exit, ok, output) + end + + self:_done() + 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() + end + end) + + if not ok then + self.error = err or "failed" + + self:_done() + end +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) + end + + self:_done() + else + local args = { + "pull", + "--recurse-submodules", + "--update-shallow", + "--progress", + } + local git = 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 = Util.git_info(self.plugin.dir) + self.plugin.dirty = not vim.deep_equal(git, git_new) + end + end, + }) + end +end + +return Task