diff --git a/after/syntax/cp.vim b/after/syntax/cp.vim index d76f87e..fe9eef7 100644 --- a/after/syntax/cp.vim +++ b/after/syntax/cp.vim @@ -5,13 +5,13 @@ endif syntax match cpOutputCode /^\[code\]:/ syntax match cpOutputTime /^\[time\]:/ syntax match cpOutputDebug /^\[debug\]:/ -syntax match cpOutputMatchesTrue /^\[matches\]:\ze true$/ -syntax match cpOutputMatchesFalse /^\[matches\]:\ze false$/ +syntax match cpOutputOkTrue /^\[ok\]:\ze true$/ +syntax match cpOutputOkFalse /^\[ok\]:\ze false$/ highlight default link cpOutputCode DiagnosticInfo highlight default link cpOutputTime Comment highlight default link cpOutputDebug Comment -highlight default link cpOutputMatchesTrue DiffAdd -highlight default link cpOutputMatchesFalse DiffDelete +highlight default link cpOutputOkTrue DiffAdd +highlight default link cpOutputOkFalse DiffDelete -let b:current_syntax = "cp" \ No newline at end of file +let b:current_syntax = "cp" diff --git a/doc/cp.txt b/doc/cp.txt index 1f2389a..37a0484 100644 --- a/doc/cp.txt +++ b/doc/cp.txt @@ -70,23 +70,6 @@ Navigation Commands ~ :CP prev Navigate to previous problem in current contest. Stops at first problem (no wrapping). -ERROR HANDLING *cp-errors* - -cp.nvim provides clear error messages for common issues: - -No Contest Configured ~ -When running `:CP run`, `:CP debug`, or `:CP test` without setting up a -contest first, you'll see: -"No contest configured. Use :CP to set up first." - -Platform Not Supported ~ -For platforms or features not yet implemented: -"test panel not yet supported for codeforces" - -Missing Dependencies ~ -When required tools are missing: -"uv is not installed. Install it to enable problem scraping: https://docs.astral.sh/uv/" - CONFIGURATION *cp-config* cp.nvim works out of the box. No setup required. @@ -314,40 +297,14 @@ The test panel uses a three-pane layout for easy comparison: > └─────────────────────────┘ └─────────────────────────┘ < -Test Status Indicators ~ - -PASS Test case passed (green highlighting) -FAIL Test case failed (red highlighting with diff) -(empty) Test case not yet executed - Keymaps ~ *cp-test-keys* j / Navigate to next test case k / Navigate to previous test case q Exit test panel (restore layout) -Test Case Sources ~ - -Test cases are loaded in priority order: -1. Individual scraped test cases from cache (AtCoder, CSES) -2. Individual test case files (*.1.cpin, *.2.cpin, etc.) -3. Combined input/output files from io/ directory (fallback) - -For AtCoder problems, individual test case files are prefixed with "1\n" -to satisfy template requirements, but this prefix is stripped in the UI -for clean display. - -For CSES problems, individual test cases are loaded directly from scraped data. - Execution Details ~ -Each test case shows: -• Input data provided to your solution (top pane) -• Expected output from the problem statement (bottom left pane) -• Actual output produced by your solution (bottom right pane) -• Execution time in milliseconds -• Pass/fail status with vim diff highlighting for failures - Test cases are executed individually using the same compilation and execution pipeline as |:CP-run|, but with isolated input/output for precise failure analysis. All tests are automatically run when the diff --git a/lua/cp/constants.lua b/lua/cp/constants.lua index e397c8f..59693a8 100644 --- a/lua/cp/constants.lua +++ b/lua/cp/constants.lua @@ -21,4 +21,23 @@ M.canonical_filetypes = { [M.PYTHON] = "python", } +---@type table +M.signal_codes = { + [128] = "SIGILL", + [130] = "SIGINT", + [131] = "SIGQUIT", + [132] = "SIGILL", + [133] = "SIGTRAP", + [134] = "SIGABRT", + [135] = "SIGBUS", + [136] = "SIGFPE", + [137] = "SIGKILL", + [138] = "SIGUSR1", + [139] = "SIGSEGV", + [140] = "SIGUSR2", + [141] = "SIGPIPE", + [142] = "SIGALRM", + [143] = "SIGTERM", +} + return M diff --git a/lua/cp/execute.lua b/lua/cp/execute.lua index d341489..d52b970 100644 --- a/lua/cp/execute.lua +++ b/lua/cp/execute.lua @@ -64,23 +64,6 @@ local function build_command(cmd_template, executable, substitutions) return cmd end -local signal_codes = { - [128] = "SIGILL", - [130] = "SIGINT", - [131] = "SIGQUIT", - [132] = "SIGILL", - [133] = "SIGTRAP", - [134] = "SIGABRT", - [135] = "SIGBUS", - [136] = "SIGFPE", - [137] = "SIGKILL", - [138] = "SIGUSR1", - [139] = "SIGSEGV", - [140] = "SIGUSR2", - [141] = "SIGPIPE", - [142] = "SIGALRM", - [143] = "SIGTERM", -} local function ensure_directories() vim.system({ "mkdir", "-p", "build", "io" }):wait() @@ -176,7 +159,7 @@ local function format_output(exec_result, expected_file, is_debug) if exec_result.timed_out then table.insert(metadata_lines, "[code]: 124 (TIMEOUT)") elseif exec_result.code >= 128 then - local signal_name = signal_codes[exec_result.code] or "SIGNAL" + 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)) @@ -193,17 +176,17 @@ local function format_output(exec_result, expected_file, is_debug) table.remove(actual_lines) end - local matches = #actual_lines == #expected_content - if matches then + local ok = #actual_lines == #expected_content + if ok then for i, line in ipairs(actual_lines) do if line ~= expected_content[i] then - matches = false + ok = false break end end end - table.insert(metadata_lines, ("[matches]: %s"):format(matches and "true" or "false")) + table.insert(metadata_lines, ("[ok]: %s"):format(ok and "true" or "false")) end return table.concat(output_lines, "") .. "\n" .. table.concat(metadata_lines, "\n") diff --git a/lua/cp/init.lua b/lua/cp/init.lua index fc2fb6a..43aa091 100644 --- a/lua/cp/init.lua +++ b/lua/cp/init.lua @@ -281,10 +281,45 @@ local function toggle_test_panel() local test_state = test_module.get_test_panel_state() local tab_lines = {} + local max_status_width = 0 + local max_code_width = 0 + local max_time_width = 0 + + for _, test_case in ipairs(test_state.test_cases) do + local status_text = test_case.status == "pending" and "" or string.upper(test_case.status) + max_status_width = math.max(max_status_width, #status_text) + + if test_case.code then + max_code_width = math.max(max_code_width, #tostring(test_case.code)) + end + + if test_case.time_ms then + local time_text = string.format("%.0fms", test_case.time_ms) + max_time_width = math.max(max_time_width, #time_text) + end + end + for i, test_case in ipairs(test_state.test_cases) do - local status_text = test_case.status == "pending" and "?" or string.upper(test_case.status) local prefix = i == test_state.current_index and "> " or " " - local tab = string.format("%s%d. %s", prefix, i, status_text) + local tab = string.format("%s%d.", prefix, i) + + if test_case.code then + tab = tab .. string.format(" [code:%-" .. max_code_width .. "s]", tostring(test_case.code)) + end + + if test_case.time_ms then + local time_text = string.format("%.0fms", test_case.time_ms) + tab = tab .. string.format(" [time:%-" .. max_time_width .. "s]", time_text) + end + + if test_case.ok ~= nil then + tab = tab .. string.format(" [ok:%-5s]", tostring(test_case.ok)) + end + + if test_case.signal then + tab = tab .. string.format(" [%s]", test_case.signal) + end + table.insert(tab_lines, tab) end @@ -301,6 +336,7 @@ local function toggle_test_panel() return tab_lines end + local function update_expected_pane() local test_state = test_module.get_test_panel_state() local current_test = test_state.test_cases[test_state.current_index] diff --git a/lua/cp/scrape.lua b/lua/cp/scrape.lua index 5fc6059..16b05e4 100644 --- a/lua/cp/scrape.lua +++ b/lua/cp/scrape.lua @@ -259,10 +259,6 @@ function M.scrape_problem(ctx) local input_content = test_case.input:gsub("\r", "") local expected_content = test_case.output:gsub("\r", "") - if ctx.contest == "atcoder" then - input_content = "1\n" .. input_content - end - vim.fn.writefile(vim.split(input_content, "\n", true), input_file) vim.fn.writefile(vim.split(expected_content, "\n", true), expected_file) end diff --git a/lua/cp/test.lua b/lua/cp/test.lua index 77be4e6..fc84316 100644 --- a/lua/cp/test.lua +++ b/lua/cp/test.lua @@ -7,6 +7,10 @@ ---@field time_ms number? ---@field error string? ---@field selected boolean +---@field code number? +---@field ok boolean? +---@field signal string? +---@field timed_out boolean? ---@class TestPanelState ---@field test_cases TestCase[] @@ -18,6 +22,7 @@ local M = {} local logger = require("cp.log") +local constants = require("cp.constants") ---@type TestPanelState local test_panel_state = { @@ -89,10 +94,6 @@ local function parse_test_cases_from_files(input_file, expected_file) local input_content = table.concat(vim.fn.readfile(individual_input_file), "\n") local expected_content = table.concat(vim.fn.readfile(individual_expected_file), "\n") - if input_content:match("^1\n") then - input_content = input_content:gsub("^1\n", "") - end - table.insert(test_cases, create_test_case(i, input_content, expected_content)) i = i + 1 else @@ -180,22 +181,32 @@ local function run_single_test_case(ctx, contest_config, test_case) local actual_output = (result.stdout or ""):gsub("\n$", "") local expected_output = test_case.expected:gsub("\n$", "") - local matches = actual_output == expected_output + local ok = actual_output == expected_output local status - if result.code == 143 or result.code == 124 then + local timed_out = result.code == 143 or result.code == 124 + if timed_out then status = "timeout" - elseif result.code == 0 and matches then + 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, error = result.code ~= 0 and result.stderr or nil, time_ms = execution_time, + code = result.code, + ok = ok, + signal = signal, + timed_out = timed_out, } end @@ -235,6 +246,10 @@ function M.run_test_case(ctx, contest_config, index) test_case.actual = result.actual test_case.error = result.error 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