From ba26cee7f9812ed5b9c64bb8e0ede02d66dcac92 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Tue, 27 Jan 2026 12:55:35 -0500 Subject: [PATCH] feat(run): make running entirely asynchronous --- lua/cp/runner/execute.lua | 129 ++++++----- lua/cp/runner/run.lua | 198 ++++++++-------- lua/cp/runner/run_render.lua | 8 +- lua/cp/ui/views.lua | 427 ++++++++++++++++++----------------- 4 files changed, 404 insertions(+), 358 deletions(-) diff --git a/lua/cp/runner/execute.lua b/lua/cp/runner/execute.lua index c1c141e..4cf330b 100644 --- a/lua/cp/runner/execute.lua +++ b/lua/cp/runner/execute.lua @@ -39,24 +39,27 @@ end ---@param compile_cmd string[] ---@param substitutions SubstitutableCommand -function M.compile(compile_cmd, substitutions) +---@param on_complete fun(r: {code: integer, stdout: string}) +function M.compile(compile_cmd, substitutions, on_complete) local cmd = substitute_template(compile_cmd, substitutions) local sh = table.concat(cmd, ' ') .. ' 2>&1' local t0 = vim.uv.hrtime() - local r = vim.system({ 'sh', '-c', sh }, { text = false }):wait() - local dt = (vim.uv.hrtime() - t0) / 1e6 + vim.system({ 'sh', '-c', sh }, { text = false }, function(r) + local dt = (vim.uv.hrtime() - t0) / 1e6 + local ansi = require('cp.ui.ansi') + r.stdout = ansi.bytes_to_string(r.stdout or '') - local ansi = require('cp.ui.ansi') - r.stdout = ansi.bytes_to_string(r.stdout or '') + if r.code == 0 then + logger.log(('Compilation successful in %.1fms.'):format(dt), vim.log.levels.INFO) + else + logger.log(('Compilation failed in %.1fms.'):format(dt)) + end - if r.code == 0 then - logger.log(('Compilation successful in %.1fms.'):format(dt), vim.log.levels.INFO) - else - logger.log(('Compilation failed in %.1fms.'):format(dt)) - end - - return r + vim.schedule(function() + on_complete(r) + end) + end) end local function parse_and_strip_time_v(output) @@ -103,7 +106,8 @@ local function parse_and_strip_time_v(output) return head, peak_mb end -function M.run(cmd, stdin, timeout_ms, memory_mb) +---@param on_complete fun(result: ExecuteResult) +function M.run(cmd, stdin, timeout_ms, memory_mb, on_complete) local time_bin = utils.time_path() local timeout_bin = utils.timeout_path() @@ -117,56 +121,56 @@ function M.run(cmd, stdin, timeout_ms, memory_mb) local sh = prefix .. timeout_prefix .. ('%s -v sh -c %q 2>&1'):format(time_bin, prog) local t0 = vim.uv.hrtime() - local r = vim - .system({ 'sh', '-c', sh }, { - stdin = stdin, - text = true, - }) - :wait() - local dt = (vim.uv.hrtime() - t0) / 1e6 + vim.system({ 'sh', '-c', sh }, { stdin = stdin, text = true }, function(r) + local dt = (vim.uv.hrtime() - t0) / 1e6 - local code = r.code or 0 - local raw = r.stdout or '' - local cleaned, peak_mb = parse_and_strip_time_v(raw) - local tled = code == 124 + local code = r.code or 0 + local raw = r.stdout or '' + local cleaned, peak_mb = parse_and_strip_time_v(raw) + local tled = code == 124 - local signal = nil - if code >= 128 then - signal = constants.signal_codes[code] - end + local signal = nil + if code >= 128 then + signal = constants.signal_codes[code] + end - local lower = (cleaned or ''):lower() - local oom_hint = lower:find('std::bad_alloc', 1, true) - or lower:find('cannot allocate memory', 1, true) - or lower:find('out of memory', 1, true) - or lower:find('oom', 1, true) - or lower:find('enomem', 1, true) - local near_cap = peak_mb >= (0.90 * memory_mb) + local lower = (cleaned or ''):lower() + local oom_hint = lower:find('std::bad_alloc', 1, true) + or lower:find('cannot allocate memory', 1, true) + or lower:find('out of memory', 1, true) + or lower:find('oom', 1, true) + or lower:find('enomem', 1, true) + local near_cap = peak_mb >= (0.90 * memory_mb) - local mled = (peak_mb >= memory_mb) or near_cap or (oom_hint and not tled) + local mled = (peak_mb >= memory_mb) or near_cap or (oom_hint and not tled) - if tled then - logger.log(('Execution timed out in %.1fms.'):format(dt)) - elseif mled then - logger.log(('Execution memory limit exceeded in %.1fms.'):format(dt)) - elseif code ~= 0 then - logger.log(('Execution failed in %.1fms (exit code %d).'):format(dt, code)) - else - logger.log(('Execution successful in %.1fms.'):format(dt)) - end + if tled then + logger.log(('Execution timed out in %.1fms.'):format(dt)) + elseif mled then + logger.log(('Execution memory limit exceeded in %.1fms.'):format(dt)) + elseif code ~= 0 then + logger.log(('Execution failed in %.1fms (exit code %d).'):format(dt, code)) + else + logger.log(('Execution successful in %.1fms.'):format(dt)) + end - return { - stdout = cleaned, - code = code, - time_ms = dt, - tled = tled, - mled = mled, - peak_mb = peak_mb, - signal = signal, - } + vim.schedule(function() + on_complete({ + stdout = cleaned, + code = code, + time_ms = dt, + tled = tled, + mled = mled, + peak_mb = peak_mb, + signal = signal, + }) + end) + end) end -function M.compile_problem(debug) +---@param debug boolean? +---@param on_complete fun(result: {success: boolean, output: string?}) +function M.compile_problem(debug, on_complete) local state = require('cp.state') local config = require('cp.config').get_config() local platform = state.get_platform() @@ -176,17 +180,20 @@ function M.compile_problem(debug) local compile_config = (debug and eff.commands.debug) or eff.commands.build if not compile_config then - return { success = true, output = nil } + on_complete({ success = true, output = nil }) + return end local binary = debug and state.get_debug_file() or state.get_binary_file() local substitutions = { source = state.get_source_file(), binary = binary } - local r = M.compile(compile_config, substitutions) - if r.code ~= 0 then - return { success = false, output = r.stdout or 'unknown error' } - end - return { success = true, output = nil } + M.compile(compile_config, substitutions, function(r) + if r.code ~= 0 then + on_complete({ success = false, output = r.stdout or 'unknown error' }) + else + on_complete({ success = true, output = nil }) + end + end) end return M diff --git a/lua/cp/runner/run.lua b/lua/cp/runner/run.lua index 80024a8..1fe1b5f 100644 --- a/lua/cp/runner/run.lua +++ b/lua/cp/runner/run.lua @@ -101,8 +101,8 @@ end ---@param test_case RanTestCase ---@param debug boolean? ----@return { status: "pass"|"fail"|"tle"|"mle", actual: string, actual_highlights: Highlight[], error: string, stderr: string, time_ms: number, code: integer, ok: boolean, signal: string, tled: boolean, mled: boolean, rss_mb: number } -local function run_single_test_case(test_case, debug) +---@param on_complete fun(result: { status: "pass"|"fail"|"tle"|"mle", actual: string, actual_highlights: Highlight[], error: string, stderr: string, time_ms: number, code: integer, ok: boolean, signal: string, tled: boolean, mled: boolean, rss_mb: number }) +local function run_single_test_case(test_case, debug, on_complete) local source_file = state.get_source_file() local binary_file = debug and state.get_debug_file() or state.get_binary_file() @@ -117,65 +117,65 @@ local function run_single_test_case(test_case, debug) local timeout_ms = (panel_state.constraints and panel_state.constraints.timeout_ms) or 0 local memory_mb = panel_state.constraints and panel_state.constraints.memory_mb or 0 - local r = execute.run(cmd, stdin_content, timeout_ms, memory_mb) + execute.run(cmd, stdin_content, timeout_ms, memory_mb, function(r) + local ansi = require('cp.ui.ansi') + local out = r.stdout or '' + local highlights = {} + if out ~= '' then + if config.ui.ansi then + local parsed = ansi.parse_ansi_text(out) + out = table.concat(parsed.lines, '\n') + highlights = parsed.highlights + else + out = out:gsub('\027%[[%d;]*[a-zA-Z]', '') + end + end - local ansi = require('cp.ui.ansi') - local out = r.stdout or '' - local highlights = {} - if out ~= '' then - if config.ui.ansi then - local parsed = ansi.parse_ansi_text(out) - out = table.concat(parsed.lines, '\n') - highlights = parsed.highlights + local max_lines = config.ui.panel.max_output_lines + local lines = vim.split(out, '\n') + if #lines > max_lines then + local trimmed = {} + for i = 1, max_lines do + table.insert(trimmed, lines[i]) + end + table.insert(trimmed, string.format('... (output trimmed after %d lines)', max_lines)) + out = table.concat(trimmed, '\n') + end + + local expected = test_case.expected or '' + local ok = normalize_lines(out) == normalize_lines(expected) + + local signal = r.signal + if not signal and r.code and r.code >= 128 then + signal = constants.signal_codes[r.code] + end + + local status + if r.tled then + status = 'tle' + elseif r.mled then + status = 'mle' + elseif ok then + status = 'pass' else - out = out:gsub('\027%[[%d;]*[a-zA-Z]', '') + status = 'fail' end - end - local max_lines = config.ui.panel.max_output_lines - local lines = vim.split(out, '\n') - if #lines > max_lines then - local trimmed = {} - for i = 1, max_lines do - table.insert(trimmed, lines[i]) - end - table.insert(trimmed, string.format('... (output trimmed after %d lines)', max_lines)) - out = table.concat(trimmed, '\n') - end - - local expected = test_case.expected or '' - local ok = normalize_lines(out) == normalize_lines(expected) - - local signal = r.signal - if not signal and r.code and r.code >= 128 then - signal = constants.signal_codes[r.code] - end - - local status - if r.tled then - status = 'tle' - elseif r.mled then - status = 'mle' - elseif ok then - status = 'pass' - else - status = 'fail' - end - - return { - status = status, - actual = out, - actual_highlights = highlights, - error = (r.code ~= 0 and not ok) and out or '', - stderr = '', - time_ms = r.time_ms, - code = r.code, - ok = ok, - signal = signal, - tled = r.tled or false, - mled = r.mled or false, - rss_mb = r.peak_mb or 0, - } + on_complete({ + status = status, + actual = out, + actual_highlights = highlights, + error = (r.code ~= 0 and not ok) and out or '', + stderr = '', + time_ms = r.time_ms, + code = r.code, + ok = ok, + signal = signal, + tled = r.tled or false, + mled = r.mled or false, + rss_mb = r.peak_mb or 0, + }) + end) end ---@return boolean @@ -199,8 +199,8 @@ function M.load_test_cases() end ---@param debug boolean? ----@return RanTestCase? -function M.run_combined_test(debug) +---@param on_complete fun(result: RanTestCase?) +function M.run_combined_test(debug, on_complete) local combined = cache.get_combined_test( state.get_platform() or '', state.get_contest_id() or '', @@ -209,7 +209,8 @@ function M.run_combined_test(debug) if not combined then logger.log('No combined test found', vim.log.levels.ERROR) - return nil + on_complete(nil) + return end local ran_test = { @@ -228,42 +229,45 @@ function M.run_combined_test(debug) selected = true, } - local result = run_single_test_case(ran_test, debug) - return result + run_single_test_case(ran_test, debug, function(result) + on_complete(result) + end) end ---@param index number ---@param debug boolean? ----@return boolean -function M.run_test_case(index, debug) +---@param on_complete fun(success: boolean) +function M.run_test_case(index, debug, on_complete) local tc = panel_state.test_cases[index] if not tc then - return false + on_complete(false) + return end tc.status = 'running' - local r = run_single_test_case(tc, debug) + run_single_test_case(tc, debug, function(r) + tc.status = r.status + tc.actual = r.actual + tc.actual_highlights = r.actual_highlights + tc.error = r.error + tc.stderr = r.stderr + tc.time_ms = r.time_ms + tc.code = r.code + tc.ok = r.ok + tc.signal = r.signal + tc.tled = r.tled + tc.mled = r.mled + tc.rss_mb = r.rss_mb - tc.status = r.status - tc.actual = r.actual - tc.actual_highlights = r.actual_highlights - tc.error = r.error - tc.stderr = r.stderr - tc.time_ms = r.time_ms - tc.code = r.code - tc.ok = r.ok - tc.signal = r.signal - tc.tled = r.tled - tc.mled = r.mled - tc.rss_mb = r.rss_mb - - return true + on_complete(true) + end) end ---@param indices? integer[] ---@param debug boolean? ----@return RanTestCase[] -function M.run_all_test_cases(indices, debug) +---@param on_each? fun(index: integer, total: integer) +---@param on_done fun(results: RanTestCase[]) +function M.run_all_test_cases(indices, debug, on_each, on_done) local to_run = indices if not to_run then to_run = {} @@ -272,20 +276,26 @@ function M.run_all_test_cases(indices, debug) end end - for _, i in ipairs(to_run) do - M.run_test_case(i, debug) + local function run_next(pos) + if pos > #to_run then + logger.log( + ('Finished %s %d test cases.'):format(debug and 'debugging' or 'running', #to_run), + vim.log.levels.INFO, + true + ) + on_done(panel_state.test_cases) + return + end + + M.run_test_case(to_run[pos], debug, function() + if on_each then + on_each(pos, #to_run) + end + run_next(pos + 1) + end) end - logger.log( - ('Finished %s %s test cases.'):format( - debug and 'debugging' or 'running', - #panel_state.test_cases - ), - vim.log.levels.INFO, - true - ) - - return panel_state.test_cases + run_next(1) end ---@return PanelState diff --git a/lua/cp/runner/run_render.lua b/lua/cp/runner/run_render.lua index 714ecd3..344370c 100644 --- a/lua/cp/runner/run_render.lua +++ b/lua/cp/runner/run_render.lua @@ -26,6 +26,12 @@ local exit_code_names = { ---@param ran_test_case RanTestCase ---@return StatusInfo function M.get_status_info(ran_test_case) + if ran_test_case.status == 'pending' then + return { text = 'PEND', highlight_group = 'CpTestNA' } + elseif ran_test_case.status == 'running' then + return { text = 'RUN', highlight_group = 'CpTestNA' } + end + if ran_test_case.ok then return { text = 'AC', highlight_group = 'CpTestAC' } end @@ -34,7 +40,7 @@ function M.get_status_info(ran_test_case) 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 + elseif ran_test_case.code 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' } diff --git a/lua/cp/ui/views.lua b/lua/cp/ui/views.lua index 29a3f46..b333b86 100644 --- a/lua/cp/ui/views.lua +++ b/lua/cp/ui/views.lua @@ -464,6 +464,158 @@ function M.ensure_io_view() end end +local function render_io_view_results(io_state, test_indices, mode, combined_result, combined_input) + local run = require('cp.runner.run') + local run_render = require('cp.runner.run_render') + local cfg = config_module.get_config() + + run_render.setup_highlights() + + local input_lines = {} + local output_lines = {} + local verdict_lines = {} + local verdict_highlights = {} + local formatter = cfg.ui.run.format_verdict + local test_state = run.get_panel_state() + + if mode == 'combined' and combined_result then + input_lines = vim.split(combined_input, '\n') + + if combined_result.actual and combined_result.actual ~= '' then + output_lines = vim.split(combined_result.actual, '\n') + end + + local status = run_render.get_status_info(combined_result) + local format_data = { + index = 1, + status = status, + time_ms = combined_result.time_ms or 0, + time_limit_ms = test_state.constraints and test_state.constraints.timeout_ms or 0, + memory_mb = combined_result.rss_mb or 0, + memory_limit_mb = test_state.constraints and test_state.constraints.memory_mb or 0, + exit_code = combined_result.code or 0, + signal = (combined_result.code and combined_result.code >= 128) + and require('cp.constants').signal_codes[combined_result.code] + or nil, + time_actual_width = #string.format('%.2f', combined_result.time_ms or 0), + time_limit_width = #tostring( + test_state.constraints and test_state.constraints.timeout_ms or 0 + ), + mem_actual_width = #string.format('%.0f', combined_result.rss_mb or 0), + mem_limit_width = #string.format( + '%.0f', + test_state.constraints and test_state.constraints.memory_mb or 0 + ), + } + + local verdict_result = formatter(format_data) + table.insert(verdict_lines, verdict_result.line) + + if verdict_result.highlights then + for _, hl in ipairs(verdict_result.highlights) do + table.insert(verdict_highlights, { + line_offset = #verdict_lines - 1, + col_start = hl.col_start, + col_end = hl.col_end, + group = hl.group, + }) + end + end + else + local max_time_actual, max_time_limit, max_mem_actual, max_mem_limit = 0, 0, 0, 0 + + for _, idx in ipairs(test_indices) do + local tc = test_state.test_cases[idx] + max_time_actual = math.max(max_time_actual, #string.format('%.2f', tc.time_ms or 0)) + max_time_limit = math.max( + max_time_limit, + #tostring(test_state.constraints and test_state.constraints.timeout_ms or 0) + ) + max_mem_actual = math.max(max_mem_actual, #string.format('%.0f', tc.rss_mb or 0)) + max_mem_limit = math.max( + max_mem_limit, + #string.format('%.0f', test_state.constraints and test_state.constraints.memory_mb or 0) + ) + end + + local all_outputs = {} + for _, idx in ipairs(test_indices) do + local tc = test_state.test_cases[idx] + for _, line in ipairs(vim.split(tc.input, '\n')) do + table.insert(input_lines, line) + end + if tc.actual then + table.insert(all_outputs, tc.actual) + end + end + + local combined_output = table.concat(all_outputs, '') + if combined_output ~= '' then + for _, line in ipairs(vim.split(combined_output, '\n')) do + table.insert(output_lines, line) + end + end + + for _, idx in ipairs(test_indices) do + local tc = test_state.test_cases[idx] + local status = run_render.get_status_info(tc) + + local format_data = { + index = idx, + status = status, + time_ms = tc.time_ms or 0, + time_limit_ms = test_state.constraints and test_state.constraints.timeout_ms or 0, + memory_mb = tc.rss_mb or 0, + memory_limit_mb = test_state.constraints and test_state.constraints.memory_mb or 0, + exit_code = tc.code or 0, + signal = (tc.code and tc.code >= 128) and require('cp.constants').signal_codes[tc.code] + or nil, + time_actual_width = max_time_actual, + time_limit_width = max_time_limit, + mem_actual_width = max_mem_actual, + mem_limit_width = max_mem_limit, + } + + local result = formatter(format_data) + table.insert(verdict_lines, result.line) + + if result.highlights then + for _, hl in ipairs(result.highlights) do + table.insert(verdict_highlights, { + line_offset = #verdict_lines - 1, + col_start = hl.col_start, + col_end = hl.col_end, + group = hl.group, + }) + end + end + end + end + + if #output_lines > 0 and #verdict_lines > 0 then + table.insert(output_lines, '') + end + + local verdict_start = #output_lines + for _, line in ipairs(verdict_lines) do + table.insert(output_lines, line) + end + + local final_highlights = {} + for _, vh in ipairs(verdict_highlights) do + table.insert(final_highlights, { + line = verdict_start + vh.line_offset, + col_start = vh.col_start, + col_end = vh.col_end, + highlight_group = vh.group, + }) + end + + utils.update_buffer_content(io_state.input_buf, input_lines, nil, nil) + local output_ns = vim.api.nvim_create_namespace('cp_io_view_output') + utils.update_buffer_content(io_state.output_buf, output_lines, final_highlights, output_ns) +end + function M.run_io_view(test_indices_arg, debug, mode) logger.log(('%s tests...'):format(debug and 'Debugging' or 'Running'), vim.log.levels.INFO, true) @@ -544,208 +696,65 @@ function M.run_io_view(test_indices_arg, debug, mode) return end - local config = config_module.get_config() + local cfg = config_module.get_config() - if config.ui.ansi then + if cfg.ui.ansi then require('cp.ui.ansi').setup_highlight_groups() end local execute = require('cp.runner.execute') - local compile_result = execute.compile_problem(debug) - if not compile_result.success then - local ansi = require('cp.ui.ansi') - local output = compile_result.output or '' - local lines, highlights - if config.ui.ansi then - local parsed = ansi.parse_ansi_text(output) - lines = parsed.lines - highlights = parsed.highlights - else - lines = vim.split(output:gsub('\027%[[%d;]*[a-zA-Z]', ''), '\n') - highlights = {} - end - - local ns = vim.api.nvim_create_namespace('cp_io_view_compile_error') - utils.update_buffer_content(io_state.output_buf, lines, highlights, ns) - return - end - - local run_render = require('cp.runner.run_render') - run_render.setup_highlights() - - local input_lines = {} - local output_lines = {} - local verdict_lines = {} - local verdict_highlights = {} - - local formatter = config.ui.run.format_verdict - - if mode == 'combined' then - local combined = cache.get_combined_test(platform, contest_id, problem_id) - - if not combined then - logger.log('No combined test found', vim.log.levels.ERROR) + execute.compile_problem(debug, function(compile_result) + if not vim.api.nvim_buf_is_valid(io_state.output_buf) then return end - run.load_test_cases() + if not compile_result.success then + local ansi = require('cp.ui.ansi') + local output = compile_result.output or '' + local lines, highlights - local result = run.run_combined_test(debug) + if cfg.ui.ansi then + local parsed = ansi.parse_ansi_text(output) + lines = parsed.lines + highlights = parsed.highlights + else + lines = vim.split(output:gsub('\027%[[%d;]*[a-zA-Z]', ''), '\n') + highlights = {} + end - if not result then - logger.log('Failed to run combined test', vim.log.levels.ERROR) + local ns = vim.api.nvim_create_namespace('cp_io_view_compile_error') + utils.update_buffer_content(io_state.output_buf, lines, highlights, ns) return end - input_lines = vim.split(combined.input, '\n') - - if result.actual and result.actual ~= '' then - output_lines = vim.split(result.actual, '\n') - end - - local status = run_render.get_status_info(result) - local test_state = run.get_panel_state() - - ---@type VerdictFormatData - local format_data = { - index = 1, - status = status, - time_ms = result.time_ms or 0, - time_limit_ms = test_state.constraints and test_state.constraints.timeout_ms or 0, - memory_mb = result.rss_mb or 0, - memory_limit_mb = test_state.constraints and test_state.constraints.memory_mb or 0, - exit_code = result.code or 0, - signal = (result.code and result.code >= 128) - and require('cp.constants').signal_codes[result.code] - or nil, - time_actual_width = #string.format('%.2f', result.time_ms or 0), - time_limit_width = #tostring( - test_state.constraints and test_state.constraints.timeout_ms or 0 - ), - mem_actual_width = #string.format('%.0f', result.rss_mb or 0), - mem_limit_width = #string.format( - '%.0f', - test_state.constraints and test_state.constraints.memory_mb or 0 - ), - } - - local verdict_result = formatter(format_data) - table.insert(verdict_lines, verdict_result.line) - - if verdict_result.highlights then - for _, hl in ipairs(verdict_result.highlights) do - table.insert(verdict_highlights, { - line_offset = #verdict_lines - 1, - col_start = hl.col_start, - col_end = hl.col_end, - group = hl.group, - }) - end - end - else - run.run_all_test_cases(test_indices, debug) - local test_state = run.get_panel_state() - - local max_time_actual = 0 - local max_time_limit = 0 - local max_mem_actual = 0 - local max_mem_limit = 0 - - for _, idx in ipairs(test_indices) do - local tc = test_state.test_cases[idx] - max_time_actual = math.max(max_time_actual, #string.format('%.2f', tc.time_ms or 0)) - max_time_limit = math.max( - max_time_limit, - #tostring(test_state.constraints and test_state.constraints.timeout_ms or 0) - ) - max_mem_actual = math.max(max_mem_actual, #string.format('%.0f', tc.rss_mb or 0)) - max_mem_limit = math.max( - max_mem_limit, - #string.format('%.0f', test_state.constraints and test_state.constraints.memory_mb or 0) - ) - end - - local all_outputs = {} - for _, idx in ipairs(test_indices) do - local tc = test_state.test_cases[idx] - - for _, line in ipairs(vim.split(tc.input, '\n')) do - table.insert(input_lines, line) + if mode == 'combined' then + local combined = cache.get_combined_test(platform, contest_id, problem_id) + if not combined then + logger.log('No combined test found', vim.log.levels.ERROR) + return end - if tc.actual then - table.insert(all_outputs, tc.actual) - end - end + run.load_test_cases() - local combined_output = table.concat(all_outputs, '') - if combined_output ~= '' then - for _, line in ipairs(vim.split(combined_output, '\n')) do - table.insert(output_lines, line) - end - end - - for _, idx in ipairs(test_indices) do - local tc = test_state.test_cases[idx] - local status = run_render.get_status_info(tc) - - ---@type VerdictFormatData - local format_data = { - index = idx, - status = status, - time_ms = tc.time_ms or 0, - time_limit_ms = test_state.constraints and test_state.constraints.timeout_ms or 0, - memory_mb = tc.rss_mb or 0, - memory_limit_mb = test_state.constraints and test_state.constraints.memory_mb or 0, - exit_code = tc.code or 0, - signal = (tc.code and tc.code >= 128) and require('cp.constants').signal_codes[tc.code] - or nil, - time_actual_width = max_time_actual, - time_limit_width = max_time_limit, - mem_actual_width = max_mem_actual, - mem_limit_width = max_mem_limit, - } - - local result = formatter(format_data) - table.insert(verdict_lines, result.line) - - if result.highlights then - for _, hl in ipairs(result.highlights) do - table.insert(verdict_highlights, { - line_offset = #verdict_lines - 1, - col_start = hl.col_start, - col_end = hl.col_end, - group = hl.group, - }) + run.run_combined_test(debug, function(result) + if not result then + logger.log('Failed to run combined test', vim.log.levels.ERROR) + return end - end + + if vim.api.nvim_buf_is_valid(io_state.output_buf) then + render_io_view_results(io_state, test_indices, mode, result, combined.input) + end + end) + else + run.run_all_test_cases(test_indices, debug, nil, function() + if vim.api.nvim_buf_is_valid(io_state.output_buf) then + render_io_view_results(io_state, test_indices, mode, nil, nil) + end + end) end - end - - if #output_lines > 0 and #verdict_lines > 0 then - table.insert(output_lines, '') - end - - local verdict_start = #output_lines - for _, line in ipairs(verdict_lines) do - table.insert(output_lines, line) - end - - local final_highlights = {} - for _, vh in ipairs(verdict_highlights) do - table.insert(final_highlights, { - line = verdict_start + vh.line_offset, - col_start = vh.col_start, - col_end = vh.col_end, - highlight_group = vh.group, - }) - end - - utils.update_buffer_content(io_state.input_buf, input_lines, nil, nil) - - local output_ns = vim.api.nvim_create_namespace('cp_io_view_output') - utils.update_buffer_content(io_state.output_buf, output_lines, final_highlights, output_ns) + end) end ---@param panel_opts? PanelOpts @@ -918,30 +927,44 @@ function M.toggle_panel(panel_opts) end) end - local execute = require('cp.runner.execute') - local compile_result = execute.compile_problem(panel_opts and panel_opts.debug) - if compile_result.success then - run.run_all_test_cases(nil, panel_opts and panel_opts.debug) - else - run.handle_compilation_failure(compile_result.output) - end - - refresh_panel() - - vim.schedule(function() - if config.ui.ansi then - require('cp.ui.ansi').setup_highlight_groups() - end - if current_diff_layout then - update_diff_panes() - end - end) - vim.api.nvim_set_current_win(test_windows.tab_win) state.test_buffers = test_buffers state.test_windows = test_windows state.set_active_panel('run') logger.log('test panel opened') + + refresh_panel() + + local function finalize_panel() + vim.schedule(function() + if config.ui.ansi then + require('cp.ui.ansi').setup_highlight_groups() + end + if current_diff_layout then + update_diff_panes() + end + end) + end + + local execute = require('cp.runner.execute') + execute.compile_problem(panel_opts and panel_opts.debug, function(compile_result) + if not test_buffers.tab_buf or not vim.api.nvim_buf_is_valid(test_buffers.tab_buf) then + return + end + + if compile_result.success then + run.run_all_test_cases(nil, panel_opts and panel_opts.debug, function() + refresh_panel() + end, function() + refresh_panel() + finalize_panel() + end) + else + run.handle_compilation_failure(compile_result.output) + refresh_panel() + finalize_panel() + end + end) end return M