feat: update docs
This commit is contained in:
parent
b2894e9edf
commit
86e8628f3f
3 changed files with 196 additions and 84 deletions
102
doc/cp.txt
102
doc/cp.txt
|
|
@ -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*
|
||||||
|
|
||||||
|
|
|
||||||
174
lua/cp/init.lua
174
lua/cp/init.lua
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue