feat: update docs

This commit is contained in:
Barrett Ruth 2025-09-15 23:46:10 -04:00
parent b2894e9edf
commit 86e8628f3f
3 changed files with 196 additions and 84 deletions

View file

@ -51,13 +51,16 @@ Action Commands ~
:CP run Compile and run current problem with test input. :CP run Compile and run current problem with test input.
Shows execution time and output comparison. Shows execution time and output comparison.
Requires contest setup first.
:CP debug Compile with debug flags and run current problem. :CP debug Compile with debug flags and run current problem.
Includes sanitizers and debug symbols. Includes sanitizers and debug symbols.
Requires contest setup first.
:CP test Toggle test panel for individual test case :CP test Toggle test panel for individual test case
debugging. Shows per-test results with debugging. Shows per-test results with three-pane
vim-native navigation and execution controls. layout for easy Expected/Actual comparison.
Requires contest setup first.
Navigation Commands ~ Navigation Commands ~
@ -67,6 +70,23 @@ Navigation Commands ~
:CP prev Navigate to previous problem in current contest. :CP prev Navigate to previous problem in current contest.
Stops at first problem (no wrapping). 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 <platform> <contest> <problem> 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* CONFIGURATION *cp-config*
cp.nvim works out of the box. No setup required. 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* TEST PANEL *cp-test*
The test panel provides individual test case debugging for competitive The test panel provides individual test case debugging with a three-pane
programming problems, particularly useful for Codeforces where multiple layout showing test list, expected output, and actual output side-by-side.
test cases are combined into single input/output files. Currently supported for AtCoder and CSES (Codeforces support coming soon).
Activation ~ Activation ~
*:CP-test* *:CP-test*
:CP test Toggle test panel on/off. When activated, :CP test Toggle test panel on/off. When activated,
replaces current layout with test interface. replaces current layout with test interface.
Automatically compiles and runs all tests.
Toggle again to restore original layout. Toggle again to restore original layout.
Interface ~ Interface ~
The test panel displays a list of test cases with their status and details The test panel uses a three-pane layout for easy comparison: >
for the currently selected test case: >
┌─ Test Cases ───────────────────────────────────────────────┐ ┌─ Test List ─────────────────────────────────────────────────┐
│ 1 ✓ PASS 12ms │ │ 1. PASS 12ms │
│ 2 ✗ FAIL 45ms │ │> 2. FAIL 45ms │
│> 3 ✓ PASS 8ms <-- current selection │ │ 3. 8ms │
│ 4 ? PENDING │ │ 4. │
│ │ │ │
│ ── Test 3 ── │ │ ── Input ── │
│ Input: │ Expected: │ Actual: │ │ 5 3 │
│ 5 3 │ 8 │ 8 │ │ │
│ │ │ │ └─────────────────────────────────────────────────────────────┘
│ │ ┌─ Expected ──────────────┐ ┌─ Actual ────────────────┐
│ j/k: navigate <space>: toggle <enter>: run a: run all │ │ 8 │ │ 7 │
└────────────────────────────────────────────────────────────┘ │ │ │ │
│ │ │ │
│ │ │ │
└─────────────────────────┘ └─────────────────────────┘
< <
Test Status Indicators ~ Test Status Indicators ~
✓ PASS Test case passed (green) PASS Test case passed (green highlighting)
✗ FAIL Test case failed (red) FAIL Test case failed (red highlighting with diff)
? PENDING Test case not yet executed (yellow) (empty) Test case not yet executed
⟳ RUNNING Test case currently executing (blue)
Keymaps ~ Keymaps ~
*cp-test-keys* *cp-test-keys*
j / <Down> Navigate to next test case j / <Down> Navigate to next test case
k / <Up> Navigate to previous test case k / <Up> Navigate to previous test case
<space> Toggle selection of current test case q Exit test panel (restore layout)
<enter> Run selected test cases
a Run all test cases
r Re-run failed test cases only
c Clear all test results
q / <esc> Exit test panel (restore layout)
Test Case Sources ~ Test Case Sources ~
Test cases are loaded in priority order: Test cases are loaded in priority order:
1. Individual scraped test cases (preferred for Codeforces) 1. Individual scraped test cases from cache (AtCoder, CSES)
2. Combined input/output files from io/ directory (fallback) 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 For AtCoder problems, individual test case files are prefixed with "1\n"
cases from the scraped contest data, enabling precise debugging of to satisfy template requirements, but this prefix is stripped in the UI
specific test case failures. for clean display.
For AtCoder and CSES problems, which typically provide single test For CSES problems, individual test cases are loaded directly from scraped data.
cases, the combined input/output approach is used.
Execution Details ~ Execution Details ~
Each test case shows: Each test case shows:
• Input data provided to your solution • Input data provided to your solution (top pane)
• Expected output from the problem statement • Expected output from the problem statement (bottom left pane)
• Actual output produced by your solution • Actual output produced by your solution (bottom right pane)
• Execution time in milliseconds • 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 Test cases are executed individually using the same compilation and
execution pipeline as |:CP-run|, but with isolated input/output for 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* FILE STRUCTURE *cp-files*

