Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 95 additions & 14 deletions lua/gitlad/ui/components/checks.lua
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,82 @@ local types = require("gitlad.forge.types")

---@class ChecksRenderOptions
---@field collapsed? boolean Whether the section is collapsed (default: false)
---@field sub_collapsed? table<string, boolean> Per-category collapsed state
---@field sub_threshold? number Min checks to trigger sub-section (default: 5)

---@class CheckLineInfo
---@field type string Line type discriminator
---@field check? ForgeCheck Check reference for check lines
---@field sub_category? string Category key for sub-section headers

---@class ChecksRenderResult
---@field lines string[] Formatted lines
---@field line_info table<number, CheckLineInfo> Maps line index (1-based) to line metadata
---@field ranges table<string, {start: number, end_line: number}> Named ranges

-- Category definitions in display order
M.categories = {
{ key = "failed", label = "Failed" },
{ key = "in_progress", label = "In progress" },
{ key = "successful", label = "Successful" },
{ key = "pending", label = "Pending" },
{ key = "skipped", label = "Skipped" },
}

--- Classify a check into a category key
---@param check ForgeCheck
---@return string category One of "failed", "in_progress", "successful", "pending", "skipped"
function M.classify_check(check)
if check.status == "in_progress" then
return "in_progress"
end
if check.status == "queued" then
return "pending"
end
if check.status == "completed" then
local c = check.conclusion
if c == "failure" or c == "timed_out" or c == "startup_failure" then
return "failed"
elseif c == "success" then
return "successful"
elseif c == "action_required" then
return "pending"
elseif c == "cancelled" or c == "skipped" or c == "neutral" then
return "skipped"
end
end
return "pending"
end

--- Format a single check line
---@param check ForgeCheck
---@param indent string Indentation prefix
---@return string
local function format_check_line(check, indent)
local icon, _ = types.format_check_icon(check)
local parts = { indent .. icon .. " " .. check.name }

if check.app_name then
table.insert(parts, " (" .. check.app_name .. ")")
end

local duration = types.format_check_duration(check.started_at, check.completed_at)
if duration ~= "" then
table.insert(parts, " " .. duration)
end

return table.concat(parts)
end

--- Render a checks section
---@param checks_summary ForgeChecksSummary
---@param opts? ChecksRenderOptions
---@return ChecksRenderResult
function M.render(checks_summary, opts)
opts = opts or {}
local collapsed = opts.collapsed or false
local sub_collapsed = opts.sub_collapsed or {}
local threshold = opts.sub_threshold or 5

