Merge pull request #176 from barrett-ruth/feat/ui/test-case-editing

test case management
This commit is contained in:
Barrett Ruth 2025-10-24 17:10:54 -04:00 committed by GitHub
commit 1d89fa0bdd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 259 additions and 19 deletions

View file

@ -14,6 +14,7 @@ https://github.com/user-attachments/assets/2f01db4a-718a-482b-89c0-e841d37a63b4
- **Automatic problem setup**: Scrape test cases and metadata in seconds
- **Dual view modes**: Lightweight I/O view for quick feedback, full panel for
detailed analysis
- **Test case management**: Quickly view, edit, add, & remove test cases
- **Rich test output**: 256 color ANSI support for compiler errors and program
output
- **Language agnostic**: Works with any language
@ -31,21 +32,20 @@ cp.nvim follows a simple principle: **solve locally, submit remotely**.
### Basic Usage
1. **Find a contest or problem** on the judge website
2. **Set up locally** with `:CP <platform> <contest>`
1. Find a contest or problem
2. Set up contests locally
```
:CP codeforces 1848
```
3. **Code and test** with instant feedback
3. Code and test
```
:CP run " Quick verdict summary in splits
:CP panel " Detailed analysis with diffs
:CP run
```
4. **Navigate between problems**
4. Navigate between problems
```
:CP next
@ -53,7 +53,14 @@ cp.nvim follows a simple principle: **solve locally, submit remotely**.
:CP e1
```
5. **Submit** on the original website
5. Debug and edit test cases
```
:CP edit
:CP panel --debug
```
5. Submit on the original website
## Documentation
@ -63,7 +70,7 @@ cp.nvim follows a simple principle: **solve locally, submit remotely**.
See
[my config](https://github.com/barrett-ruth/dots/blob/main/nvim/lua/plugins/cp.lua)
for a relatively advanced setup.
for the setup in the video shown above.
## Similar Projects

View file

@ -114,8 +114,12 @@ COMMANDS *cp-commands*
Changes saved to both cache and disk on exit,
taking effect immediately in :CP run and CLI.
Keybindings:
Keybindings (configurable via |EditConfig|):
q Save all and exit editor
]t Jump to next test column
[t Jump to previous test column
gd Delete current test column
ga Add new test column at end
<c-w> Normal window navigation
Examples: >
@ -348,6 +352,15 @@ run CSES problems with Rust using the single schema:
{format_verdict} (|VerdictFormatter|, default: nil) Custom verdict line
formatter. See |cp-verdict-format|.
*EditConfig*
Fields: ~
{next_test_key} (string|nil, default: ']t') Jump to next test.
{prev_test_key} (string|nil, default: '[t') Jump to previous test.
{delete_test_key} (string|nil, default: 'gd') Delete current test.
{add_test_key} (string|nil, default: 'ga') Add new test.
{save_and_exit_key} (string|nil, default: 'q') Save and exit editor.
All keys are nil-able. Set to nil to disable.
*cp.PanelConfig*
Fields: ~
{diff_mode} (string, default: "none") Diff backend: "none",

View file

@ -65,9 +65,17 @@
---@field prev_test_key string|nil
---@field format_verdict VerdictFormatter
---@class EditConfig
---@field next_test_key string|nil
---@field prev_test_key string|nil
---@field delete_test_key string|nil
---@field add_test_key string|nil
---@field save_and_exit_key string|nil
---@class CpUI
---@field ansi boolean
---@field run RunConfig
---@field edit EditConfig
---@field panel PanelConfig
---@field diff DiffConfig
---@field picker string|nil
@ -154,6 +162,13 @@ M.defaults = {
prev_test_key = '<c-p>',
format_verdict = helpers.default_verdict_formatter,
},
edit = {
next_test_key = ']t',
prev_test_key = '[t',
delete_test_key = 'gd',
add_test_key = 'ga',
save_and_exit_key = 'q',
},
panel = { diff_mode = 'none', max_output_lines = 50 },
diff = {
git = {
@ -329,6 +344,41 @@ function M.setup(user_config)
cfg.ui.run.format_verdict,
'function',
},
edit_next_test_key = {
cfg.ui.edit.next_test_key,
function(v)
return v == nil or (type(v) == 'string' and #v > 0)
end,
'nil or non-empty string',
},
edit_prev_test_key = {
cfg.ui.edit.prev_test_key,
function(v)
return v == nil or (type(v) == 'string' and #v > 0)
end,
'nil or non-empty string',
},
delete_test_key = {
cfg.ui.edit.delete_test_key,
function(v)
return v == nil or (type(v) == 'string' and #v > 0)
end,
'nil or non-empty string',
},
add_test_key = {
cfg.ui.edit.add_test_key,
function(v)
return v == nil or (type(v) == 'string' and #v > 0)
end,
'nil or non-empty string',
},
save_and_exit_key = {
cfg.ui.edit.save_and_exit_key,
function(v)
return v == nil or (type(v) == 'string' and #v > 0)
end,
'nil or non-empty string',
},
})
for id, lang in pairs(cfg.languages) do

View file

@ -21,12 +21,48 @@ local utils = require('cp.utils')
---@type EditState?
local edit_state = nil
local function setup_keybindings(buf)
vim.keymap.set('n', 'q', function()
M.toggle_edit()
end, { buffer = buf, silent = true, desc = 'Save and exit test editor' })
local setup_keybindings
---@param bufnr integer
---@return integer? test_index
local function get_current_test_index(bufnr)
if not edit_state then
return nil
end
for i, pair in ipairs(edit_state.test_buffers) do
if pair.input_buf == bufnr or pair.expected_buf == bufnr then
return i
end
end
return nil
end
---@param index integer
local function jump_to_test(index)
if not edit_state then
return
end
local pair = edit_state.test_buffers[index]
if pair and vim.api.nvim_win_is_valid(pair.input_win) then
vim.api.nvim_set_current_win(pair.input_win)
end
end
---@param delta integer
local function navigate_test(delta)
local current_buf = vim.api.nvim_get_current_buf()
local current_index = get_current_test_index(current_buf)
if not current_index or not edit_state then
return
end
local new_index = current_index + delta
if new_index < 1 or new_index > #edit_state.test_buffers then
return
end
jump_to_test(new_index)
end
---@param test_index integer
local function load_test_into_buffer(test_index)
if not edit_state then
return
@ -49,6 +85,140 @@ local function load_test_into_buffer(test_index)
vim.api.nvim_buf_set_name(pair.expected_buf, string.format('cp://test-%d-expected', test_index))
end
local function delete_current_test()
if not edit_state then
return
end
if #edit_state.test_buffers == 1 then
logger.log('Cannot have 0 problem tests.', vim.log.levels.ERROR)
return
end
local current_buf = vim.api.nvim_get_current_buf()
local current_index = get_current_test_index(current_buf)
if not current_index then
return
end
local pair = edit_state.test_buffers[current_index]
if vim.api.nvim_win_is_valid(pair.input_win) then
vim.api.nvim_win_close(pair.input_win, true)
end
if vim.api.nvim_win_is_valid(pair.expected_win) then
vim.api.nvim_win_close(pair.expected_win, true)
end
if vim.api.nvim_buf_is_valid(pair.input_buf) then
vim.api.nvim_buf_delete(pair.input_buf, { force = true })
end
if vim.api.nvim_buf_is_valid(pair.expected_buf) then
vim.api.nvim_buf_delete(pair.expected_buf, { force = true })
end
table.remove(edit_state.test_buffers, current_index)
table.remove(edit_state.test_cases, current_index)
for i = current_index, #edit_state.test_buffers do
load_test_into_buffer(i)
end
local next_index = math.min(current_index, #edit_state.test_buffers)
jump_to_test(next_index)
logger.log(('Deleted test %d'):format(current_index))
end
local function add_new_test()
if not edit_state then
return
end
local last_pair = edit_state.test_buffers[#edit_state.test_buffers]
if not last_pair or not vim.api.nvim_win_is_valid(last_pair.input_win) then
return
end
vim.api.nvim_set_current_win(last_pair.input_win)
vim.cmd.vsplit()
local input_win = vim.api.nvim_get_current_win()
local input_buf = utils.create_buffer_with_options()
vim.api.nvim_win_set_buf(input_win, input_buf)
vim.bo[input_buf].modifiable = true
vim.bo[input_buf].readonly = false
vim.bo[input_buf].buftype = 'nofile'
vim.bo[input_buf].buflisted = false
helpers.clearcol(input_buf)
vim.api.nvim_set_current_win(last_pair.expected_win)
vim.cmd.vsplit()
local expected_win = vim.api.nvim_get_current_win()
local expected_buf = utils.create_buffer_with_options()
vim.api.nvim_win_set_buf(expected_win, expected_buf)
vim.bo[expected_buf].modifiable = true
vim.bo[expected_buf].readonly = false
vim.bo[expected_buf].buftype = 'nofile'
vim.bo[expected_buf].buflisted = false
helpers.clearcol(expected_buf)
local new_index = #edit_state.test_buffers + 1
local new_pair = {
input_buf = input_buf,
expected_buf = expected_buf,
input_win = input_win,
expected_win = expected_win,
}
table.insert(edit_state.test_buffers, new_pair)
table.insert(edit_state.test_cases, { index = new_index, input = '', expected = '' })
setup_keybindings(input_buf)
setup_keybindings(expected_buf)
load_test_into_buffer(new_index)
vim.api.nvim_set_current_win(input_win)
logger.log(('Added test %d'):format(new_index))
end
---@param buf integer
setup_keybindings = function(buf)
local config = config_module.get_config()
local keys = config.ui.edit
if keys.save_and_exit_key then
vim.keymap.set('n', keys.save_and_exit_key, function()
M.toggle_edit()
end, { buffer = buf, silent = true, desc = 'Save and exit test editor' })
end
if keys.next_test_key then
vim.keymap.set('n', keys.next_test_key, function()
navigate_test(1)
end, { buffer = buf, silent = true, desc = 'Next test' })
end
if keys.prev_test_key then
vim.keymap.set('n', keys.prev_test_key, function()
navigate_test(-1)
end, { buffer = buf, silent = true, desc = 'Previous test' })
end
if keys.delete_test_key then
vim.keymap.set(
'n',
keys.delete_test_key,
delete_current_test,
{ buffer = buf, silent = true, desc = 'Delete test' }
)
end
if keys.add_test_key then
vim.keymap.set(
'n',
keys.add_test_key,
add_new_test,
{ buffer = buf, silent = true, desc = 'Add test' }
)
end
end
local function save_all_tests()
if not edit_state then
return

View file

@ -12,8 +12,8 @@ async def pump(
data = await reader.readline()
if not data:
break
sys.stdout.buffer.write(data)
sys.stdout.flush()
_ = sys.stdout.buffer.write(data)
_ = sys.stdout.flush()
if writer:
writer.write(data)
await writer.drain()
@ -42,9 +42,9 @@ async def main(interactor_cmd: Sequence[str], interactee_cmd: Sequence[str]) ->
asyncio.create_task(pump(interactor.stdout, interactee.stdin)),
asyncio.create_task(pump(interactee.stdout, interactor.stdin)),
]
await asyncio.wait(tasks, return_when=asyncio.ALL_COMPLETED)
await interactor.wait()
await interactee.wait()
_ = await asyncio.wait(tasks, return_when=asyncio.ALL_COMPLETED)
_ = await interactor.wait()
_ = await interactee.wait()
if __name__ == "__main__":
@ -55,4 +55,4 @@ if __name__ == "__main__":
interactor_cmd = shlex.split(sys.argv[1])
interactee_cmd = shlex.split(sys.argv[2])
asyncio.run(main(interactor_cmd, interactee_cmd))
_ = asyncio.run(main(interactor_cmd, interactee_cmd))