diff --git a/lua/lazy/view/float.lua b/lua/lazy/view/float.lua new file mode 100644 index 0000000..0a92dbd --- /dev/null +++ b/lua/lazy/view/float.lua @@ -0,0 +1,191 @@ +local Config = require("lazy.core.config") +local ViewConfig = require("lazy.view.config") + +---@class LazyViewOptions +---@field buf? number +---@field file? string +---@field margin? {top?:number, right?:number, bottom?:number, left?:number} +---@field win_opts LazyViewWinOpts +local defaults = { + win_opts = {}, +} + +---@class LazyFloat +---@field buf number +---@field win number +---@field opts LazyViewOptions +---@overload fun(opts?:LazyViewOptions):LazyFloat +local M = {} + +setmetatable(M, { + __call = function(_, ...) + return M.new(...) + end, +}) + +---@param opts? LazyViewOptions +function M.new(opts) + local self = setmetatable({}, { __index = M }) + return self:init(opts) +end + +---@param opts? LazyViewOptions +function M:init(opts) + self.opts = vim.tbl_deep_extend("force", defaults, opts or {}) + self:mount() + self:on_key(ViewConfig.keys.close, self.close) + self:on({ "BufDelete", "BufLeave", "BufHidden" }, self.close, { once = true }) + return self +end + +function M:layout() + local function size(max, value) + return value > 1 and math.min(value, max) or math.floor(max * value) + end + self.opts.win_opts.width = size(vim.o.columns, Config.options.ui.size.width) + self.opts.win_opts.height = size(vim.o.lines, Config.options.ui.size.height) + self.opts.win_opts.row = math.floor((vim.o.lines - self.opts.win_opts.height) / 2) + self.opts.win_opts.col = math.floor((vim.o.columns - self.opts.win_opts.width) / 2) + + if self.opts.margin then + if self.opts.margin.top then + self.opts.win_opts.height = self.opts.win_opts.height - self.opts.margin.top + self.opts.win_opts.row = self.opts.win_opts.row + self.opts.margin.top + end + if self.opts.margin.right then + self.opts.win_opts.width = self.opts.win_opts.width - self.opts.margin.right + end + if self.opts.margin.bottom then + self.opts.win_opts.height = self.opts.win_opts.height - self.opts.margin.bottom + end + if self.opts.margin.left then + self.opts.win_opts.width = self.opts.win_opts.width - self.opts.margin.left + self.opts.win_opts.col = self.opts.win_opts.col + self.opts.margin.left + end + end +end + +function M:mount() + if self.opts.file then + self.buf = vim.fn.bufadd(self.opts.file) + vim.fn.bufload(self.buf) + vim.bo[self.buf].modifiable = false + elseif self.opts.buf then + self.buf = self.opts.buf + else + self.buf = vim.api.nvim_create_buf(false, false) + end + + ---@class LazyViewWinOpts + local win_opts = { + relative = "editor", + style = "minimal", + border = Config.options.ui.border, + noautocmd = true, + zindex = 50, + } + self.opts.win_opts = vim.tbl_extend("force", win_opts, self.opts.win_opts) + if self.opts.win_opts.style == "" then + self.opts.win_opts.style = nil + end + + self:layout() + self.win = vim.api.nvim_open_win(self.buf, true, self.opts.win_opts) + self:focus() + + vim.bo[self.buf].buftype = "nofile" + if vim.bo[self.buf].filetype == "" then + vim.bo[self.buf].filetype = "lazy" + end + 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" + + vim.api.nvim_create_autocmd("VimResized", { + callback = function() + if not self.win then + return true + end + self:layout() + local config = {} + for _, key in ipairs({ "relative", "width", "height", "col", "row" }) do + config[key] = self.opts.win_opts[key] + end + vim.api.nvim_win_set_config(self.win, config) + end, + }) +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 + +---@param key string +---@param fn fun(self?) +---@param desc? string +function M:on_key(key, fn, desc) + vim.keymap.set("n", key, function() + fn(self) + end, { + nowait = true, + buffer = self.buf, + desc = desc, + }) +end + +function M:close() + local buf = self.buf + local win = self.win + self.win = nil + self.buf = 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 + +return M diff --git a/lua/lazy/view/init.lua b/lua/lazy/view/init.lua index e2ca164..73a3091 100644 --- a/lua/lazy/view/init.lua +++ b/lua/lazy/view/init.lua @@ -3,6 +3,8 @@ local Render = require("lazy.view.render") local Config = require("lazy.core.config") local ViewConfig = require("lazy.view.config") local Git = require("lazy.manage.git") +local Diff = require("lazy.view.diff") +local Float = require("lazy.view.float") ---@class LazyViewState ---@field mode string @@ -15,12 +17,9 @@ local default_state = { }, } ----@class LazyView ----@field buf number ----@field win number +---@class LazyView: LazyFloat ---@field render LazyRender ---@field state LazyViewState ----@field win_opts LazyViewWinOpts local M = {} ---@type LazyView @@ -32,27 +31,27 @@ function M.show(mode) return end - M.view = M.view or M.create({ mode = mode }) - M.view:update(mode) + M.view = (M.view and M.view.win) and M.view or M.create({ mode = mode }) + if mode then + M.view.state.mode = mode + end + M.view:update() end ---@param opts? {mode?:string} function M.create(opts) + local self = setmetatable({}, { __index = setmetatable(M, { __index = Float }) }) + ---@cast self LazyView + Float.init(self) + require("lazy.view.colors").setup() opts = opts or {} - local self = setmetatable({}, { __index = M }) self.state = vim.deepcopy(default_state) - self:mount() - self.render = Render.new(self) self.update = Util.throttle(Config.options.ui.throttle, self.update) - self:on_key(ViewConfig.keys.close, 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 @@ -96,52 +95,21 @@ function M.create(opts) end end) + for key, handler in pairs(Config.options.ui.custom_keys) do + self:on_key(key, function() + local plugin = self.render:get_plugin() + if plugin then + handler(plugin) + end + end) + end + self:setup_patterns() 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 - ----@param key string ----@param fn fun(self?) ----@param desc? string -function M:on_key(key, fn, desc) - vim.keymap.set("n", key, function() - fn(self) - end, { - nowait = true, - buffer = self.buf, - desc = desc, - }) -end - ----@param mode? string -function M:update(mode) - if mode then - self.state.mode = mode - end +function M:update() if self.buf and vim.api.nvim_buf_is_valid(self.buf) then vim.bo[self.buf].modifiable = true self.render:update() @@ -162,74 +130,10 @@ function M:open_url(path) 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 - ---@class LazyViewWinOpts - self.win_opts = { - relative = "editor", - style = "minimal", - border = Config.options.ui.border, - width = size(vim.o.columns, Config.options.ui.size.width), - height = size(vim.o.lines, Config.options.ui.size.height), - noautocmd = true, - } - - 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() - - 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 - function M:setup_patterns() + local commit_pattern = "%f[%w](" .. string.rep("%w", 7) .. ")%f[%W]" self:on_pattern(ViewConfig.keys.hover, { - ["%f[a-z0-9](" .. string.rep("[a-z0-9]", 7) .. ")%f[^a-z0-9]"] = function(hash) + [commit_pattern] = function(hash) self:diff({ commit = hash, browser = true }) end, ["#(%d+)"] = function(issue) @@ -246,6 +150,13 @@ function M:setup_patterns() Util.open(url) end, }, self.hover) + self:on_pattern(ViewConfig.keys.diff, { + [commit_pattern] = function(hash) + self:diff({ commit = hash }) + end, + }, self.diff) +end + function M:hover() if self:diff({ browser = true }) then return @@ -253,8 +164,6 @@ function M:hover() self:open_url("") end ----@alias LazyDiff string|{from:string, to:string} - ---@param opts? {commit?:string, browser:boolean} function M:diff(opts) opts = opts or {} @@ -262,9 +171,9 @@ function M:diff(opts) if plugin then local diff if opts.commit then - diff = opts.commit + diff = { commit = opts.commit } elseif plugin._.updated then - diff = plugin._.updated + diff = vim.deepcopy(plugin._.updated) else local info = assert(Git.info(plugin.dir)) local target = assert(Git.get_target(plugin)) @@ -275,17 +184,14 @@ function M:diff(opts) return end + for k, v in pairs(diff) do + diff[k] = v:sub(1, 7) + end + if opts.browser then - if plugin.url then - local url = plugin.url:gsub("%.git$", "") - if type(diff) == "string" then - Util.open(url .. "/commit/" .. diff) - else - Util.open(url .. "/compare/" .. diff.from .. ".." .. diff.to) - end - else - Util.error("No url for " .. plugin.name) - end + Diff.handlers.browser(plugin, diff) + else + Diff.handlers[Config.options.diff.cmd](plugin, diff) end end end @@ -325,7 +231,8 @@ function M:setup_modes() if m.key then self:on_key(m.key, function() if self.state.mode == name and m.toggle then - return self:update("home") + self.state.mode = "home" + return self:update() end Commands.cmd(name) end, m.desc)