local result = {
lines = {},
Expand All @@ -53,24 +112,44 @@ function M.render(checks_summary, opts)
return result
end

-- Individual check lines
local checks_start = #result.lines + 1
-- Classify checks into category buckets
local buckets = {}
for _, cat in ipairs(M.categories) do
buckets[cat.key] = {}
end
for _, check in ipairs(checks_summary.checks) do
local icon, _ = types.format_check_icon(check)
local parts = { " " .. icon .. " " .. check.name }

-- App name in parentheses
if check.app_name then
table.insert(parts, " (" .. check.app_name .. ")")
local cat = M.classify_check(check)
if buckets[cat] then
table.insert(buckets[cat], check)
end
end

-- Duration
local duration = types.format_check_duration(check.started_at, check.completed_at)
if duration ~= "" then
table.insert(parts, " " .. duration)
-- Render each category
local checks_start = #result.lines + 1
for _, cat in ipairs(M.categories) do
local checks = buckets[cat.key]
if #checks > 0 then
if #checks > threshold then
-- Sub-section with header
local is_collapsed = sub_collapsed[cat.key] or false
local sub_indicator = is_collapsed and ">" or "v"
local sub_header = " " .. sub_indicator .. " " .. cat.label .. " (" .. #checks .. ")"
add_line(sub_header, { type = "checks_sub_header", sub_category = cat.key })

local sub_start = #result.lines
if not is_collapsed then
for _, check in ipairs(checks) do
add_line(format_check_line(check, " "), { type = "check", check = check })
end
end
result.ranges["checks_sub_" .. cat.key] = { start = sub_start, end_line = #result.lines }
else
-- Flat rendering
for _, check in ipairs(checks) do
add_line(format_check_line(check, " "), { type = "check", check = check })
end
end
end

add_line(table.concat(parts), { type = "check", check = check })
end

if #checks_summary.checks > 0 then
Expand Down Expand Up @@ -100,6 +179,8 @@ function M.apply_highlights(bufnr, ns, start_line, result)

if info.type == "checks_header" then
hl.set(bufnr, ns, line_idx, 0, #line, "GitladSectionHeader")
elseif info.type == "checks_sub_header" then
hl.set(bufnr, ns, line_idx, 0, #line, "GitladSectionHeader")
elseif info.type == "check" and info.check then
-- Highlight the icon character based on check state
local _, icon_hl = types.format_check_icon(info.check)
Expand Down
8 changes: 7 additions & 1 deletion lua/gitlad/ui/components/comment.lua
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ local types = require("gitlad.forge.types")
---@class CommentRenderOptions
---@field wrap_width? number Wrap text at this width (default: 80)
---@field checks_collapsed? boolean Whether checks section is collapsed (default: false)
---@field checks_sub_collapsed? table<string, boolean> Per-category collapsed state

---@class CommentLineInfo
---@field type string Line type discriminator
Expand Down Expand Up @@ -144,6 +145,7 @@ function M.render(pr, opts)
local checks_component = require("gitlad.ui.components.checks")
local checks_result = checks_component.render(pr.checks_summary, {
collapsed = opts.checks_collapsed or false,
sub_collapsed = opts.checks_sub_collapsed,
})

-- Merge checks lines into result with offset
Expand Down Expand Up @@ -342,7 +344,11 @@ function M.apply_highlights(bufnr, ns, start_line, result)
local _, merge_hl = types.format_merge_status(info.pr.mergeable, info.pr.merge_state_status)
hl.set(bufnr, ns, line_idx, #prefix, #line, merge_hl)
end
elseif info.type == "checks_header" or info.type == "check" then
elseif
info.type == "checks_header"
or info.type == "checks_sub_header"
or info.type == "check"
then
-- Delegate to checks component highlighting
local checks_component = require("gitlad.ui.components.checks")
-- Build a minimal single-line result for the checks highlighter
Expand Down
20 changes: 17 additions & 3 deletions lua/gitlad/ui/views/pr_detail.lua
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ local hl = require("gitlad.ui.hl")
---@field line_map table<number, CommentLineInfo> Map of line numbers to line info
---@field ranges table<string, {start: number, end_line: number}> Named ranges
---@field checks_collapsed boolean Whether checks section is collapsed
---@field checks_sub_collapsed table<string, boolean> Per-category collapsed state
local PRDetailBuffer = {}
PRDetailBuffer.__index = PRDetailBuffer

Expand Down Expand Up @@ -48,6 +49,7 @@ local function get_or_create_buffer(repo_state, provider)
self.line_map = {}
self.ranges = {}
self.checks_collapsed = false
self.checks_sub_collapsed = {}

-- Create buffer
self.bufnr = vim.api.nvim_create_buf(false, true)
Expand Down Expand Up @@ -219,10 +221,13 @@ function PRDetailBuffer:_is_nav_target(line)
return true
end

-- Checks header is a navigation target
-- Checks header and sub-headers are navigation targets
if info.type == "checks_header" then
return true
end
if info.type == "checks_sub_header" then
return true
end

-- Comment/review lines: only the @author header line
if info.type == "comment" or info.type == "review" then
Expand Down Expand Up @@ -287,9 +292,17 @@ function PRDetailBuffer:_action_at_cursor()
end
end

--- Toggle checks section collapsed/expanded
--- Toggle checks section or sub-section collapsed/expanded
function PRDetailBuffer:_toggle_checks()
self.checks_collapsed = not self.checks_collapsed
local info = self:_get_current_info()
if info and info.type == "checks_sub_header" and info.sub_category then
-- Toggle sub-category
local cat = info.sub_category
self.checks_sub_collapsed[cat] = not self.checks_sub_collapsed[cat]
else
-- Toggle whole section
self.checks_collapsed = not self.checks_collapsed
end
self:render()
end

Expand Down Expand Up @@ -527,6 +540,7 @@ function PRDetailBuffer:render()

local result = comment_component.render(self.pr, {
checks_collapsed = self.checks_collapsed,
checks_sub_collapsed = self.checks_sub_collapsed,
})
local lines = result.lines
self.line_map = result.line_info
Expand Down
Loading