diff --git a/README.md b/README.md index 7feb50b..fec275a 100644 --- a/README.md +++ b/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 ` +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 diff --git a/doc/cp.nvim.txt b/doc/cp.nvim.txt index 2956d4b..3e450ce 100644 --- a/doc/cp.nvim.txt +++ b/doc/cp.nvim.txt @@ -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 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", diff --git a/lua/cp/config.lua b/lua/cp/config.lua index 764f324..5b3b584 100644 --- a/lua/cp/config.lua +++ b/lua/cp/config.lua @@ -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 = '', 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 diff --git a/lua/cp/ui/edit.lua b/lua/cp/ui/edit.lua index a19d06d..ada41bb 100644 --- a/lua/cp/ui/edit.lua +++ b/lua/cp/ui/edit.lua @@ -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 diff --git a/scripts/interact.py b/scripts/interact.py index 4c24173..d4ddfa4 100644 --- a/scripts/interact.py +++ b/scripts/interact.py @@ -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))