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
|
- **Automatic problem setup**: Scrape test cases and metadata in seconds
|
||||||
- **Dual view modes**: Lightweight I/O view for quick feedback, full panel for
|
- **Dual view modes**: Lightweight I/O view for quick feedback, full panel for
|
||||||
detailed analysis
|
detailed analysis
|
||||||
|
- **Test case management**: Quickly view, edit, add, & remove test cases
|
||||||
- **Rich test output**: 256 color ANSI support for compiler errors and program
|
- **Rich test output**: 256 color ANSI support for compiler errors and program
|
||||||
output
|
output
|
||||||
- **Language agnostic**: Works with any language
|
- **Language agnostic**: Works with any language
|
||||||
|
|
@ -31,21 +32,20 @@ cp.nvim follows a simple principle: **solve locally, submit remotely**.
|
||||||
|
|
||||||
### Basic Usage
|
### Basic Usage
|
||||||
|
|
||||||
1. **Find a contest or problem** on the judge website
|
1. Find a contest or problem
|
||||||
2. **Set up locally** with `:CP <platform> <contest>`
|
2. Set up contests locally
|
||||||
|
|
||||||
```
|
```
|
||||||
:CP codeforces 1848
|
:CP codeforces 1848
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Code and test** with instant feedback
|
3. Code and test
|
||||||
|
|
||||||
```
|
```
|
||||||
:CP run " Quick verdict summary in splits
|
:CP run
|
||||||
:CP panel " Detailed analysis with diffs
|
|
||||||
```
|
```
|
||||||
|
|
||||||
4. **Navigate between problems**
|
4. Navigate between problems
|
||||||
|
|
||||||
```
|
```
|
||||||
:CP next
|
:CP next
|
||||||
|
|
@ -53,7 +53,14 @@ cp.nvim follows a simple principle: **solve locally, submit remotely**.
|
||||||
:CP e1
|
: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
|
## Documentation
|
||||||
|
|
||||||
|
|
@ -63,7 +70,7 @@ cp.nvim follows a simple principle: **solve locally, submit remotely**.
|
||||||
|
|
||||||
See
|
See
|
||||||
[my config](https://github.com/barrett-ruth/dots/blob/main/nvim/lua/plugins/cp.lua)
|
[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
|
## Similar Projects
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -114,8 +114,12 @@ COMMANDS *cp-commands*
|
||||||
Changes saved to both cache and disk on exit,
|
Changes saved to both cache and disk on exit,
|
||||||
taking effect immediately in :CP run and CLI.
|
taking effect immediately in :CP run and CLI.
|
||||||
|
|
||||||
Keybindings:
|
Keybindings (configurable via |EditConfig|):
|
||||||
q Save all and exit editor
|
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
|
<c-w> Normal window navigation
|
||||||
|
|
||||||
Examples: >
|
Examples: >
|
||||||
|
|
@ -348,6 +352,15 @@ run CSES problems with Rust using the single schema:
|
||||||
{format_verdict} (|VerdictFormatter|, default: nil) Custom verdict line
|
{format_verdict} (|VerdictFormatter|, default: nil) Custom verdict line
|
||||||
formatter. See |cp-verdict-format|.
|
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*
|
*cp.PanelConfig*
|
||||||
Fields: ~
|
Fields: ~
|
||||||
{diff_mode} (string, default: "none") Diff backend: "none",
|
{diff_mode} (string, default: "none") Diff backend: "none",
|
||||||
|
|
|
||||||
|
|
@ -65,9 +65,17 @@
|
||||||
---@field prev_test_key string|nil
|
---@field prev_test_key string|nil
|
||||||
---@field format_verdict VerdictFormatter
|
---@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
|
---@class CpUI
|
||||||
---@field ansi boolean
|
---@field ansi boolean
|
||||||
---@field run RunConfig
|
---@field run RunConfig
|
||||||
|
---@field edit EditConfig
|
||||||
---@field panel PanelConfig
|
---@field panel PanelConfig
|
||||||
---@field diff DiffConfig
|
---@field diff DiffConfig
|
||||||
---@field picker string|nil
|
---@field picker string|nil
|
||||||
|
|
@ -154,6 +162,13 @@ M.defaults = {
|
||||||
prev_test_key = '<c-p>',
|
prev_test_key = '<c-p>',
|
||||||
format_verdict = helpers.default_verdict_formatter,
|
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 },
|
panel = { diff_mode = 'none', max_output_lines = 50 },
|
||||||
diff = {
|
diff = {
|
||||||
git = {
|
git = {
|
||||||
|
|
@ -329,6 +344,41 @@ function M.setup(user_config)
|
||||||
cfg.ui.run.format_verdict,
|
cfg.ui.run.format_verdict,
|
||||||
'function',
|
'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
|
for id, lang in pairs(cfg.languages) do
|
||||||
|
|
|
||||||
|
|
@ -21,12 +21,48 @@ local utils = require('cp.utils')
|
||||||
---@type EditState?
|
---@type EditState?
|
||||||
local edit_state = nil
|
local edit_state = nil
|
||||||
|
|
||||||
local function setup_keybindings(buf)
|
local setup_keybindings
|
||||||
vim.keymap.set('n', 'q', function()
|
|
||||||
M.toggle_edit()
|
---@param bufnr integer
|
||||||
end, { buffer = buf, silent = true, desc = 'Save and exit test editor' })
|
---@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
|
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)
|
local function load_test_into_buffer(test_index)
|
||||||
if not edit_state then
|
if not edit_state then
|
||||||
return
|
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))
|
vim.api.nvim_buf_set_name(pair.expected_buf, string.format('cp://test-%d-expected', test_index))
|
||||||
end
|
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()
|
local function save_all_tests()
|
||||||
if not edit_state then
|
if not edit_state then
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,8 @@ async def pump(
|
||||||
data = await reader.readline()
|
data = await reader.readline()
|
||||||
if not data:
|
if not data:
|
||||||
break
|
break
|
||||||
sys.stdout.buffer.write(data)
|
_ = sys.stdout.buffer.write(data)
|
||||||
sys.stdout.flush()
|
_ = sys.stdout.flush()
|
||||||
if writer:
|
if writer:
|
||||||
writer.write(data)
|
writer.write(data)
|
||||||
await writer.drain()
|
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(interactor.stdout, interactee.stdin)),
|
||||||
asyncio.create_task(pump(interactee.stdout, interactor.stdin)),
|
asyncio.create_task(pump(interactee.stdout, interactor.stdin)),
|
||||||
]
|
]
|
||||||
await asyncio.wait(tasks, return_when=asyncio.ALL_COMPLETED)
|
_ = await asyncio.wait(tasks, return_when=asyncio.ALL_COMPLETED)
|
||||||
await interactor.wait()
|
_ = await interactor.wait()
|
||||||
await interactee.wait()
|
_ = await interactee.wait()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
@ -55,4 +55,4 @@ if __name__ == "__main__":
|
||||||
interactor_cmd = shlex.split(sys.argv[1])
|
interactor_cmd = shlex.split(sys.argv[1])
|
||||||
interactee_cmd = shlex.split(sys.argv[2])
|
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