From 4b1b75fd6ee0b328bfcd7c21c61e4f3ffcee7b38 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Fri, 24 Oct 2025 14:44:33 -0400 Subject: [PATCH] fix(config): padding spacing --- doc/cp.nvim.txt | 27 ++++- lua/cp/commands/init.lua | 3 + lua/cp/config.lua | 4 + lua/cp/helpers.lua | 24 +++- lua/cp/ui/edit.lua | 231 +++++++++++++++++++++++++++++++++++++++ lua/cp/ui/views.lua | 23 ++++ plugin/cp.lua | 17 +++ 7 files changed, 320 insertions(+), 9 deletions(-) create mode 100644 lua/cp/ui/edit.lua diff --git a/doc/cp.nvim.txt b/doc/cp.nvim.txt index a90ca9b..71d73d7 100644 --- a/doc/cp.nvim.txt +++ b/doc/cp.nvim.txt @@ -102,6 +102,27 @@ COMMANDS *cp-commands* :CP C --lang python < + Edit Commands ~ + :CP edit [n] + Open grid test editor showing all test cases. + Tests displayed as 2×N grid (2 rows, N columns): + • Top row: Test inputs (editable) + • Bottom row: Expected outputs (editable) + + Optional [n]: Jump cursor to test n's input buffer + + Changes saved to both cache and disk on exit, + taking effect immediately in :CP run and CLI. + + Keybindings: + q Save all and exit editor + Normal window navigation + + Examples: > + :CP edit " Edit all tests + :CP edit 3 " Edit all, start at test 3 +< + State Restoration ~ :CP Restore state from current file. Automatically detects platform, contest, problem, @@ -115,9 +136,9 @@ COMMANDS *cp-commands* • [platform]: Clear all data for a platform • [platform] [contest]: Clear specific contest Examples: > - :CP cache clear " Clear all - :CP cache clear codeforces " Clear CF - :CP cache clear codeforces 1848 " Clear CF 1848 + :CP cache clear + :CP cache clear codeforces + :CP cache clear codeforces 1848 < :CP cache read View the cache in a pretty-printed lua buffer. diff --git a/lua/cp/commands/init.lua b/lua/cp/commands/init.lua index 06e0be4..b585a5b 100644 --- a/lua/cp/commands/init.lua +++ b/lua/cp/commands/init.lua @@ -207,6 +207,9 @@ function M.handle_command(opts) elseif cmd.action == 'pick' then local picker = require('cp.commands.picker') picker.handle_pick_action(cmd.language) + elseif cmd.action == 'edit' then + local edit = require('cp.ui.edit') + edit.toggle_edit(cmd.test_index) end elseif cmd.type == 'problem_jump' then local platform = state.get_platform() diff --git a/lua/cp/config.lua b/lua/cp/config.lua index 46b13b4..764f324 100644 --- a/lua/cp/config.lua +++ b/lua/cp/config.lua @@ -43,6 +43,10 @@ ---@field memory_limit_mb number ---@field exit_code integer ---@field signal string|nil +---@field time_actual_width? integer +---@field time_limit_width? integer +---@field mem_actual_width? integer +---@field mem_limit_width? integer ---@class VerdictHighlight ---@field col_start integer diff --git a/lua/cp/helpers.lua b/lua/cp/helpers.lua index e31b6f9..070193c 100644 --- a/lua/cp/helpers.lua +++ b/lua/cp/helpers.lua @@ -51,17 +51,29 @@ end ---@param data VerdictFormatData ---@return VerdictFormatResult function M.default_verdict_formatter(data) - local time_data = string.format('%.2f', data.time_ms) .. '/' .. data.time_limit_ms - local mem_data = string.format('%.0f', data.memory_mb) - .. '/' - .. string.format('%.0f', data.memory_limit_mb) + local time_actual = string.format('%.2f', data.time_ms) + local time_limit = tostring(data.time_limit_ms) + local mem_actual = string.format('%.0f', data.memory_mb) + local mem_limit = string.format('%.0f', data.memory_limit_mb) local exit_str = data.signal and string.format('%d (%s)', data.exit_code, data.signal) or tostring(data.exit_code) + -- Use dynamic widths if provided, otherwise use reasonable defaults + local time_actual_w = data.time_actual_width or 6 + local time_limit_w = data.time_limit_width or 4 + local mem_actual_w = data.mem_actual_width or 3 + local mem_limit_w = data.mem_limit_width or 3 + local test_num_part = 'Test ' .. data.index .. ':' local status_part = M.pad_right(data.status.text, 3) - local time_part = time_data .. ' ms' - local mem_part = mem_data .. ' MB' + local time_part = M.pad_left(time_actual, time_actual_w) + .. '/' + .. M.pad_left(time_limit, time_limit_w) + .. ' ms' + local mem_part = M.pad_left(mem_actual, mem_actual_w) + .. '/' + .. M.pad_left(mem_limit, mem_limit_w) + .. ' MB' local exit_part = 'exit: ' .. exit_str local line = test_num_part diff --git a/lua/cp/ui/edit.lua b/lua/cp/ui/edit.lua new file mode 100644 index 0000000..897c4c4 --- /dev/null +++ b/lua/cp/ui/edit.lua @@ -0,0 +1,231 @@ +local M = {} + +local cache = require('cp.cache') +local config_module = require('cp.config') +local helpers = require('cp.helpers') +local logger = require('cp.log') +local state = require('cp.state') +local utils = require('cp.utils') + +---@class TestBufferPair +---@field input_buf integer +---@field expected_buf integer +---@field input_win integer +---@field expected_win integer + +---@class EditState +---@field test_buffers TestBufferPair[] +---@field test_cases TestCase[] +---@field constraints ProblemConstraints? + +---@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' }) +end + +local function load_test_into_buffer(test_index) + if not edit_state then + return + end + + local tc = edit_state.test_cases[test_index] + local pair = edit_state.test_buffers[test_index] + + if not tc or not pair then + return + end + + local input_lines = vim.split(tc.input or '', '\n', { plain = true, trimempty = false }) + vim.api.nvim_buf_set_lines(pair.input_buf, 0, -1, false, input_lines) + + local expected_lines = vim.split(tc.expected or '', '\n', { plain = true, trimempty = false }) + vim.api.nvim_buf_set_lines(pair.expected_buf, 0, -1, false, expected_lines) + + vim.api.nvim_buf_set_name(pair.input_buf, string.format('cp://test-%d-input', test_index)) + vim.api.nvim_buf_set_name(pair.expected_buf, string.format('cp://test-%d-expected', test_index)) +end + +local function save_all_tests() + if not edit_state then + return + end + + local platform = state.get_platform() + local contest_id = state.get_contest_id() + local problem_id = state.get_problem_id() + + if not platform or not contest_id or not problem_id then + return + end + + for i, pair in ipairs(edit_state.test_buffers) do + if + vim.api.nvim_buf_is_valid(pair.input_buf) and vim.api.nvim_buf_is_valid(pair.expected_buf) + then + local input_lines = vim.api.nvim_buf_get_lines(pair.input_buf, 0, -1, false) + local expected_lines = vim.api.nvim_buf_get_lines(pair.expected_buf, 0, -1, false) + + edit_state.test_cases[i].input = table.concat(input_lines, '\n') + edit_state.test_cases[i].expected = table.concat(expected_lines, '\n') + end + end + + cache.set_test_cases( + platform, + contest_id, + problem_id, + edit_state.test_cases, + edit_state.constraints and edit_state.constraints.timeout_ms or 0, + edit_state.constraints and edit_state.constraints.memory_mb or 0, + false + ) + + local config = config_module.get_config() + local base_name = config.filename and config.filename(platform, contest_id, problem_id, config) + or config_module.default_filename(contest_id, problem_id) + + vim.fn.mkdir('io', 'p') + + for i, tc in ipairs(edit_state.test_cases) do + local input_file = string.format('io/%s.%d.cpin', base_name, i) + local expected_file = string.format('io/%s.%d.cpout', base_name, i) + + local input_content = (tc.input or ''):gsub('\r', '') + local expected_content = (tc.expected or ''):gsub('\r', '') + + vim.fn.writefile(vim.split(input_content, '\n', { trimempty = true }), input_file) + vim.fn.writefile(vim.split(expected_content, '\n', { trimempty = true }), expected_file) + end + + logger.log('Saved all test cases') +end + +function M.toggle_edit(test_index) + if edit_state then + save_all_tests() + local saved = state.get_saved_session() + if saved then + vim.cmd(('source %s'):format(saved)) + vim.fn.delete(saved) + state.set_saved_session(nil) + end + edit_state = nil + logger.log('Closed test editor') + return + end + + local platform, contest_id, problem_id = + state.get_platform(), state.get_contest_id(), state.get_problem_id() + + if not platform or not contest_id or not problem_id then + logger.log('No problem context. Run :CP first.', vim.log.levels.ERROR) + return + end + + cache.load() + local test_cases = cache.get_test_cases(platform, contest_id, problem_id) + + if not test_cases or #test_cases == 0 then + logger.log('No test cases available for editing.', vim.log.levels.ERROR) + return + end + + local timeout_ms, memory_mb = cache.get_constraints(platform, contest_id, problem_id) + local constraints = (timeout_ms and memory_mb) + and { timeout_ms = timeout_ms, memory_mb = memory_mb } + or nil + + local target_index = test_index or 1 + if target_index < 1 or target_index > #test_cases then + logger.log( + ('Test %d does not exist (only %d tests available)'):format(target_index, #test_cases), + vim.log.levels.ERROR + ) + return + end + + local session_file = vim.fn.tempname() + state.set_saved_session(session_file) + vim.cmd(('mksession! %s'):format(session_file)) + vim.cmd('silent only') + + local test_buffers = {} + local num_tests = #test_cases + + -- Step 1: Create N columns (vsplit creates full-height columns) + for i = 1, num_tests - 1 do + vim.cmd('vsplit') + end + + -- Step 2: Go to leftmost window + vim.cmd('1wincmd w') + + -- Step 3: For each column, split horizontally into input (top) and expected (bottom) + for col = 1, num_tests do + -- Split current window horizontally + vim.cmd('split') + + -- After split, cursor is in bottom window. Go up to input window. + vim.cmd('wincmd k') + 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) + + -- Go down to expected window + vim.cmd('wincmd j') + 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) + + test_buffers[col] = { + input_buf = input_buf, + expected_buf = expected_buf, + input_win = input_win, + expected_win = expected_win, + } + + -- Move to next column (go up to top, then right) + vim.cmd('wincmd k') + vim.cmd('wincmd l') + end + + edit_state = { + test_buffers = test_buffers, + test_cases = test_cases, + constraints = constraints, + } + + for i = 1, num_tests do + load_test_into_buffer(i) + end + + for _, pair in ipairs(test_buffers) do + setup_keybindings(pair.input_buf) + setup_keybindings(pair.expected_buf) + end + + if + test_buffers[target_index] + and vim.api.nvim_win_is_valid(test_buffers[target_index].input_win) + then + vim.api.nvim_set_current_win(test_buffers[target_index].input_win) + end + + logger.log(('Editing %d test cases'):format(num_tests)) +end + +return M diff --git a/lua/cp/ui/views.lua b/lua/cp/ui/views.lua index 23e2fc6..4aa877f 100644 --- a/lua/cp/ui/views.lua +++ b/lua/cp/ui/views.lua @@ -400,6 +400,25 @@ function M.run_io_view(test_index, debug) local formatter = config.ui.run.format_verdict + local max_time_actual = 0 + local max_time_limit = 0 + local max_mem_actual = 0 + local max_mem_limit = 0 + + for _, idx in ipairs(test_indices) do + local tc = test_state.test_cases[idx] + max_time_actual = math.max(max_time_actual, #string.format('%.2f', tc.time_ms or 0)) + max_time_limit = math.max( + max_time_limit, + #tostring(test_state.constraints and test_state.constraints.timeout_ms or 0) + ) + max_mem_actual = math.max(max_mem_actual, #string.format('%.0f', tc.rss_mb or 0)) + max_mem_limit = math.max( + max_mem_limit, + #string.format('%.0f', test_state.constraints and test_state.constraints.memory_mb or 0) + ) + end + for _, idx in ipairs(test_indices) do local tc = test_state.test_cases[idx] @@ -425,6 +444,10 @@ function M.run_io_view(test_index, debug) exit_code = tc.code or 0, signal = (tc.code and tc.code >= 128) and require('cp.constants').signal_codes[tc.code] or nil, + time_actual_width = max_time_actual, + time_limit_width = max_time_limit, + mem_actual_width = max_mem_actual, + mem_limit_width = max_mem_limit, } local result = formatter(format_data) diff --git a/plugin/cp.lua b/plugin/cp.lua index 66023fc..b91a3d5 100644 --- a/plugin/cp.lua +++ b/plugin/cp.lua @@ -69,6 +69,23 @@ end, { elseif args[2] == 'interact' then local utils = require('cp.utils') return filter_candidates(utils.cwd_executables()) + elseif args[2] == 'edit' then + local state = require('cp.state') + local platform = state.get_platform() + local contest_id = state.get_contest_id() + local problem_id = state.get_problem_id() + local candidates = {} + if platform and contest_id and problem_id then + local cache = require('cp.cache') + cache.load() + local test_cases = cache.get_test_cases(platform, contest_id, problem_id) + if test_cases then + for i = 1, #test_cases do + table.insert(candidates, tostring(i)) + end + end + end + return filter_candidates(candidates) elseif args[2] == 'run' or args[2] == 'panel' then local state = require('cp.state') local platform = state.get_platform()