fix(config): padding spacing

This commit is contained in:
Barrett Ruth 2025-10-24 14:44:33 -04:00
parent 3daf582b7a
commit 4b1b75fd6e
7 changed files with 320 additions and 9 deletions

View file

@ -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
<c-w> 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.

View file

@ -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()

View file

@ -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

View file

@ -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

231
lua/cp/ui/edit.lua Normal file
View file

@ -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 <platform> <contest> 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

View file

@ -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)

View file

@ -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()