cp.nvim/lua/cp/runner/run_render.lua
2025-10-23 09:54:55 -04:00

370 lines
9.7 KiB
Lua

---@class StatusInfo
---@field text string
---@field highlight_group string
local M = {}
local exit_code_names = {
[128] = 'SIGHUP',
[129] = 'SIGINT',
[130] = 'SIGQUIT',
[131] = 'SIGILL',
[132] = 'SIGTRAP',
[133] = 'SIGABRT',
[134] = 'SIGBUS',
[135] = 'SIGFPE',
[136] = 'SIGKILL',
[137] = 'SIGUSR1',
[138] = 'SIGSEGV',
[139] = 'SIGUSR2',
[140] = 'SIGPIPE',
[141] = 'SIGALRM',
[142] = 'SIGTERM',
[143] = 'SIGCHLD',
}
---@param ran_test_case RanTestCase
---@return StatusInfo
function M.get_status_info(ran_test_case)
if ran_test_case.ok then
return { text = 'AC', highlight_group = 'CpTestAC' }
end
if ran_test_case.tled then
return { text = 'TLE', highlight_group = 'CpTestTLE' }
elseif ran_test_case.mled then
return { text = 'MLE', highlight_group = 'CpTestMLE' }
elseif ran_test_case.code > 0 and ran_test_case.code >= 128 then
return { text = 'RTE', highlight_group = 'CpTestRTE' }
elseif ran_test_case.code == 0 and not ran_test_case.ok then
return { text = 'WA', highlight_group = 'CpTestWA' }
end
return { text = 'N/A', highlight_group = 'CpTestNA' }
end
local function format_exit_code(code)
if not code then
return ''
end
local signal_name = exit_code_names[code]
return signal_name and string.format('%d (%s)', code, signal_name) or tostring(code)
end
local function compute_cols(test_state)
local w = { num = 5, status = 8, time = 6, timeout = 8, rss = 8, memory = 8, exit = 11 }
local timeout_str = ''
local memory_str = ''
if test_state.constraints then
timeout_str = tostring(test_state.constraints.timeout_ms)
memory_str = string.format('%.0f', test_state.constraints.memory_mb)
end
for i, tc in ipairs(test_state.test_cases) do
local prefix = (i == test_state.current_index) and '>' or ' '
w.num = math.max(w.num, #(' ' .. prefix .. i .. ' '))
w.status = math.max(w.status, #(' ' .. M.get_status_info(tc).text .. ' '))
local time_str = tc.time_ms and string.format('%.2f', tc.time_ms) or ''
w.time = math.max(w.time, #(' ' .. time_str .. ' '))
w.timeout = math.max(w.timeout, #(' ' .. timeout_str .. ' '))
local rss_str = (tc.rss_mb and string.format('%.0f', tc.rss_mb)) or ''
w.rss = math.max(w.rss, #(' ' .. rss_str .. ' '))
w.memory = math.max(w.memory, #(' ' .. memory_str .. ' '))
w.exit = math.max(w.exit, #(' ' .. format_exit_code(tc.code) .. ' '))
end
w.num = math.max(w.num, #' # ')
w.status = math.max(w.status, #' Status ')
w.time = math.max(w.time, #' Runtime (ms) ')
w.timeout = math.max(w.timeout, #' Time (ms) ')
w.rss = math.max(w.rss, #' RSS (MB) ')
w.memory = math.max(w.memory, #' Mem (MB) ')
w.exit = math.max(w.exit, #' Exit Code ')
local sum = w.num + w.status + w.time + w.timeout + w.rss + w.memory + w.exit
local inner = sum + 6
local total = inner + 2
return { w = w, sum = sum, inner = inner, total = total }
end
local function center(text, width)
local pad = width - #text
if pad <= 0 then
return text
end
local left = math.ceil(pad / 2)
return string.rep(' ', left) .. text .. string.rep(' ', pad - left)
end
local function format_num_column(prefix, idx, width)
local num_str = tostring(idx)
local content = (#num_str == 1) and (' ' .. prefix .. ' ' .. num_str .. ' ')
or (' ' .. prefix .. num_str .. ' ')
local total_pad = width - #content
if total_pad <= 0 then
return content
end
local left_pad = math.ceil(total_pad / 2)
local right_pad = total_pad - left_pad
return string.rep(' ', left_pad) .. content .. string.rep(' ', right_pad)
end
local function top_border(c)
local w = c.w
return ''
.. string.rep('', w.num)
.. ''
.. string.rep('', w.status)
.. ''
.. string.rep('', w.time)
.. ''
.. string.rep('', w.timeout)
.. ''
.. string.rep('', w.rss)
.. ''
.. string.rep('', w.memory)
.. ''
.. string.rep('', w.exit)
.. ''
end
local function row_sep(c)
local w = c.w
return ''
.. string.rep('', w.num)
.. ''
.. string.rep('', w.status)
.. ''
.. string.rep('', w.time)
.. ''
.. string.rep('', w.timeout)
.. ''
.. string.rep('', w.rss)
.. ''
.. string.rep('', w.memory)
.. ''
.. string.rep('', w.exit)
.. ''
end
local function bottom_border(c)
local w = c.w
return ''
.. string.rep('', w.num)
.. ''
.. string.rep('', w.status)
.. ''
.. string.rep('', w.time)
.. ''
.. string.rep('', w.timeout)
.. ''
.. string.rep('', w.rss)
.. ''
.. string.rep('', w.memory)
.. ''
.. string.rep('', w.exit)
.. ''
end
local function flat_fence_above(c)
local w = c.w
return ''
.. string.rep('', w.num)
.. ''
.. string.rep('', w.status)
.. ''
.. string.rep('', w.time)
.. ''
.. string.rep('', w.timeout)
.. ''
.. string.rep('', w.rss)
.. ''
.. string.rep('', w.memory)
.. ''
.. string.rep('', w.exit)
.. ''
end
local function flat_fence_below(c)
local w = c.w
return ''
.. string.rep('', w.num)
.. ''
.. string.rep('', w.status)
.. ''
.. string.rep('', w.time)
.. ''
.. string.rep('', w.timeout)
.. ''
.. string.rep('', w.rss)
.. ''
.. string.rep('', w.memory)
.. ''
.. string.rep('', w.exit)
.. ''
end
local function flat_bottom_border(c)
return '' .. string.rep('', c.inner) .. ''
end
local function header_line(c)
local w = c.w
return ''
.. center('#', w.num)
.. ''
.. center('Status', w.status)
.. ''
.. center('Runtime (ms)', w.time)
.. ''
.. center('Time (ms)', w.timeout)
.. ''
.. center('RSS (MB)', w.rss)
.. ''
.. center('Mem (MB)', w.memory)
.. ''
.. center('Exit Code', w.exit)
.. ''
end
local function data_row(c, idx, tc, is_current, test_state)
local w = c.w
local prefix = is_current and '>' or ' '
local status = M.get_status_info(tc)
local time = tc.time_ms and string.format('%.2f', tc.time_ms) or ''
local exit = format_exit_code(tc.code)
local timeout = ''
local memory = ''
if test_state.constraints then
timeout = tostring(test_state.constraints.timeout_ms)
memory = string.format('%.0f', test_state.constraints.memory_mb)
end
local rss = (tc.rss_mb and string.format('%.0f', tc.rss_mb)) or ''
local line = ''
.. format_num_column(prefix, idx, w.num)
.. ''
.. center(status.text, w.status)
.. ''
.. center(time, w.time)
.. ''
.. center(timeout, w.timeout)
.. ''
.. center(rss, w.rss)
.. ''
.. center(memory, w.memory)
.. ''
.. center(exit, w.exit)
.. ''
local hi
if status.text ~= '' then
local status_pos = line:find(status.text, 1, true)
if status_pos then
hi = {
col_start = status_pos - 1,
col_end = status_pos - 1 + #status.text,
highlight_group = status.highlight_group,
}
end
end
return line, hi
end
---@param test_state PanelState
---@return string[], Highlight[] lines and highlight positions
function M.render_test_list(test_state)
local lines, highlights = {}, {}
local c = compute_cols(test_state)
table.insert(lines, top_border(c))
table.insert(lines, header_line(c))
table.insert(lines, row_sep(c))
for i, tc in ipairs(test_state.test_cases) do
local is_current = (i == test_state.current_index)
local row, hi = data_row(c, i, tc, is_current, test_state)
table.insert(lines, row)
if hi then
hi.line = #lines - 1
table.insert(highlights, hi)
end
local has_next = (i < #test_state.test_cases)
local has_input = is_current and tc.input and tc.input ~= ''
if has_input then
table.insert(lines, flat_fence_above(c))
local input_header = 'Input:'
local header_pad = c.inner - #input_header
table.insert(lines, '' .. input_header .. string.rep(' ', header_pad) .. '')
for _, input_line in ipairs(vim.split(tc.input, '\n', { plain = true, trimempty = false })) do
local s = input_line or ''
if #s > c.inner then
s = string.sub(s, 1, c.inner)
end
local pad = c.inner - #s
table.insert(lines, '' .. s .. string.rep(' ', pad) .. '')
end
if has_next then
table.insert(lines, flat_fence_below(c))
else
table.insert(lines, flat_bottom_border(c))
end
else
if has_next then
table.insert(lines, row_sep(c))
else
table.insert(lines, bottom_border(c))
end
end
end
return lines, highlights
end
---@param ran_test_case RanTestCase?
---@return string
function M.render_status_bar(ran_test_case)
if not ran_test_case then
return ''
end
local parts = {}
if ran_test_case.time_ms then
table.insert(parts, string.format('%.2fms', ran_test_case.time_ms))
end
if ran_test_case.code then
table.insert(parts, string.format('Exit: %d', ran_test_case.code))
end
return table.concat(parts, '')
end
---@return table<string, table>
function M.get_highlight_groups()
return {
CpTestAC = { fg = '#10b981' },
CpTestWA = { fg = '#ef4444' },
CpTestTLE = { fg = '#f59e0b' },
CpTestMLE = { fg = '#f59e0b' },
CpTestRTE = { fg = '#8b5cf6' },
CpTestNA = { fg = '#6b7280' },
CpDiffRemoved = { fg = '#ef4444', bg = '#1f1f1f' },
CpDiffAdded = { fg = '#10b981', bg = '#1f1f1f' },
}
end
function M.setup_highlights()
local groups = M.get_highlight_groups()
for name, opts in pairs(groups) do
vim.api.nvim_set_hl(0, name, opts)
end
end
return M