diff --git a/doc/diffview_defaults.txt b/doc/diffview_defaults.txt index 1a89b468..2b55f00f 100644 --- a/doc/diffview_defaults.txt +++ b/doc/diffview_defaults.txt @@ -21,7 +21,7 @@ DEFAULT CONFIG *diffview.defaults* }, view = { -- Configure the layout and behavior of different types of views. - -- Available layouts: + -- Available layouts: -- 'diff1_plain' -- |'diff2_horizontal' -- |'diff2_vertical' @@ -74,6 +74,16 @@ DEFAULT CONFIG *diffview.defaults* multi_file = {}, }, }, + commit_format = { + 'status', + 'stats', + 'hash', + 'reflog', + 'ref', + 'subject', + 'author', + 'date', + }, win_config = { -- See |diffview-config-win_config| position = "bottom", height = 16, diff --git a/lua/diffview/config.lua b/lua/diffview/config.lua index 86922fe1..195cf329 100644 --- a/lua/diffview/config.lua +++ b/lua/diffview/config.lua @@ -96,6 +96,16 @@ M.defaults = { multi_file = {}, }, }, + commit_format = { + 'status', + 'stats', + 'hash', + 'reflog', + 'ref', + 'subject', + 'author', + 'date', + }, win_config = { position = "bottom", height = 16, @@ -349,7 +359,7 @@ function M.get_log_options(single_file, t, vcs) local log_options if single_file then - log_options = M._config.file_history_panel.log_options[vcs].single_file + log_options = M._config.file_history_panel.log_options[vcs].single_file else log_options = M._config.file_history_panel.log_options[vcs].multi_file end @@ -500,7 +510,7 @@ function M.setup(user_config) local old_win_config_spec = { "position", "width", "height" } for _, panel_name in ipairs({ "file_panel", "file_history_panel" }) do local panel_config = M._config[panel_name] - ---@cast panel_config table + ---@cast panel_config table local notified = false for _, option in ipairs(old_win_config_spec) do diff --git a/lua/diffview/scene/views/file_history/render.lua b/lua/diffview/scene/views/file_history/render.lua index 24b13970..5620c321 100644 --- a/lua/diffview/scene/views/file_history/render.lua +++ b/lua/diffview/scene/views/file_history/render.lua @@ -10,6 +10,103 @@ local pl = utils.path local cache = setmetatable({}, { __mode = "k" }) +local formatters = { + ---@type fun(comp:RenderComponent, entry:LogEntry, params:table) + status = function(comp, entry, _) + if entry.status then + comp:add_text(entry.status, hl.get_git_hl(entry.status)) + else + comp:add_text("-", "DiffviewNonText") + end + end, + + ---@type fun(comp:RenderComponent, entry:LogEntry, params:table) + files = function(comp, entry, params) + if entry.single_file then return end + + local s_num_files = tostring(params.max_num_files) + + if entry.nulled then + comp:add_text(utils.str_center_pad("empty", #s_num_files + 7), "DiffviewFilePanelCounter") + else + comp:add_text( + fmt( + " %s file%s", + utils.str_left_pad(tostring(#entry.files), #s_num_files), + #entry.files > 1 and "s" or " " + ), + "DiffviewFilePanelCounter" + ) + end + end, + + ---@type fun(comp:RenderComponent, entry:LogEntry, params:table) + hash = function(comp, entry, _) + if entry.commit.hash then comp:add_text(" " .. entry.commit.hash:sub(1, 8), "DiffviewHash") end + end, + + ---@type fun(comp:RenderComponent, entry:LogEntry, params:table) + stats = function(comp, entry, params) + if params.max_len_stats == -1 then return end + + local adds = { "-", "DiffviewNonText" } + local dels = { "-", "DiffviewNonText" } + + if entry.stats and entry.stats.additions then + adds = { tostring(entry.stats.additions), "DiffviewFilePanelInsertions" } + end + + if entry.stats and entry.stats.deletions then + dels = { tostring(entry.stats.deletions), "DiffviewFilePanelDeletions" } + end + + comp:add_text(" | ", "DiffviewNonText") + comp:add_text(unpack(adds)) + comp:add_text(string.rep(" ", params.max_len_stats - (#adds[1] + #dels[1]))) + comp:add_text(unpack(dels)) + comp:add_text(" |", "DiffviewNonText") + end, + + ---@type fun(comp:RenderComponent, entry:LogEntry, params:table) + reflog = function(comp, entry, _) + local reflog_selector = (entry.commit --[[@as GitCommit ]]).reflog_selector + if reflog_selector then + comp:add_text((" %s"):format(reflog_selector), "DiffviewReflogSelector") + end + end, + + ---@type fun(comp:RenderComponent, entry:LogEntry, params:table) + ref = function(comp, entry, _) + if entry.commit.ref_names then + comp:add_text((" (%s)"):format(entry.commit.ref_names), "DiffviewReference") + end + end, + + ---@type fun(comp:RenderComponent, entry:LogEntry, params:table) + subject = function(comp, entry, params) + local subject = utils.str_trunc(entry.commit.subject, 72) + if subject == "" then subject = "[empty message]" end + comp:add_text( + " " .. subject, + params.panel.cur_item[1] == entry and "DiffviewFilePanelSelected" + or "DiffviewFilePanelFileName" + ) + end, + + ---@type fun(comp:RenderComponent, entry:LogEntry, params:table) + author = function(comp, entry, _) comp:add_text(" " .. entry.commit.author) end, + + ---@type fun(comp:RenderComponent, entry:LogEntry, params:table) + date = function(comp, entry, _) + -- 3 months + local date = ( + os.difftime(os.time(), entry.commit.time) > 60 * 60 * 24 * 30 * 3 and entry.commit.iso_date + or entry.commit.rel_date + ) + comp:add_text(" " .. date, "DiffviewFilePanelPath") + end, +} + ---@param comp RenderComponent ---@param files FileEntry[] local function render_files(comp, files) @@ -88,79 +185,13 @@ local function render_entries(panel, parent, entries, updating) comp:add_text((entry.folded and c.signs.fold_closed or c.signs.fold_open) .. " ", "CursorLineNr") end - if entry.status then - comp:add_text(entry.status, hl.get_git_hl(entry.status)) - else - comp:add_text("-", "DiffviewNonText") - end - - if not entry.single_file then - local s_num_files = tostring(max_num_files) - - if entry.nulled then - comp:add_text(utils.str_center_pad("empty", #s_num_files + 7), "DiffviewFilePanelCounter") + for format in c.file_history_panel.commit_format do + local fn = formatters[format] + if fn then + fn(comp, entry, { max_len_stats, max_num_files, panel }) else - comp:add_text( - fmt( - " %s file%s", - utils.str_left_pad(tostring(#entry.files), #s_num_files), - #entry.files > 1 and "s" or " " - ), - "DiffviewFilePanelCounter" - ) - end - end - - if max_len_stats ~= -1 then - local adds = { "-", "DiffviewNonText" } - local dels = { "-", "DiffviewNonText" } - - if entry.stats and entry.stats.additions then - adds = { tostring(entry.stats.additions), "DiffviewFilePanelInsertions" } - end - - if entry.stats and entry.stats.deletions then - dels = { tostring(entry.stats.deletions), "DiffviewFilePanelDeletions" } + logger:warn("[format] Unsopported commit format: " .. format) end - - comp:add_text(" | ", "DiffviewNonText") - comp:add_text(unpack(adds)) - comp:add_text(string.rep(" ", max_len_stats - (#adds[1] + #dels[1]))) - comp:add_text(unpack(dels)) - comp:add_text(" |", "DiffviewNonText") - end - - if entry.commit.hash then - comp:add_text(" " .. entry.commit.hash:sub(1, 8), "DiffviewHash") - end - - if (entry.commit --[[@as GitCommit ]]).reflog_selector then - comp:add_text((" %s"):format((entry.commit --[[@as GitCommit ]]).reflog_selector), "DiffviewReflogSelector") - end - - if entry.commit.ref_names then - comp:add_text((" (%s)"):format(entry.commit.ref_names), "DiffviewReference") - end - - local subject = utils.str_trunc(entry.commit.subject, 72) - - if subject == "" then - subject = "[empty message]" - end - - comp:add_text( - " " .. subject, - panel.cur_item[1] == entry and "DiffviewFilePanelSelected" or "DiffviewFilePanelFileName" - ) - - if entry.commit then - -- 3 months - local date = ( - os.difftime(os.time(), entry.commit.time) > 60 * 60 * 24 * 30 * 3 - and entry.commit.iso_date - or entry.commit.rel_date - ) - comp:add_text(" " .. entry.commit.author .. ", " .. date, "DiffviewFilePanelPath") end comp:ln() @@ -186,6 +217,8 @@ local function prepare_panel_cache(panel) end return { + commit_formatters = formatters, + ---@param panel FileHistoryPanel file_history_panel = function(panel) if not panel.render_data then diff --git a/lua/diffview/tests/unit/file_history_commit_format_spec.lua b/lua/diffview/tests/unit/file_history_commit_format_spec.lua new file mode 100644 index 00000000..b51d3cfa --- /dev/null +++ b/lua/diffview/tests/unit/file_history_commit_format_spec.lua @@ -0,0 +1,200 @@ +local helpers = require("diffview.tests.helpers") +local config = require("diffview.config") + +local eq, neq = helpers.eq, helpers.neq +local formatters = require("diffview.scene.views.file_history.render").commit_formatters + +-- Windows path standards: +-- https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats + +describe("diffview.scenes.views.file_history.render.formatters", function() + local renderer = require("diffview.renderer") + + it("status()", function() + --- @type RenderComponent + local comp = renderer.RenderComponent.create_static_component(nil) + formatters.status(comp, { status = "M" }, {}) + comp:ln() + eq("M", comp.lines[1]) + + comp:clear() + formatters.status(comp, { status = nil }, {}) + comp:ln() + eq("-", comp.lines[1]) + + comp:destroy() + end) + + it("files()", function() + --- @type RenderComponent + local comp = renderer.RenderComponent.create_static_component(nil) + formatters.files(comp, { single_file = false, files = { "a file" } }, { max_num_files = 1 }) + comp:ln() + eq(" 1 file ", comp.lines[1]) + eq("DiffviewFilePanelCounter", comp.hl[1].group) + + comp:clear() + formatters.files( + comp, + { nulled = true, single_file = false, files = { "a file" } }, + { max_num_files = 1 } + ) + comp:ln() + eq(" empty ", comp.lines[1]) + + comp:clear() + formatters.files(comp, { single_file = true }, {}) + comp:ln() + eq("", comp.lines[1]) + + comp:destroy() + end) + + it("hash()", function() + --- @type RenderComponent + local comp = renderer.RenderComponent.create_static_component(nil) + formatters.hash( + comp, + { commit = { hash = "762489b5c8d74bf8bbfb211d49aed686" } }, + { max_num_files = 1 } + ) + comp:ln() + eq(" 762489b5", comp.lines[1]) + eq("DiffviewHash", comp.hl[1].group) + + comp:destroy() + end) + + it("stats()", function() + --- @type RenderComponent + local comp = renderer.RenderComponent.create_static_component(nil) + formatters.stats(comp, { stats = { additions = 10, deletions = 22 } }, { max_len_stats = 4 }) + comp:ln() + eq(" | 1022 |", comp.lines[1]) + eq("DiffviewNonText", comp.hl[1].group) + eq("DiffviewFilePanelInsertions", comp.hl[2].group) + eq("DiffviewFilePanelDeletions", comp.hl[3].group) + eq("DiffviewNonText", comp.hl[4].group) + + comp:destroy() + end) + + it("reflog()", function() + --- @type RenderComponent + local comp = renderer.RenderComponent.create_static_component(nil) + formatters.reflog(comp, { commit = { reflog_selector = "reflog" } }, {}) + comp:ln() + eq(" reflog", comp.lines[1]) + eq("DiffviewReflogSelector", comp.hl[1].group) + + comp:destroy() + end) + + it("ref()", function() + --- @type RenderComponent + local comp = renderer.RenderComponent.create_static_component(nil) + formatters.ref(comp, { commit = { ref_names = "main" } }, {}) + comp:ln() + eq(" (main)", comp.lines[1]) + eq("DiffviewReference", comp.hl[1].group) + + comp:destroy() + end) + + it("subject()", function() + --- @type RenderComponent + local comp = renderer.RenderComponent.create_static_component(nil) + formatters.subject( + comp, + { commit = { subject = "refactor: cleanup" } }, + { panel = { cur_item = { nil } } } + ) + comp:ln() + eq(" refactor: cleanup", comp.lines[1]) + eq("DiffviewFilePanelFileName", comp.hl[1].group) + + comp:clear() + formatters.subject(comp, { commit = { subject = "" } }, { panel = { cur_item = { nil } } }) + comp:ln() + eq(" [empty message]", comp.lines[1]) + eq("DiffviewFilePanelFileName", comp.hl[1].group) + + comp:clear() + local entry = { commit = { subject = "fix #1111" } } + formatters.subject(comp, entry, { panel = { cur_item = { entry } } }) + comp:ln() + eq(" fix #1111", comp.lines[1]) + eq("DiffviewFilePanelSelected", comp.hl[1].group) + + comp:destroy() + end) + + it("author()", function() + --- @type RenderComponent + local comp = renderer.RenderComponent.create_static_component(nil) + formatters.author(comp, { commit = { author = "Dale Cooper" } }, {}) + comp:ln() + eq(" Dale Cooper", comp.lines[1]) + + comp:destroy() + end) + + it("date()", function() + --- @type RenderComponent + local comp = renderer.RenderComponent.create_static_component(nil) + local time = os.time({ year = 2023, month = 1, day = 1 }) + local iso = os.date("%FT%TZ", time) + formatters.date(comp, { commit = { time = time, iso_date = iso } }, {}) + comp:ln() + eq(" " .. iso, comp.lines[1]) + + comp:clear() + local tmp_diff_time = os.difftime + os.difftime = function() + return 1 + end + + formatters.date(comp, { commit = { time = time, iso_date = iso, rel_date = '1 day' } }, {}) + comp:ln() + eq(" 1 day", comp.lines[1]) + + comp:destroy() + + os.difftime = tmp_diff_time + end) + + it("default config format", function() + --- @type RenderComponent + local comp = renderer.RenderComponent.create_static_component(nil) + local c = config.get_config() + local time = os.time({ year = 2023, month = 1, day = 1 }) + local iso = os.date("%FT%TZ", time) + local entry = { + stats = { additions = 121, deletions = 101 }, + status = "M", + commit = { + time = time, + iso_date = iso, + hash = "ba89b7310101", + subject = "fix #1", + author = "Dale Cooper", + }, + } + + local params = { + panel = { cur_item = { nil } }, + max_num_files = 1, + max_len_stats = 7, + } + + for _, f in ipairs(c.file_history_panel.commit_format) do + formatters[f](comp, entry, params) + end + + comp:ln() + local expected = string.format("M | 121 101 | ba89b731 fix #1 Dale Cooper %s", iso) + eq(expected, table.concat(comp.lines, " ")) + + comp:destroy() + end) +end)