feat: refactor file structure

This commit is contained in:
Barrett Ruth 2025-09-21 14:29:01 -04:00
parent 9761cded88
commit 965e47a1df
17 changed files with 19 additions and 22 deletions

307
lua/cp/runner/execute.lua Normal file
View file

@ -0,0 +1,307 @@
---@class ExecuteResult
---@field stdout string
---@field stderr string
---@field code integer
---@field time_ms number
---@field timed_out boolean
local M = {}
local logger = require('cp.log')
local constants = require('cp.constants')
local filetype_to_language = constants.filetype_to_language
---@param source_file string
---@param contest_config table
---@return string
local function get_language_from_file(source_file, contest_config)
vim.validate({
source_file = { source_file, 'string' },
contest_config = { contest_config, 'table' },
})
local extension = vim.fn.fnamemodify(source_file, ':e')
local language = filetype_to_language[extension] or contest_config.default_language
return language
end
---@param cmd_template string[]
---@param substitutions table<string, string>
---@return string[]
local function substitute_template(cmd_template, substitutions)
vim.validate({
cmd_template = { cmd_template, 'table' },
substitutions = { substitutions, 'table' },
})
local result = {}
for _, arg in ipairs(cmd_template) do
local substituted = arg
for key, value in pairs(substitutions) do
substituted = substituted:gsub('{' .. key .. '}', value)
end
table.insert(result, substituted)
end
return result
end
---@param cmd_template string[]
---@param executable? string
---@param substitutions table<string, string>
---@return string[]
local function build_command(cmd_template, executable, substitutions)
vim.validate({
cmd_template = { cmd_template, 'table' },
executable = { executable, { 'string', 'nil' }, true },
substitutions = { substitutions, 'table' },
})
local cmd = substitute_template(cmd_template, substitutions)
if executable then
table.insert(cmd, 1, executable)
end
return cmd
end
---@param language_config table
---@param substitutions table<string, string>
---@return {code: integer, stdout: string, stderr: string}
function M.compile_generic(language_config, substitutions)
vim.validate({
language_config = { language_config, 'table' },
substitutions = { substitutions, 'table' },
})
if not language_config.compile then
logger.log('no compilation step required')
return { code = 0, stderr = '' }
end
local compile_cmd = substitute_template(language_config.compile, substitutions)
local redirected_cmd = vim.deepcopy(compile_cmd)
if #redirected_cmd > 0 then
redirected_cmd[#redirected_cmd] = redirected_cmd[#redirected_cmd] .. ' 2>&1'
end
local start_time = vim.uv.hrtime()
local result = vim
.system({ 'sh', '-c', table.concat(redirected_cmd, ' ') }, { text = false })
:wait()
local compile_time = (vim.uv.hrtime() - start_time) / 1000000
local ansi = require('cp.ui.ansi')
result.stdout = ansi.bytes_to_string(result.stdout or '')
result.stderr = ansi.bytes_to_string(result.stderr or '')
if result.code == 0 then
logger.log(('compilation successful (%.1fms)'):format(compile_time))
else
logger.log(('compilation failed (%.1fms)'):format(compile_time))
end
return result
end
---@param cmd string[]
---@param input_data string
---@param timeout_ms number
---@return ExecuteResult
local function execute_command(cmd, input_data, timeout_ms)
vim.validate({
cmd = { cmd, 'table' },
input_data = { input_data, 'string' },
timeout_ms = { timeout_ms, 'number' },
})
local redirected_cmd = vim.deepcopy(cmd)
if #redirected_cmd > 0 then
redirected_cmd[#redirected_cmd] = redirected_cmd[#redirected_cmd] .. ' 2>&1'
end
local start_time = vim.uv.hrtime()
local result = vim
.system({ 'sh', '-c', table.concat(redirected_cmd, ' ') }, {
stdin = input_data,
timeout = timeout_ms,
text = true,
})
:wait()
local end_time = vim.uv.hrtime()
local execution_time = (end_time - start_time) / 1000000
local actual_code = result.code or 0
if result.code == 124 then
logger.log(('execution timed out after %.1fms'):format(execution_time), vim.log.levels.WARN)
elseif actual_code ~= 0 then
logger.log(
('execution failed (exit code %d, %.1fms)'):format(actual_code, execution_time),
vim.log.levels.WARN
)
else
logger.log(('execution successful (%.1fms)'):format(execution_time))
end
return {
stdout = result.stdout or '',
stderr = result.stderr or '',
code = actual_code,
time_ms = execution_time,
timed_out = result.code == 124,
}
end
---@param exec_result ExecuteResult
---@param expected_file string
---@param is_debug boolean
---@return string
local function format_output(exec_result, expected_file, is_debug)
vim.validate({
exec_result = { exec_result, 'table' },
expected_file = { expected_file, 'string' },
is_debug = { is_debug, 'boolean' },
})
local output_lines = { exec_result.stdout }
local metadata_lines = {}
if exec_result.timed_out then
table.insert(metadata_lines, '[code]: 124 (TIMEOUT)')
elseif exec_result.code >= 128 then
local signal_name = constants.signal_codes[exec_result.code] or 'SIGNAL'
table.insert(metadata_lines, ('[code]: %d (%s)'):format(exec_result.code, signal_name))
else
table.insert(metadata_lines, ('[code]: %d'):format(exec_result.code))
end
table.insert(metadata_lines, ('[time]: %.2f ms'):format(exec_result.time_ms))
table.insert(metadata_lines, ('[debug]: %s'):format(is_debug and 'true' or 'false'))
if vim.fn.filereadable(expected_file) == 1 and exec_result.code == 0 then
local expected_content = vim.fn.readfile(expected_file)
local actual_lines = vim.split(exec_result.stdout, '\n')
while #actual_lines > 0 and actual_lines[#actual_lines] == '' do
table.remove(actual_lines)
end
local ok = #actual_lines == #expected_content
if ok then
for i, line in ipairs(actual_lines) do
if line ~= expected_content[i] then
ok = false
break
end
end
end
table.insert(metadata_lines, ('[ok]: %s'):format(ok and 'true' or 'false'))
end
return table.concat(output_lines, '') .. '\n' .. table.concat(metadata_lines, '\n')
end
---@param ctx ProblemContext
---@param contest_config ContestConfig
---@param is_debug? boolean
---@return {success: boolean, output: string?}
function M.compile_problem(ctx, contest_config, is_debug)
vim.validate({
ctx = { ctx, 'table' },
contest_config = { contest_config, 'table' },
})
local language = get_language_from_file(ctx.source_file, contest_config)
local language_config = contest_config[language]
if not language_config then
logger.log('No configuration for language: ' .. language, vim.log.levels.ERROR)
return { success = false, output = 'No configuration for language: ' .. language }
end
local substitutions = {
source = ctx.source_file,
binary = ctx.binary_file,
version = tostring(language_config.version),
}
local compile_cmd = (is_debug and language_config.debug) and language_config.debug
or language_config.compile
if compile_cmd then
language_config.compile = compile_cmd
local compile_result = M.compile_generic(language_config, substitutions)
if compile_result.code ~= 0 then
return { success = false, output = compile_result.stdout or 'unknown error' }
end
logger.log(('compilation successful (%s)'):format(is_debug and 'debug mode' or 'test mode'))
end
return { success = true, output = nil }
end
function M.run_problem(ctx, contest_config, is_debug)
vim.validate({
ctx = { ctx, 'table' },
contest_config = { contest_config, 'table' },
is_debug = { is_debug, 'boolean' },
})
vim.system({ 'mkdir', '-p', 'build', 'io' }):wait()
local language = get_language_from_file(ctx.source_file, contest_config)
local language_config = contest_config[language]
if not language_config then
vim.fn.writefile({ 'Error: No configuration for language: ' .. language }, ctx.output_file)
return
end
local substitutions = {
source = ctx.source_file,
binary = ctx.binary_file,
version = tostring(language_config.version),
}
local compile_cmd = is_debug and language_config.debug or language_config.compile
if compile_cmd then
local compile_result = M.compile_generic(language_config, substitutions)
if compile_result.code ~= 0 then
vim.fn.writefile({ compile_result.stderr }, ctx.output_file)
return
end
end
local input_data = ''
if vim.fn.filereadable(ctx.input_file) == 1 then
input_data = table.concat(vim.fn.readfile(ctx.input_file), '\n') .. '\n'
end
local cache = require('cp.cache')
cache.load()
local timeout_ms, _ = cache.get_constraints(ctx.contest, ctx.contest_id, ctx.problem_id)
timeout_ms = timeout_ms or 2000
local run_cmd = build_command(language_config.test, language_config.executable, substitutions)
local exec_result = execute_command(run_cmd, input_data, timeout_ms)
local formatted_output = format_output(exec_result, ctx.expected_file, is_debug)
local output_buf = vim.fn.bufnr(ctx.output_file)
if output_buf ~= -1 then
local was_modifiable = vim.api.nvim_get_option_value('modifiable', { buf = output_buf })
local was_readonly = vim.api.nvim_get_option_value('readonly', { buf = output_buf })
vim.api.nvim_set_option_value('readonly', false, { buf = output_buf })
vim.api.nvim_set_option_value('modifiable', true, { buf = output_buf })
vim.api.nvim_buf_set_lines(output_buf, 0, -1, false, vim.split(formatted_output, '\n'))
vim.api.nvim_set_option_value('modifiable', was_modifiable, { buf = output_buf })
vim.api.nvim_set_option_value('readonly', was_readonly, { buf = output_buf })
vim.api.nvim_buf_call(output_buf, function()
vim.cmd.write()
end)
else
vim.fn.writefile(vim.split(formatted_output, '\n'), ctx.output_file)
end
end
return M

396
lua/cp/runner/run.lua Normal file
View file

@ -0,0 +1,396 @@
---@class TestCase
---@field index number
---@field input string
---@field expected string
---@field status "pending"|"pass"|"fail"|"running"|"timeout"
---@field actual string?
---@field actual_highlights table[]?
---@field time_ms number?
---@field error string?
---@field stderr string?
---@field selected boolean
---@field code number?
---@field ok boolean?
---@field signal string?
---@field timed_out boolean?
---@class ProblemConstraints
---@field timeout_ms number
---@field memory_mb number
---@class RunPanelState
---@field test_cases TestCase[]
---@field current_index number
---@field buffer number?
---@field namespace number?
---@field is_active boolean
---@field saved_layout table?
---@field constraints ProblemConstraints?
local M = {}
local constants = require('cp.constants')
local logger = require('cp.log')
---@type RunPanelState
local run_panel_state = {
test_cases = {},
current_index = 1,
buffer = nil,
namespace = nil,
is_active = false,
saved_layout = nil,
constraints = nil,
}
---@param index number
---@param input string
---@param expected string
---@return TestCase
local function create_test_case(index, input, expected)
return {
index = index,
input = input,
expected = expected,
status = 'pending',
actual = nil,
time_ms = nil,
error = nil,
selected = true,
}
end
---@param platform string
---@param contest_id string
---@param problem_id string?
---@return TestCase[]
local function parse_test_cases_from_cache(platform, contest_id, problem_id)
local cache = require('cp.cache')
cache.load()
local cached_test_cases = cache.get_test_cases(platform, contest_id, problem_id)
if not cached_test_cases or #cached_test_cases == 0 then
return {}
end
local test_cases = {}
for i, test_case in ipairs(cached_test_cases) do
local index = test_case.index or i
local expected = test_case.expected or test_case.output or ''
table.insert(test_cases, create_test_case(index, test_case.input, expected))
end
return test_cases
end
---@param input_file string
---@param expected_file string
---@return TestCase[]
local function parse_test_cases_from_files(input_file, expected_file)
if vim.fn.filereadable(input_file) == 0 or vim.fn.filereadable(expected_file) == 0 then
return {}
end
local base_name = vim.fn.fnamemodify(input_file, ':r')
local test_cases = {}
local i = 1
while true do
local individual_input_file = base_name .. '.' .. i .. '.cpin'
local individual_expected_file = base_name .. '.' .. i .. '.cpout'
if
vim.fn.filereadable(individual_input_file) == 1
and vim.fn.filereadable(individual_expected_file) == 1
then
local input_content = table.concat(vim.fn.readfile(individual_input_file), '\n')
local expected_content = table.concat(vim.fn.readfile(individual_expected_file), '\n')
table.insert(test_cases, create_test_case(i, input_content, expected_content))
i = i + 1
else
break
end
end
if #test_cases == 0 then
local input_content = table.concat(vim.fn.readfile(input_file), '\n')
local expected_content = table.concat(vim.fn.readfile(expected_file), '\n')
return { create_test_case(1, input_content, expected_content) }
end
return test_cases
end
---@param platform string
---@param contest_id string
---@param problem_id string?
---@return ProblemConstraints?
local function load_constraints_from_cache(platform, contest_id, problem_id)
local cache = require('cp.cache')
cache.load()
local timeout_ms, memory_mb = cache.get_constraints(platform, contest_id, problem_id)
if timeout_ms and memory_mb then
return {
timeout_ms = timeout_ms,
memory_mb = memory_mb,
}
end
return nil
end
---@param ctx ProblemContext
---@param contest_config ContestConfig
---@param test_case TestCase
---@return table
local function run_single_test_case(ctx, contest_config, cp_config, test_case)
local language = vim.fn.fnamemodify(ctx.source_file, ':e')
local language_name = constants.filetype_to_language[language] or contest_config.default_language
local language_config = contest_config[language_name]
if not language_config then
return {
status = 'fail',
actual = '',
error = 'No language configuration',
time_ms = 0,
}
end
local function substitute_template(cmd_template, substitutions)
local result = {}
for _, arg in ipairs(cmd_template) do
local substituted = arg
for key, value in pairs(substitutions) do
substituted = substituted:gsub('{' .. key .. '}', value)
end
table.insert(result, substituted)
end
return result
end
local function build_command(cmd_template, executable, substitutions)
local cmd = substitute_template(cmd_template, substitutions)
if executable then
table.insert(cmd, 1, executable)
end
return cmd
end
local substitutions = {
source = ctx.source_file,
binary = ctx.binary_file,
version = tostring(language_config.version or ''),
}
if language_config.compile and vim.fn.filereadable(ctx.binary_file) == 0 then
logger.log('binary not found, compiling first...')
local compile_cmd = substitute_template(language_config.compile, substitutions)
local redirected_cmd = vim.deepcopy(compile_cmd)
redirected_cmd[#redirected_cmd] = redirected_cmd[#redirected_cmd] .. ' 2>&1'
local compile_result = vim
.system({ 'sh', '-c', table.concat(redirected_cmd, ' ') }, { text = false })
:wait()
local ansi = require('cp.ui.ansi')
compile_result.stdout = ansi.bytes_to_string(compile_result.stdout or '')
compile_result.stderr = ansi.bytes_to_string(compile_result.stderr or '')
if compile_result.code ~= 0 then
return {
status = 'fail',
actual = '',
error = 'Compilation failed: ' .. (compile_result.stdout or 'Unknown error'),
stderr = compile_result.stdout or '',
time_ms = 0,
code = compile_result.code,
ok = false,
signal = nil,
timed_out = false,
}
end
end
local run_cmd = build_command(language_config.test, language_config.executable, substitutions)
local stdin_content = test_case.input .. '\n'
local start_time = vim.uv.hrtime()
local timeout_ms = run_panel_state.constraints and run_panel_state.constraints.timeout_ms or 2000
if not run_panel_state.constraints then
logger.log('no problem constraints available, using default 2000ms timeout')
end
local redirected_run_cmd = vim.deepcopy(run_cmd)
redirected_run_cmd[#redirected_run_cmd] = redirected_run_cmd[#redirected_run_cmd] .. ' 2>&1'
local result = vim
.system({ 'sh', '-c', table.concat(redirected_run_cmd, ' ') }, {
stdin = stdin_content,
timeout = timeout_ms,
text = false,
})
:wait()
local execution_time = (vim.uv.hrtime() - start_time) / 1000000
local ansi = require('cp.ui.ansi')
local stdout_str = ansi.bytes_to_string(result.stdout or '')
local actual_output = stdout_str:gsub('\n$', '')
local actual_highlights = {}
if actual_output ~= '' then
if cp_config.run_panel.ansi then
local parsed = ansi.parse_ansi_text(actual_output)
actual_output = table.concat(parsed.lines, '\n')
actual_highlights = parsed.highlights
else
actual_output = actual_output:gsub('\027%[[%d;]*[a-zA-Z]', '')
end
end
local max_lines = cp_config.run_panel.max_output_lines
local output_lines = vim.split(actual_output, '\n')
if #output_lines > max_lines then
local trimmed_lines = {}
for i = 1, max_lines do
table.insert(trimmed_lines, output_lines[i])
end
table.insert(trimmed_lines, string.format('... (output trimmed after %d lines)', max_lines))
actual_output = table.concat(trimmed_lines, '\n')
end
local expected_output = test_case.expected:gsub('\n$', '')
local ok = actual_output == expected_output
local status
local timed_out = result.code == 143 or result.code == 124
if timed_out then
status = 'timeout'
elseif result.code == 0 and ok then
status = 'pass'
else
status = 'fail'
end
local signal = nil
if result.code >= 128 then
signal = constants.signal_codes[result.code]
end
return {
status = status,
actual = actual_output,
actual_highlights = actual_highlights,
error = result.code ~= 0 and actual_output or nil,
stderr = '',
time_ms = execution_time,
code = result.code,
ok = ok,
signal = signal,
timed_out = timed_out,
}
end
---@param ctx ProblemContext
---@param state table
---@return boolean
function M.load_test_cases(ctx, state)
local test_cases = parse_test_cases_from_cache(state.platform, state.contest_id, state.problem_id)
if #test_cases == 0 then
test_cases = parse_test_cases_from_files(ctx.input_file, ctx.expected_file)
end
run_panel_state.test_cases = test_cases
run_panel_state.current_index = 1
run_panel_state.constraints =
load_constraints_from_cache(state.platform, state.contest_id, state.problem_id)
local constraint_info = run_panel_state.constraints
and string.format(
' with %dms/%dMB limits',
run_panel_state.constraints.timeout_ms,
run_panel_state.constraints.memory_mb
)
or ''
logger.log(('loaded %d test case(s)%s'):format(#test_cases, constraint_info))
return #test_cases > 0
end
---@param ctx ProblemContext
---@param contest_config ContestConfig
---@param index number
---@return boolean
function M.run_test_case(ctx, contest_config, cp_config, index)
local test_case = run_panel_state.test_cases[index]
if not test_case then
return false
end
test_case.status = 'running'
local result = run_single_test_case(ctx, contest_config, cp_config, test_case)
test_case.status = result.status
test_case.actual = result.actual
test_case.actual_highlights = result.actual_highlights
test_case.error = result.error
test_case.stderr = result.stderr
test_case.time_ms = result.time_ms
test_case.code = result.code
test_case.ok = result.ok
test_case.signal = result.signal
test_case.timed_out = result.timed_out
return true
end
---@param ctx ProblemContext
---@param contest_config ContestConfig
---@return TestCase[]
function M.run_all_test_cases(ctx, contest_config, cp_config)
local results = {}
for i, _ in ipairs(run_panel_state.test_cases) do
M.run_test_case(ctx, contest_config, cp_config, i)
table.insert(results, run_panel_state.test_cases[i])
end
return results
end
---@return RunPanelState
function M.get_run_panel_state()
return run_panel_state
end
function M.handle_compilation_failure(compilation_output)
local ansi = require('cp.ui.ansi')
local config = require('cp.config').setup()
local clean_text
local highlights = {}
if config.run_panel.ansi then
local parsed = ansi.parse_ansi_text(compilation_output or '')
clean_text = table.concat(parsed.lines, '\n')
highlights = parsed.highlights
else
clean_text = (compilation_output or ''):gsub('\027%[[%d;]*[a-zA-Z]', '')
end
for _, test_case in ipairs(run_panel_state.test_cases) do
test_case.status = 'fail'
test_case.actual = clean_text
test_case.actual_highlights = highlights
test_case.error = 'Compilation failed'
test_case.stderr = ''
test_case.time_ms = 0
test_case.code = 1
test_case.ok = false
test_case.signal = nil
test_case.timed_out = false
end
end
return M

View file

@ -0,0 +1,371 @@
---@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 test_case TestCase
---@return StatusInfo
function M.get_status_info(test_case)
if test_case.status == 'pass' then
return { text = 'AC', highlight_group = 'CpTestAC' }
elseif test_case.status == 'fail' then
if test_case.timed_out then
return { text = 'TLE', highlight_group = 'CpTestTLE' }
elseif test_case.code and test_case.code >= 128 then
return { text = 'RTE', highlight_group = 'CpTestRTE' }
else
return { text = 'WA', highlight_group = 'CpTestWA' }
end
elseif test_case.status == 'timeout' then
return { text = 'TLE', highlight_group = 'CpTestTLE' }
elseif test_case.status == 'running' then
return { text = '...', highlight_group = 'CpTestPending' }
else
return { text = '', highlight_group = 'CpTestPending' }
end
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, 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)
else
timeout_str = ''
memory_str = ''
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 .. ' '))
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.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.memory + w.exit
local inner = sum + 5
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.floor(pad / 2)
return string.rep(' ', left) .. text .. string.rep(' ', pad - left)
end
local function right_align(text, width)
local content = (' %s '):format(text)
local pad = width - #content
if pad <= 0 then
return content
end
return string.rep(' ', pad) .. content
end
local function format_num_column(prefix, idx, width)
local num_str = tostring(idx)
local content
if #num_str == 1 then
content = ' ' .. prefix .. ' ' .. num_str .. ' '
else
content = ' ' .. prefix .. num_str .. ' '
end
local total_pad = width - #content
if total_pad <= 0 then
return content
end
local left_pad = math.floor(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.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.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.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.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.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('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)
else
timeout = ''
memory = ''
end
local line = ''
.. format_num_column(prefix, idx, w.num)
.. ''
.. right_align(status.text, w.status)
.. ''
.. right_align(time, w.time)
.. ''
.. right_align(timeout, w.timeout)
.. ''
.. right_align(memory, w.memory)
.. ''
.. right_align(exit, w.exit)
.. ''
local hi
if status.text ~= '' then
local status_pos = line:find(status.text)
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 RunPanelState
---@return string[], table[] 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 test_case TestCase?
---@return string
function M.render_status_bar(test_case)
if not test_case then
return ''
end
local parts = {}
if test_case.time_ms then
table.insert(parts, string.format('%.2fms', test_case.time_ms))
end
if test_case.code then
table.insert(parts, string.format('Exit: %d', 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' },
CpTestRTE = { fg = '#8b5cf6' },
CpTestPending = { 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