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.
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 <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*
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 <space>: toggle <enter>: 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 / <Down> Navigate to next test case
k / <Up> Navigate to previous test case
<space> Toggle selection of current test case
<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)
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*

View file

@ -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 <platform> <contest> <problem> 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 <platform> <contest> <problem> 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", "<CR>", 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

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 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