diff --git a/doc/cp.txt b/doc/cp.txt index e28d6b7..1f2389a 100644 --- a/doc/cp.txt +++ b/doc/cp.txt @@ -51,13 +51,16 @@ Action Commands ~ :CP run Compile and run current problem with test input. Shows execution time and output comparison. + Requires contest setup first. :CP debug Compile with debug flags and run current problem. Includes sanitizers and debug symbols. + Requires contest setup first. :CP test Toggle test panel for individual test case - debugging. Shows per-test results with - vim-native navigation and execution controls. + debugging. Shows per-test results with three-pane + layout for easy Expected/Actual comparison. + Requires contest setup first. Navigation Commands ~ @@ -67,6 +70,23 @@ 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. @@ -261,79 +281,77 @@ Example: Quick setup for single Codeforces problem > TEST PANEL *cp-test* -The test panel provides individual test case debugging for competitive -programming problems, particularly useful for Codeforces where multiple -test cases are combined into single input/output files. +The test panel provides individual test case debugging with a three-pane +layout showing test list, expected output, and actual output side-by-side. +Currently supported for AtCoder and CSES (Codeforces support coming soon). Activation ~ *:CP-test* :CP test Toggle test panel on/off. When activated, replaces current layout with test interface. + Automatically compiles and runs all tests. Toggle again to restore original layout. Interface ~ -The test panel displays a list of test cases with their status and details -for the currently selected test case: > +The test panel uses a three-pane layout for easy comparison: > - ┌─ Test Cases ───────────────────────────────────────────────┐ - │ 1 ✓ PASS 12ms │ - │ 2 ✗ FAIL 45ms │ - │> 3 ✓ PASS 8ms <-- current selection │ - │ 4 ? PENDING │ - │ │ - │ ── Test 3 ── │ - │ Input: │ Expected: │ Actual: │ - │ 5 3 │ 8 │ 8 │ - │ │ │ │ - │ │ - │ j/k: navigate : toggle : run a: run all │ - └────────────────────────────────────────────────────────────┘ + ┌─ Test List ─────────────────────────────────────────────────┐ + │ 1. PASS 12ms │ + │> 2. FAIL 45ms │ + │ 3. 8ms │ + │ 4. │ + │ │ + │ ── Input ── │ + │ 5 3 │ + │ │ + └─────────────────────────────────────────────────────────────┘ + ┌─ Expected ──────────────┐ ┌─ Actual ────────────────┐ + │ 8 │ │ 7 │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + └─────────────────────────┘ └─────────────────────────┘ < Test Status Indicators ~ -✓ PASS Test case passed (green) -✗ FAIL Test case failed (red) -? PENDING Test case not yet executed (yellow) -⟳ RUNNING Test case currently executing (blue) +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 - Toggle selection of current test case - Run selected test cases -a Run all test cases -r Re-run failed test cases only -c Clear all test results -q / Exit test panel (restore layout) +q Exit test panel (restore layout) Test Case Sources ~ Test cases are loaded in priority order: -1. Individual scraped test cases (preferred for Codeforces) -2. Combined input/output files from io/ directory (fallback) +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 Codeforces problems, the plugin attempts to parse individual test -cases from the scraped contest data, enabling precise debugging of -specific test case failures. +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 AtCoder and CSES problems, which typically provide single test -cases, the combined input/output approach is used. +For CSES problems, individual test cases are loaded directly from scraped data. Execution Details ~ Each test case shows: -• Input data provided to your solution -• Expected output from the problem statement -• Actual output produced by your solution +• 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 -• Error messages (if execution failed) +• 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. +precise failure analysis. All tests are automatically run when the +panel opens. FILE STRUCTURE *cp-files* diff --git a/lua/cp/init.lua b/lua/cp/init.lua index 5fc55b2..3261e4b 100644 --- a/lua/cp/init.lua +++ b/lua/cp/init.lua @@ -157,7 +157,7 @@ local function run_problem() logger.log(("running problem: %s"):format(problem_id)) if not state.platform then - logger.log("no platform set", vim.log.levels.ERROR) + logger.log("No contest configured. Use :CP to set up first.", vim.log.levels.ERROR) return end @@ -210,6 +210,11 @@ local function toggle_test_panel() return end + if not state.platform then + logger.log("No contest configured. Use :CP to set up first.", vim.log.levels.ERROR) + return + end + if state.platform == "codeforces" then logger.log("test panel not yet supported for codeforces", vim.log.levels.ERROR) return @@ -233,53 +238,128 @@ local function toggle_test_panel() vim.cmd("silent only") - local test_buf = vim.api.nvim_create_buf(false, true) - vim.api.nvim_set_current_buf(test_buf) - vim.bo.filetype = "cptest" - vim.bo.bufhidden = "wipe" + local tab_buf = vim.api.nvim_create_buf(false, true) + local expected_buf = vim.api.nvim_create_buf(false, true) + local actual_buf = vim.api.nvim_create_buf(false, true) - local function refresh_test_panel() - if not test_buf or not vim.api.nvim_buf_is_valid(test_buf) then - return - end + vim.api.nvim_buf_set_option(tab_buf, "bufhidden", "wipe") + vim.api.nvim_buf_set_option(expected_buf, "bufhidden", "wipe") + vim.api.nvim_buf_set_option(actual_buf, "bufhidden", "wipe") + local main_win = vim.api.nvim_get_current_win() + vim.api.nvim_win_set_buf(main_win, tab_buf) + vim.api.nvim_buf_set_option(tab_buf, "filetype", "cptest") + + vim.cmd("split") + local content_win = vim.api.nvim_get_current_win() + vim.api.nvim_win_set_buf(content_win, actual_buf) + vim.api.nvim_buf_set_option(actual_buf, "filetype", "cptest") + + vim.cmd("vsplit") + local expected_win = vim.api.nvim_get_current_win() + vim.api.nvim_win_set_buf(expected_win, expected_buf) + vim.api.nvim_buf_set_option(expected_buf, "filetype", "cptest") + + local test_windows = { + tab_win = main_win, + actual_win = content_win, + expected_win = expected_win + } + local test_buffers = { + tab_buf = tab_buf, + expected_buf = expected_buf, + actual_buf = actual_buf + } + + local function render_test_tabs() local test_state = test_module.get_test_panel_state() - local test_lines = {} + local tab_lines = {} 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 line = string.format("%s%d %s", prefix, i, status_text) - table.insert(test_lines, line) + local tab = string.format("%s%d. %s", prefix, i, status_text) + table.insert(tab_lines, tab) end - if test_state.test_cases[test_state.current_index] then - local current_test = test_state.test_cases[test_state.current_index] - table.insert(test_lines, "") - table.insert(test_lines, string.format("── Test %d ──", test_state.current_index)) - - table.insert(test_lines, "Input:") + local current_test = test_state.test_cases[test_state.current_index] + if current_test then + table.insert(tab_lines, "") + table.insert(tab_lines, "Input:") + table.insert(tab_lines, "") for _, line in ipairs(vim.split(current_test.input, "\n", { plain = true, trimempty = true })) do - table.insert(test_lines, line) - end - - table.insert(test_lines, "Expected:") - for _, line in ipairs(vim.split(current_test.expected, "\n", { plain = true, trimempty = true })) do - table.insert(test_lines, line) - end - - if current_test.actual then - table.insert(test_lines, "Actual:") - for _, line in ipairs(vim.split(current_test.actual, "\n", { plain = true, trimempty = true })) do - table.insert(test_lines, line) - end + table.insert(tab_lines, line) end end - table.insert(test_lines, "") - table.insert(test_lines, "[j/k] Navigate [Enter] Run all tests [q] Close") + return tab_lines + end - vim.api.nvim_buf_set_lines(test_buf, 0, -1, false, test_lines) + 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] + + if not current_test then + return + end + + local expected_lines = {} + table.insert(expected_lines, "Expected:") + table.insert(expected_lines, "") + + local expected_text = current_test.expected + for _, line in ipairs(vim.split(expected_text, "\n", { plain = true, trimempty = true })) do + table.insert(expected_lines, line) + end + + vim.api.nvim_buf_set_lines(test_buffers.expected_buf, 0, -1, false, expected_lines) + end + + local function update_actual_pane() + local test_state = test_module.get_test_panel_state() + local current_test = test_state.test_cases[test_state.current_index] + + if not current_test then + return + end + + local actual_lines = {} + + if current_test.actual then + table.insert(actual_lines, "Actual:") + table.insert(actual_lines, "") + for _, line in ipairs(vim.split(current_test.actual, "\n", { plain = true, trimempty = true })) do + table.insert(actual_lines, line) + end + + if current_test.status == "fail" then + vim.api.nvim_win_set_option(test_windows.expected_win, "diff", true) + vim.api.nvim_win_set_option(test_windows.actual_win, "diff", true) + else + vim.api.nvim_win_set_option(test_windows.expected_win, "diff", false) + vim.api.nvim_win_set_option(test_windows.actual_win, "diff", false) + end + else + table.insert(actual_lines, "Actual:") + table.insert(actual_lines, "(not run yet)") + + vim.api.nvim_win_set_option(test_windows.expected_win, "diff", false) + vim.api.nvim_win_set_option(test_windows.actual_win, "diff", false) + end + + vim.api.nvim_buf_set_lines(test_buffers.actual_buf, 0, -1, false, actual_lines) + end + + local function refresh_test_panel() + if not test_buffers.tab_buf or not vim.api.nvim_buf_is_valid(test_buffers.tab_buf) then + return + end + + local tab_lines = render_test_tabs() + vim.api.nvim_buf_set_lines(test_buffers.tab_buf, 0, -1, false, tab_lines) + + update_expected_pane() + update_actual_pane() end local function navigate_test_case(delta) @@ -314,20 +394,30 @@ local function toggle_test_panel() vim.keymap.set("n", "j", function() navigate_test_case(1) - end, { buffer = test_buf, silent = true }) + end, { buffer = test_buffers.tab_buf, silent = true }) vim.keymap.set("n", "k", function() navigate_test_case(-1) - end, { buffer = test_buf, silent = true }) - vim.keymap.set("n", "", function() - run_all_tests() - end, { buffer = test_buf, silent = true }) - vim.keymap.set("n", "q", function() - toggle_test_panel() - end, { buffer = test_buf, silent = true }) + end, { buffer = test_buffers.tab_buf, silent = true }) + + for _, buf in pairs(test_buffers) do + vim.keymap.set("n", "q", function() + toggle_test_panel() + end, { buffer = buf, silent = true }) + end + + local execute_module = require("cp.execute") + local contest_config = config.contests[state.platform] + if execute_module.compile_problem(ctx, contest_config) then + test_module.run_all_test_cases(ctx, contest_config) + end refresh_test_panel() + vim.api.nvim_set_current_win(test_windows.tab_win) + state.test_panel_active = true + state.test_buffers = test_buffers + state.test_windows = test_windows local test_state = test_module.get_test_panel_state() logger.log(string.format("test panel opened (%d test cases)", #test_state.test_cases)) end diff --git a/lua/cp/test.lua b/lua/cp/test.lua index 8468717..77be4e6 100644 --- a/lua/cp/test.lua +++ b/lua/cp/test.lua @@ -89,6 +89,10 @@ 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