From eaae490988364541af692f54fdb6ba1c44aba3db Mon Sep 17 00:00:00 2001 From: Lucas Adelino Date: Tue, 19 Aug 2025 14:03:06 -0400 Subject: [PATCH 1/4] feat(margin): add shortstat toggle --- lua/neogit/buffers/status/ui.lua | 142 ++++++++++++++++----------- lua/neogit/lib/git/cli.lua | 2 + lua/neogit/lib/util.lua | 18 +++- lua/neogit/popups/margin/actions.lua | 6 ++ lua/neogit/popups/margin/init.lua | 2 +- 5 files changed, 107 insertions(+), 63 deletions(-) diff --git a/lua/neogit/buffers/status/ui.lua b/lua/neogit/buffers/status/ui.lua index 0452bdeea..0d6c4e2fc 100755 --- a/lua/neogit/buffers/status/ui.lua +++ b/lua/neogit/buffers/status/ui.lua @@ -5,6 +5,7 @@ local common = require("neogit.buffers.common") local config = require("neogit.config") local a = require("plenary.async") local state = require("neogit.lib.state") +local git = require("neogit.lib.git") local col = Ui.col local row = Ui.row @@ -365,73 +366,98 @@ local SectionItemCommit = Component.new(function(item) -- Render author and date in margin, if visible if state.get({ "margin", "visibility" }, false) then - local margin_date_style = state.get({ "margin", "date_style" }, 1) - local details = state.get({ "margin", "details" }, false) - - local date - local rel_date - local date_width = 10 - local clamp_width = 30 -- to avoid having too much space when relative date is short - - -- Render date - if item.commit.rel_date:match(" years?,") then - rel_date, _ = item.commit.rel_date:gsub(" years?,", "y") - rel_date = rel_date .. " " - elseif item.commit.rel_date:match("^%d ") then - rel_date = " " .. item.commit.rel_date - else - rel_date = item.commit.rel_date - end + local is_shortstat = state.get({ "margin", "shortstat" }, false) + + if is_shortstat then + local cli_shortstat = git.cli.show.format("").shortstat.args(item.commit.oid).call().stdout[1] + local files_changed + local insertions + local deletions + + files_changed = cli_shortstat:match("^ (%d+) files?") + files_changed = util.str_min_width(files_changed, 3, nil, false) + insertions = cli_shortstat:match("(%d+) insertions?") + insertions = util.str_min_width(insertions and insertions .. "+" or " ", 5, nil, false) + deletions = cli_shortstat:match("(%d+) deletions?") + deletions = util.str_min_width(deletions and deletions .. "-" or " ", 5, nil, false) + + virtual_text = { + { " ", "Constant" }, + { insertions, "NeogitDiffAdditions" }, + { " ", "Constant" }, + { deletions, "NeogitDiffDeletions" }, + { " ", "Constant" }, + { files_changed, "NeogitSubtleText" }, + } + else -- Author & date margin + local margin_date_style = state.get({ "margin", "date_style" }, 1) + local details = state.get({ "margin", "details" }, false) + + local date + local rel_date + local date_width = 10 + local clamp_width = 30 -- to avoid having too much space when relative date is short + + -- Render date + if item.commit.rel_date:match(" years?,") then + rel_date, _ = item.commit.rel_date:gsub(" years?,", "y") + rel_date = rel_date .. " " + elseif item.commit.rel_date:match("^%d ") then + rel_date = " " .. item.commit.rel_date + else + rel_date = item.commit.rel_date + end - if margin_date_style == 1 then -- relative date (short) - local unpacked = vim.split(rel_date, " ") + if margin_date_style == 1 then -- relative date (short) + local unpacked = vim.split(rel_date, " ") - -- above, we added a space if the rel_date started with a single number - -- we get the last two elements to deal with that - local date_number = unpacked[#unpacked - 1] - local date_quantifier = unpacked[#unpacked] - if date_quantifier:match("months?") then - date_quantifier = date_quantifier:gsub("m", "M") -- to distinguish from minutes - end + -- above, we added a space if the rel_date started with a single number + -- we get the last two elements to deal with that + local date_number = unpacked[#unpacked - 1] + local date_quantifier = unpacked[#unpacked] + if date_quantifier:match("months?") then + date_quantifier = date_quantifier:gsub("m", "M") -- to distinguish from minutes + end - -- add back the space if we have a single number - local left_pad - if #unpacked > 2 then - left_pad = " " - else - left_pad = "" + -- add back the space if we have a single number + local left_pad + if #unpacked > 2 then + left_pad = " " + else + left_pad = "" + end + + date = left_pad .. date_number .. date_quantifier:sub(1, 1) + date_width = 3 + clamp_width = 23 + elseif margin_date_style == 2 then -- relative date (long) + date = rel_date + date_width = 10 + else -- local iso date + if config.values.log_date_format == nil then + -- we get the unix date to be able to convert the date to the local timezone + date = os.date("%Y-%m-%d %H:%M", item.commit.unix_date) + date_width = 16 -- TODO: what should the width be here? + else + date = item.commit.log_date + date_width = 16 + end end - date = left_pad .. date_number .. date_quantifier:sub(1, 1) - date_width = 3 - clamp_width = 23 - elseif margin_date_style == 2 then -- relative date (long) - date = rel_date - date_width = 10 - else -- local iso date - if config.values.log_date_format == nil then - -- we get the unix date to be able to convert the date to the local timezone - date = os.date("%Y-%m-%d %H:%M", item.commit.unix_date) - date_width = 16 -- TODO: what should the width be here? - else - date = item.commit.log_date - date_width = 16 + local author_table = { "" } + if details then + author_table = { + util.str_clamp(item.commit.author_name, clamp_width - (#date > date_width and #date or date_width)), + "NeogitGraphAuthor", + } end - end - local author_table = { "" } - if details then - author_table = { - util.str_clamp(item.commit.author_name, clamp_width - (#date > date_width and #date or date_width)), - "NeogitGraphAuthor", + virtual_text = { + { " ", "Constant" }, + author_table, + { util.str_min_width(date, date_width), "Special" }, } end - - virtual_text = { - { " ", "Constant" }, - author_table, - { util.str_min_width(date, date_width), "Special" }, - } end return row( diff --git a/lua/neogit/lib/git/cli.lua b/lua/neogit/lib/git/cli.lua index 315246e41..d65c78960 100644 --- a/lua/neogit/lib/git/cli.lua +++ b/lua/neogit/lib/git/cli.lua @@ -41,6 +41,7 @@ local runner = require("neogit.runner") ---@class GitCommandShow: GitCommandBuilder ---@field stat self +---@field shortstat self ---@field oneline self ---@field no_patch self ---@field format fun(string): self @@ -396,6 +397,7 @@ local configurations = { show = config { flags = { stat = "--stat", + shortstat = "--shortstat", oneline = "--oneline", no_patch = "--no-patch", }, diff --git a/lua/neogit/lib/util.lua b/lua/neogit/lib/util.lua index 46c9942f6..b3d4c9836 100644 --- a/lua/neogit/lib/util.lua +++ b/lua/neogit/lib/util.lua @@ -197,13 +197,21 @@ end -- return res -- end -function M.str_min_width(str, len, sep) +---@param append boolean? If true or nil, adds spaces to the end of `str`. If false, adds spaces to the beginning +function M.str_min_width(str, len, sep, append) + append = append == nil and true or append local length = vim.fn.strdisplaywidth(str) if length > len then return str end - return str .. string.rep(sep or " ", len - length) + if append then + -- Add spaces to the right of str + return str .. string.rep(sep or " ", len - length) + else + -- Add spaces to the left of str + return string.rep(sep or " ", len - length) .. str + end end function M.slice(tbl, s, e) @@ -255,8 +263,10 @@ function M.str_truncate(str, max_length, trailing) return str end -function M.str_clamp(str, len, sep) - return M.str_min_width(M.str_truncate(str, len - 1, ""), len, sep or " ") +---@param append boolean? If true or nil, adds spaces to the end of `str`. If false, adds spaces to the beginning +function M.str_clamp(str, len, sep, append) + append = append == nil and true or append + return M.str_min_width(M.str_truncate(str, len - 1, ""), len, sep or " ", append) end --- Splits a string every n characters, respecting word boundaries diff --git a/lua/neogit/popups/margin/actions.lua b/lua/neogit/popups/margin/actions.lua index 8ab733736..df2df5079 100644 --- a/lua/neogit/popups/margin/actions.lua +++ b/lua/neogit/popups/margin/actions.lua @@ -29,4 +29,10 @@ function M.toggle_details() state.set({ "margin", "details" }, new_details) end +function M.toggle_shortstat() + local shortstat = state.get({ "margin", "shortstat" }, false) + local new_shortstat = not shortstat + state.set({ "margin", "shortstat" }, new_shortstat) +end + return M diff --git a/lua/neogit/popups/margin/init.lua b/lua/neogit/popups/margin/init.lua index 154d4f6b4..8ac61ed91 100644 --- a/lua/neogit/popups/margin/init.lua +++ b/lua/neogit/popups/margin/init.lua @@ -50,7 +50,7 @@ function M.create(env) :action("L", "toggle visibility", actions.toggle_visibility, { persist_popup = true }) :action("l", "cycle style", actions.cycle_date_style, { persist_popup = true }) :action("d", "toggle details", actions.toggle_details, { persist_popup = true }) - :action("x", "toggle shortstat", actions.log_current, { persist_popup = true }) + :action("x", "toggle shortstat", actions.toggle_shortstat, { persist_popup = true }) :build() p:show() From 431f7959ea981efe6e7a2af271b6652d497d055b Mon Sep 17 00:00:00 2001 From: Lucas Adelino Date: Wed, 20 Aug 2025 12:22:45 -0400 Subject: [PATCH 2/4] change append boolean to opts table in str util functions --- lua/neogit/buffers/status/ui.lua | 6 +++--- lua/neogit/lib/util.lua | 16 ++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/lua/neogit/buffers/status/ui.lua b/lua/neogit/buffers/status/ui.lua index 0d6c4e2fc..79e77a025 100755 --- a/lua/neogit/buffers/status/ui.lua +++ b/lua/neogit/buffers/status/ui.lua @@ -375,11 +375,11 @@ local SectionItemCommit = Component.new(function(item) local deletions files_changed = cli_shortstat:match("^ (%d+) files?") - files_changed = util.str_min_width(files_changed, 3, nil, false) + files_changed = util.str_min_width(files_changed, 3, nil, { mode = "insert" }) insertions = cli_shortstat:match("(%d+) insertions?") - insertions = util.str_min_width(insertions and insertions .. "+" or " ", 5, nil, false) + insertions = util.str_min_width(insertions and insertions .. "+" or " ", 5, nil, { mode = "insert" }) deletions = cli_shortstat:match("(%d+) deletions?") - deletions = util.str_min_width(deletions and deletions .. "-" or " ", 5, nil, false) + deletions = util.str_min_width(deletions and deletions .. "-" or " ", 5, nil, { mode = "insert" }) virtual_text = { { " ", "Constant" }, diff --git a/lua/neogit/lib/util.lua b/lua/neogit/lib/util.lua index b3d4c9836..02c15dc1b 100644 --- a/lua/neogit/lib/util.lua +++ b/lua/neogit/lib/util.lua @@ -197,15 +197,15 @@ end -- return res -- end ----@param append boolean? If true or nil, adds spaces to the end of `str`. If false, adds spaces to the beginning -function M.str_min_width(str, len, sep, append) - append = append == nil and true or append +---@param opts table? If { mode = 'append' }, adds spaces to the end of `str`. If { mode = 'insert' }, adds spaces to the beginning. +function M.str_min_width(str, len, sep, opts) + local mode = (type(opts) == "table" and opts.mode) or "append" local length = vim.fn.strdisplaywidth(str) if length > len then return str end - if append then + if mode == "append" then -- Add spaces to the right of str return str .. string.rep(sep or " ", len - length) else @@ -263,10 +263,10 @@ function M.str_truncate(str, max_length, trailing) return str end ----@param append boolean? If true or nil, adds spaces to the end of `str`. If false, adds spaces to the beginning -function M.str_clamp(str, len, sep, append) - append = append == nil and true or append - return M.str_min_width(M.str_truncate(str, len - 1, ""), len, sep or " ", append) +---@param opts table? If { mode = 'append' }, adds spaces to the end of `str`. If { mode = 'insert' }, adds spaces to the beginning. +function M.str_clamp(str, len, sep, opts) + local opts = (type(opts) == "table" and opts.mode) or { mode = "append" } + return M.str_min_width(M.str_truncate(str, len - 1, ""), len, sep or " ", opts) end --- Splits a string every n characters, respecting word boundaries From 062168240e3bc90cd76646b4fc12093af01b4293 Mon Sep 17 00:00:00 2001 From: Lucas Adelino Date: Wed, 20 Aug 2025 17:39:49 -0400 Subject: [PATCH 3/4] clear up margin comment --- lua/neogit/buffers/status/ui.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/neogit/buffers/status/ui.lua b/lua/neogit/buffers/status/ui.lua index 79e77a025..810998e44 100755 --- a/lua/neogit/buffers/status/ui.lua +++ b/lua/neogit/buffers/status/ui.lua @@ -364,7 +364,7 @@ local SectionItemCommit = Component.new(function(item) local virtual_text - -- Render author and date in margin, if visible + -- Render margin, if visible if state.get({ "margin", "visibility" }, false) then local is_shortstat = state.get({ "margin", "shortstat" }, false) From 8c477e9fd86f195242e47c9ea1de6806dba4e7fb Mon Sep 17 00:00:00 2001 From: Lucas Adelino Date: Wed, 20 Aug 2025 17:42:56 -0400 Subject: [PATCH 4/4] refactor(margin): move shortstat fetcthing out of ui layer --- lua/neogit/buffers/status/ui.lua | 3 +-- lua/neogit/lib/git/log.lua | 7 +++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lua/neogit/buffers/status/ui.lua b/lua/neogit/buffers/status/ui.lua index 810998e44..9accb8c10 100755 --- a/lua/neogit/buffers/status/ui.lua +++ b/lua/neogit/buffers/status/ui.lua @@ -5,7 +5,6 @@ local common = require("neogit.buffers.common") local config = require("neogit.config") local a = require("plenary.async") local state = require("neogit.lib.state") -local git = require("neogit.lib.git") local col = Ui.col local row = Ui.row @@ -369,7 +368,7 @@ local SectionItemCommit = Component.new(function(item) local is_shortstat = state.get({ "margin", "shortstat" }, false) if is_shortstat then - local cli_shortstat = git.cli.show.format("").shortstat.args(item.commit.oid).call().stdout[1] + local cli_shortstat = item.shortstat local files_changed local insertions local deletions diff --git a/lua/neogit/lib/git/log.lua b/lua/neogit/lib/git/log.lua index 54ac725b7..a6a0e3314 100644 --- a/lua/neogit/lib/git/log.lua +++ b/lua/neogit/lib/git/log.lua @@ -462,11 +462,18 @@ function M.present_commit(commit) return end + local is_shortstat = state.get({ "margin", "shortstat" }, false) + local shortstat + if is_shortstat then + shortstat = git.cli.show.format("").shortstat.args(commit.oid).call().stdout[1] + end + return { name = string.format("%s %s", commit.abbreviated_commit, commit.subject or ""), decoration = M.branch_info(commit.ref_name, git.remote.list()), oid = commit.oid, commit = commit, + shortstat = shortstat, } end