View file

@ -157,7 +157,7 @@ local function run_problem()
logger.log(("running problem: %s"):format(problem_id)) logger.log(("running problem: %s"):format(problem_id))
if not state.platform then if not state.platform then
logger.log("no platform set", vim.log.levels.ERROR) logger.log("No contest configured. Use :CP <platform> <contest> <problem> to set up first.", vim.log.levels.ERROR)
return return
end end
@ -210,6 +210,11 @@ local function toggle_test_panel()
return return
end end
if not state.platform then
logger.log("No contest configured. Use :CP <platform> <contest> <problem> to set up first.", vim.log.levels.ERROR)
return
end
if state.platform == "codeforces" then if state.platform == "codeforces" then
logger.log("test panel not yet supported for codeforces", vim.log.levels.ERROR) logger.log("test panel not yet supported for codeforces", vim.log.levels.ERROR)
return return
@ -233,53 +238,128 @@ local function toggle_test_panel()
vim.cmd("silent only") vim.cmd("silent only")
local test_buf = vim.api.nvim_create_buf(false, true) local tab_buf = vim.api.nvim_create_buf(false, true)
vim.api.nvim_set_current_buf(test_buf) local expected_buf = vim.api.nvim_create_buf(false, true)
vim.bo.filetype = "cptest" local actual_buf = vim.api.nvim_create_buf(false, true)
vim.bo.bufhidden = "wipe"
local function refresh_test_panel() vim.api.nvim_buf_set_option(tab_buf, "bufhidden", "wipe")
if not test_buf or not vim.api.nvim_buf_is_valid(test_buf) then vim.api.nvim_buf_set_option(expected_buf, "bufhidden", "wipe")
return vim.api.nvim_buf_set_option(actual_buf, "bufhidden", "wipe")
end
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_state = test_module.get_test_panel_state()
local test_lines = {} local tab_lines = {}
for i, test_case in ipairs(test_state.test_cases) do 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 status_text = test_case.status == "pending" and "?" or string.upper(test_case.status)
local prefix = i == test_state.current_index and "> " or " " local prefix = i == test_state.current_index and "> " or " "
local line = string.format("%s%d %s", prefix, i, status_text) local tab = string.format("%s%d. %s", prefix, i, status_text)
table.insert(test_lines, line) table.insert(tab_lines, tab)
end end
if test_state.test_cases[test_state.current_index] then local current_test = test_state.test_cases[test_state.current_index]
local current_test = test_state.test_cases[test_state.current_index] if current_test then
table.insert(test_lines, "") table.insert(tab_lines, "")
table.insert(test_lines, string.format("── Test %d ──", test_state.current_index)) table.insert(tab_lines, "Input:")
table.insert(tab_lines, "")
table.insert(test_lines, "Input:")
for _, line in ipairs(vim.split(current_test.input, "\n", { plain = true, trimempty = true })) do for _, line in ipairs(vim.split(current_test.input, "\n", { plain = true, trimempty = true })) do
table.insert(test_lines, line) table.insert(tab_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
end end
end end
table.insert(test_lines, "") return tab_lines
table.insert(test_lines, "[j/k] Navigate [Enter] Run all tests [q] Close") 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 end
local function navigate_test_case(delta) local function navigate_test_case(delta)
@ -314,20 +394,30 @@ local function toggle_test_panel()
vim.keymap.set("n", "j", function() vim.keymap.set("n", "j", function()
navigate_test_case(1) navigate_test_case(1)
end, { buffer = test_buf, silent = true }) end, { buffer = test_buffers.tab_buf, silent = true })
vim.keymap.set("n", "k", function() vim.keymap.set("n", "k", function()
navigate_test_case(-1) navigate_test_case(-1)
end, { buffer = test_buf, silent = true }) end, { buffer = test_buffers.tab_buf, silent = true })
vim.keymap.set("n", "<CR>", function()
run_all_tests() for _, buf in pairs(test_buffers) do
end, { buffer = test_buf, silent = true }) vim.keymap.set("n", "q", function()
vim.keymap.set("n", "q", function() toggle_test_panel()
toggle_test_panel() end, { buffer = buf, silent = true })
end, { buffer = test_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() refresh_test_panel()
vim.api.nvim_set_current_win(test_windows.tab_win)
state.test_panel_active = true state.test_panel_active = true
state.test_buffers = test_buffers
state.test_windows = test_windows
local test_state = test_module.get_test_panel_state() local test_state = test_module.get_test_panel_state()
logger.log(string.format("test panel opened (%d test cases)", #test_state.test_cases)) logger.log(string.format("test panel opened (%d test cases)", #test_state.test_cases))
end end

View file

@ -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 input_content = table.concat(vim.fn.readfile(individual_input_file), "\n")
local expected_content = table.concat(vim.fn.readfile(individual_expected_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)) table.insert(test_cases, create_test_case(i, input_content, expected_content))
i = i + 1 i = i + 1
else else