diff --git a/lua/lazy/core/util.lua b/lua/lazy/core/util.lua index 54b7148..1c0facb 100644 --- a/lua/lazy/core/util.lua +++ b/lua/lazy/core/util.lua @@ -29,10 +29,13 @@ function M.track(data, time) end ---@param name string +---@return string function M.normname(name) - return name:lower():gsub("^n?vim%-", ""):gsub("%.n?vim$", ""):gsub("%.lua", ""):gsub("[^a-z]+", "") + local ret = name:lower():gsub("^n?vim%-", ""):gsub("%.n?vim$", ""):gsub("%.lua", ""):gsub("[^a-z]+", "") + return ret end +---@return string function M.norm(path) if path:sub(1, 1) == "~" then local home = vim.loop.os_homedir() diff --git a/lua/lazy/util.lua b/lua/lazy/util.lua index a16feb5..772a8be 100644 --- a/lua/lazy/util.lua +++ b/lua/lazy/util.lua @@ -45,23 +45,29 @@ function M.write_file(file, contents) fd:close() end +---@generic F: fun() ---@param ms number ----@param fn fun() +---@param fn F +---@return F function M.throttle(ms, fn) local timer = vim.loop.new_timer() local running = false local first = true - return function() + return function(...) + local args = { ... } + local wrapped = function() + fn(unpack(args)) + end if not running then if first then - fn() + wrapped() first = false end timer:start(ms, 0, function() running = false - vim.schedule(fn) + vim.schedule(wrapped) end) running = true diff --git a/lua/lazy/view/init.lua b/lua/lazy/view/init.lua index e82f644..3d40df9 100644 --- a/lua/lazy/view/init.lua +++ b/lua/lazy/view/init.lua @@ -2,6 +2,16 @@ local Util = require("lazy.util") local Render = require("lazy.view.render") local Config = require("lazy.core.config") +---@class LazyViewState +---@field mode string +---@field plugin? string + +---@class LazyView +---@field buf number +---@field win number +---@field render LazyRender +---@field state LazyViewState +---@field win_opts LazyViewWinOpts local M = {} M.modes = { @@ -40,29 +50,159 @@ M.modes = { M.hover = "K" ----@type string? -M.mode = nil +---@type LazyView +M.view = nil +---@param mode? string function M.show(mode) if Config.headless then return end - M.mode = mode or M.mode or "home" + + M.view = M.view or M.create({ mode = mode }) + M.view:update(mode) +end + +---@param opts? {mode?:string} +function M.create(opts) require("lazy.view.colors").setup() + opts = opts or {} + local self = setmetatable({}, { __index = M }) - if M._buf and vim.api.nvim_buf_is_valid(M._buf) then - -- vim.api.nvim_win_set_cursor(M._win, { 1, 0 }) - vim.cmd([[do User LazyRender]]) - return + self.state = { mode = "home" } + + self:mount() + + self.render = Render.new(self) + self.update = Util.throttle(Config.options.ui.throttle, self.update) + + self:on_key("q", self.close) + + self:on({ "BufDelete", "BufLeave", "BufHidden" }, self.close, { once = true }) + + self:on("User LazyRender", function() + if not (self.buf and vim.api.nvim_buf_is_valid(self.buf)) then + return true + end + self:update() + end) + + -- plugin details + self:on_key("", function() + local plugin = self.render:get_plugin() + if plugin then + self.state.plugin = self.state.plugin ~= plugin.name and plugin.name or nil + self:update() + end + end) + + self:setup_hover() + self:setup_modes() + return self +end + +---@param events string|string[] +---@param fn fun(self?):boolean? +---@param opts? table +function M:on(events, fn, opts) + if type(events) == "string" then + events = { events } end + for _, e in ipairs(events) do + local event, pattern = e:match("(%w+) (%w+)") + event = event or e + vim.api.nvim_create_autocmd( + event, + vim.tbl_extend("force", { + pattern = pattern, + buffer = not pattern and self.buf or nil, + callback = function() + return fn(self) + end, + }, opts or {}) + ) + end +end - local buf = vim.api.nvim_create_buf(false, false) - M._buf = buf +---@param key string +---@param fn fun(self?) +function M:on_key(key, fn) + vim.keymap.set("n", key, function() + fn(self) + end, { + nowait = true, + buffer = self.buf, + }) +end + +---@param mode? string +function M:update(mode) + if mode then + self.state.mode = mode + end + if self.buf and vim.api.nvim_buf_is_valid(self.buf) then + vim.bo[self.buf].modifiable = true + self.render:update() + vim.bo[self.buf].modifiable = false + vim.cmd.redraw() + end +end + +function M:open_url(path) + local plugin = self.render:get_plugin() + if plugin then + if plugin.url then + local url = plugin.url:gsub("%.git$", "") + Util.open(url .. path) + else + Util.error("No url for " .. plugin.name) + end + end +end + +function M:close() + local buf = self.buf + local win = self.win + self.win = nil + self.buf = nil + M.view = nil + vim.diagnostic.reset(Config.ns, buf) + vim.schedule(function() + if win and vim.api.nvim_win_is_valid(win) then + vim.api.nvim_win_close(win, true) + end + if buf and vim.api.nvim_buf_is_valid(buf) then + vim.api.nvim_buf_delete(buf, { force = true }) + end + end) +end + +function M:focus() + vim.api.nvim_set_current_win(self.win) + + -- it seems that setting the current win doesn't work before VimEnter, + -- so do that then + if vim.v.vim_did_enter ~= 1 then + vim.api.nvim_create_autocmd("VimEnter", { + once = true, + callback = function() + if self.win and vim.api.nvim_win_is_valid(self.win) then + pcall(vim.api.nvim_set_current_win, self.win) + end + return true + end, + }) + end +end + +function M:mount() + self.buf = vim.api.nvim_create_buf(false, false) local function size(max, value) return value > 1 and math.min(value, max) or math.floor(max * value) end - local opts = { + ---@class LazyViewWinOpts + self.win_opts = { relative = "editor", style = "minimal", border = Config.options.ui.border, @@ -71,178 +211,87 @@ function M.show(mode) noautocmd = true, } - opts.row = (vim.o.lines - opts.height) / 2 - opts.col = (vim.o.columns - opts.width) / 2 - local win = vim.api.nvim_open_win(buf, true, opts) - M._win = win - vim.api.nvim_set_current_win(win) + self.win_opts.row = (vim.o.lines - self.win_opts.height) / 2 + self.win_opts.col = (vim.o.columns - self.win_opts.width) / 2 + self.win = vim.api.nvim_open_win(self.buf, true, self.win_opts) + self:focus() - -- it seems that setting the current win doesn't work before VimEnter, - -- so do that then - if vim.v.vim_did_enter ~= 1 then - vim.api.nvim_create_autocmd("VimEnter", { - once = true, - callback = function() - if win and vim.api.nvim_win_is_valid(win) then - pcall(vim.api.nvim_set_current_win, win) - end - end, - }) - end + vim.bo[self.buf].buftype = "nofile" + vim.bo[self.buf].filetype = "lazy" + vim.bo[self.buf].bufhidden = "wipe" + vim.wo[self.win].conceallevel = 3 + vim.wo[self.win].spell = false + vim.wo[self.win].wrap = true + vim.wo[self.win].winhighlight = "Normal:LazyNormal" +end - vim.bo[buf].buftype = "nofile" - vim.bo[buf].filetype = "lazy" - vim.bo[buf].bufhidden = "wipe" - vim.wo[win].conceallevel = 3 - vim.wo[win].spell = false - vim.wo[win].wrap = true - vim.wo[win].winhighlight = "Normal:LazyNormal" - - local function close() - M._buf = nil - vim.diagnostic.reset(Config.ns, buf) - vim.schedule(function() - if vim.api.nvim_win_is_valid(win) then - vim.api.nvim_win_close(win, true) - end - if vim.api.nvim_buf_is_valid(buf) then - vim.api.nvim_buf_delete(buf, { force = true }) - end - end) - end - - vim.keymap.set("n", "q", close, { - nowait = true, - buffer = buf, - }) - - vim.api.nvim_create_autocmd({ "BufDelete", "BufLeave", "BufHidden" }, { - once = true, - buffer = buf, - callback = close, - }) - - local render = Render.new(buf, win, 2, opts.width) - local update = Util.throttle(Config.options.ui.throttle, function() - if buf and vim.api.nvim_buf_is_valid(buf) then - vim.bo[buf].modifiable = true - render:update() - vim.bo[buf].modifiable = false - vim.cmd.redraw() - end - end) - - local function get_plugin() - local pos = vim.api.nvim_win_get_cursor(win) - return render:get_plugin(pos[1]) - end - - vim.keymap.set("n", "", function() - local plugin = get_plugin() - if plugin then - if render._details == plugin.name then - render._details = nil - else - render._details = plugin.name - end - update() - end - end, { - nowait = true, - buffer = buf, - }) - - local function open(path) - local plugin = get_plugin() - if plugin then - local url = plugin.url:gsub("%.git$", "") - if Util.file_exists(url) then - url = "https://github.com/" .. plugin[1] - end - Util.open(url .. path) - end - end - - M.keys(buf, { +function M:setup_hover() + local handlers = { ["%s(" .. string.rep("[a-z0-9]", 7) .. ")%s"] = function(hash) - open("/commit/" .. hash) + self:open_url("/commit/" .. hash) end, ["%s(" .. string.rep("[a-z0-9]", 7) .. ")$"] = function(hash) - open("/commit/" .. hash) + self:open_url("/commit/" .. hash) end, ["^(" .. string.rep("[a-z0-9]", 7) .. ")%s"] = function(hash) - open("/commit/" .. hash) + self:open_url("/commit/" .. hash) end, ["#(%d+)"] = function(issue) - open("/issues/" .. issue) + self:open_url("/issues/" .. issue) end, ["README.md"] = function() - local plugin = get_plugin() - Util.open(plugin.dir .. "/README.md") + local plugin = self.render:get_plugin() + if plugin then + Util.open(plugin.dir .. "/README.md") + end end, ["|(%S-)|"] = vim.cmd.help, -- vim help links ["(https?://%S+)"] = function(url) Util.open(url) end, - }) + } + self:on_key(M.hover, function() + local line = vim.api.nvim_get_current_line() + local pos = vim.api.nvim_win_get_cursor(0) + local col = pos[2] + 1 + + for pattern, handler in pairs(handlers) do + local from = 1 + local to, url + while from do + from, to, url = line:find(pattern, from) + if from and col >= from and col <= to then + return handler(url) + end + if from then + from = to + 1 + end + end + end + end) +end + +function M:setup_modes() for _, m in ipairs(M.modes) do if m.key then - vim.keymap.set("n", m.key, function() + self:on_key(m.key, function() local Commands = require("lazy.view.commands") if m.plugin then - local plugin = get_plugin() + local plugin = self.render:get_plugin() if plugin then Commands.cmd(m.name, { plugins = { plugin } }) end else - if M.mode == m.name and m.toggle then - M.mode = nil - return update() + if self.state.mode == m.name and m.toggle then + self.state.mode = "home" + return self:update() end Commands.cmd(m.name) end - end, { buffer = buf }) + end) end end - - vim.api.nvim_create_autocmd("User", { - pattern = "LazyRender", - callback = function() - if not vim.api.nvim_buf_is_valid(buf) then - return true - end - update() - end, - }) - update() -end - ----@param handlers table -function M.keys(buf, handlers) - local function map(lhs) - vim.keymap.set("n", lhs, function() - local line = vim.api.nvim_get_current_line() - local pos = vim.api.nvim_win_get_cursor(0) - local col = pos[2] + 1 - - for pattern, handler in pairs(handlers) do - local from = 1 - local to, url - while from do - from, to, url = line:find(pattern, from) - if from and col >= from and col <= to then - return handler(url) - end - if from then - from = to + 1 - end - end - end - end, { buffer = buf, silent = true }) - end - - map(M.hover) end return M diff --git a/lua/lazy/view/render.lua b/lua/lazy/view/render.lua index 29003ed..ca69c42 100644 --- a/lua/lazy/view/render.lua +++ b/lua/lazy/view/render.lua @@ -9,24 +9,22 @@ local Text = require("lazy.view.text") ---@alias LazyDiagnostic {row: number, severity: number, message:string} ----@class Render:Text ----@field buf buffer ----@field win window +---@class LazyRender:Text +---@field view LazyView ---@field plugins LazyPlugin[] ---@field progress {total:number, done:number} ---@field _diagnostics LazyDiagnostic[] ---@field plugin_range table ----@field _details? string local M = {} ----@return Render -function M.new(buf, win, padding, wrap) - ---@type Render +---@return LazyRender +---@param view LazyView +function M.new(view) + ---@type LazyRender local self = setmetatable({}, { __index = setmetatable(M, { __index = Text }) }) - self.buf = buf - self.win = win - self.padding = padding or 0 - self.wrap = wrap + self.view = view + self.padding = 2 + self.wrap = view.win_opts.width return self end @@ -72,10 +70,10 @@ function M:update() end self:trim() - self:render(self.buf) + self:render(self.view.buf) vim.diagnostic.set( Config.ns, - self.buf, + self.view.buf, ---@param diag LazyDiagnostic vim.tbl_map(function(diag) diag.col = 0 @@ -86,9 +84,10 @@ function M:update() ) end ----@param row number +---@param row? number ---@return LazyPlugin? function M:get_plugin(row) + row = row or vim.api.nvim_win_get_cursor(self.view.win)[1] for name, range in pairs(self.plugin_range) do if row >= range.from and row <= range.to then return Config.plugins[name] @@ -98,20 +97,18 @@ end function M:title() self:nl():nl() - local View = require("lazy.view") - - for _, mode in ipairs(View.modes) do + for _, mode in ipairs(self.view.modes) do if not mode.hide and not mode.plugin then local title = " " .. mode.name:sub(1, 1):upper() .. mode.name:sub(2) .. " (" .. mode.key .. ") " if mode.name == "home" then - if View.mode == "home" then + if self.view.state.mode == "home" then title = " lazy.nvim 鈴 " else title = " lazy.nvim (H) " end end - if View.mode == mode.name then + if self.view.state.mode == mode.name then if mode.name == "home" then self:append(title, "LazyH1") else @@ -131,7 +128,7 @@ function M:title() end self:nl() - if View.mode ~= "help" and View.mode ~= "profile" and View.mode ~= "debug" then + if self.view.state.mode ~= "help" and self.view.state.mode ~= "profile" and self.view.state.mode ~= "debug" then if self.progress.done < self.progress.total then self:append("Tasks: ", "LazyH2") self:append(self.progress.done .. "/" .. self.progress.total, "LazyMuted") @@ -141,11 +138,10 @@ function M:title() end self:nl():nl() end - return View.mode + return self.view.state.mode end function M:help() - local View = require("lazy.view") self:append("Help", "LazyH2"):nl():nl() self:append("You can press "):append("", "LazySpecial"):append(" on a plugin to show its details."):nl() @@ -155,7 +151,7 @@ function M:help() self:append(" to open links, help files, readmes and git commits."):nl():nl() self:append("Keyboard Shortcuts", "LazyH2"):nl() - for _, mode in ipairs(View.modes) do + for _, mode in ipairs(self.view.modes) do local title = mode.name:sub(1, 1):upper() .. mode.name:sub(2) self:append("- ", "LazySpecial", { indent = 2 }) self:append(title, "Title") @@ -167,7 +163,7 @@ function M:help() end function M:progressbar() - local width = vim.api.nvim_win_get_width(self.win) - 2 * self.padding + local width = vim.api.nvim_win_get_width(self.view.win) - 2 * self.padding local done = math.floor((self.progress.done / self.progress.total) * width + 0.5) if self.progress.done == self.progress.total then done = 0 @@ -341,7 +337,7 @@ function M:plugin(plugin) self:diagnostics(plugin) self:nl() - if self._details == plugin.name then + if self.view.state.plugin == plugin.name then self:details(plugin) end self:tasks(plugin) @@ -351,14 +347,14 @@ end ---@param plugin LazyPlugin function M:tasks(plugin) for _, task in ipairs(plugin._.tasks or {}) do - if self._details == plugin.name then + if self.view.state.plugin == plugin.name then self:append("✔ [task] ", "Title", { indent = 4 }):append(task.name) self:append(" " .. math.floor((task:time()) * 100) / 100 .. "ms", "Bold") self:nl() end if task.name == "log" and not task.error then self:log(task) - elseif task.error or self._details == plugin.name then + elseif task.error or self.view.state.plugin == plugin.name then if task.error then self:append(vim.trim(task.error), "LazyError", { indent = 4, prefix = "│ " }) self:nl()