Merge pull request #176 from barrett-ruth/feat/ui/test-case-editing
test case management
This commit is contained in:
commit
1d89fa0bdd
5 changed files with 259 additions and 19 deletions
23
README.md
23
README.md
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue