feat: :CP test -> :CP run

This commit is contained in:
Barrett Ruth 2025-09-19 18:53:39 -04:00
parent ef3d39c7f4
commit dd6bf47684
7 changed files with 64 additions and 90 deletions

View file

@ -49,7 +49,7 @@ Setup Commands ~
Action Commands ~ Action Commands ~
:CP test [--debug] Toggle test panel for individual test case :CP run [--debug] Toggle run panel for individual test case
debugging. Shows per-test results with redesigned debugging. Shows per-test results with redesigned
layout for efficient comparison. layout for efficient comparison.
Use --debug flag to compile with debug flags Use --debug flag to compile with debug flags
@ -112,9 +112,8 @@ Optional configuration with lazy.nvim: >
vim.diagnostic.enable(false) vim.diagnostic.enable(false)
end, end,
}, },
test_panel = { run_panel = {
diff_mode = "vim", -- "vim" or "git" diff_mode = "vim", -- "vim" or "git"
toggle_key = "t", -- toggle test panel
next_test_key = "<c-n>", -- navigate to next test case next_test_key = "<c-n>", -- navigate to next test case
prev_test_key = "<c-p>", -- navigate to previous test case prev_test_key = "<c-p>", -- navigate to previous test case
}, },
@ -141,7 +140,7 @@ Optional configuration with lazy.nvim: >
during operation. during operation.
• {scrapers} (`table<string,boolean>`) Per-platform scraper control. • {scrapers} (`table<string,boolean>`) Per-platform scraper control.
Default enables all platforms. Default enables all platforms.
• {test_panel} (`TestPanelConfig`) Test panel behavior configuration. • {run_panel} (`RunPanelConfig`) Test panel behavior configuration.
• {diff} (`DiffConfig`) Diff backend configuration. • {diff} (`DiffConfig`) Diff backend configuration.
• {filename}? (`function`) Custom filename generation function. • {filename}? (`function`) Custom filename generation function.
`function(contest, contest_id, problem_id, config, language)` `function(contest, contest_id, problem_id, config, language)`
@ -169,12 +168,11 @@ Optional configuration with lazy.nvim: >
• {extension} (`string`) File extension (e.g. "cc", "py"). • {extension} (`string`) File extension (e.g. "cc", "py").
• {executable}? (`string`) Executable name for interpreted languages. • {executable}? (`string`) Executable name for interpreted languages.
*cp.TestPanelConfig* *cp.RunPanelConfig*
Fields: ~ Fields: ~
• {diff_mode} (`string`, default: `"vim"`) Diff backend: "vim" or "git". • {diff_mode} (`string`, default: `"vim"`) Diff backend: "vim" or "git".
Git provides character-level precision, vim uses built-in diff. Git provides character-level precision, vim uses built-in diff.
• {toggle_key} (`string`, default: `"t"`) Key to toggle test panel.
• {next_test_key} (`string`, default: `"<c-n>"`) Key to navigate to next test case. • {next_test_key} (`string`, default: `"<c-n>"`) Key to navigate to next test case.
• {prev_test_key} (`string`, default: `"<c-p>"`) Key to navigate to previous test case. • {prev_test_key} (`string`, default: `"<c-p>"`) Key to navigate to previous test case.
@ -295,15 +293,15 @@ Example: Quick setup for single Codeforces problem >
:CP test " Test immediately :CP test " Test immediately
< <
TEST PANEL *cp-test* RUN PANEL *cp-run*
The test panel provides individual test case debugging with a streamlined The run panel provides individual test case debugging with a streamlined
layout optimized for modern screens. Shows test status with competitive layout optimized for modern screens. Shows test status with competitive
programming terminology and efficient space usage. programming terminology and efficient space usage.
Activation ~ Activation ~
*:CP-test* *:CP-run*
:CP test [--debug] Toggle test panel on/off. When activated, :CP run [--debug] Toggle run panel on/off. When activated,
replaces current layout with test interface. replaces current layout with test interface.
Automatically compiles and runs all tests. Automatically compiles and runs all tests.
Use --debug flag to compile with debug symbols Use --debug flag to compile with debug symbols
@ -312,7 +310,7 @@ Activation ~
Interface ~ Interface ~
The test panel uses a redesigned two-pane layout for efficient comparison: The run panel uses a redesigned two-pane layout for efficient comparison:
(note that the diff is indeed highlighted, not the weird amalgamation of (note that the diff is indeed highlighted, not the weird amalgamation of
characters below) > characters below) >
@ -338,10 +336,9 @@ Test cases use competitive programming terminology:
Keymaps ~ Keymaps ~
*cp-test-keys* *cp-test-keys*
<c-n> Navigate to next test case (configurable via test_panel.next_test_key) <c-n> Navigate to next test case (configurable via run_panel.next_test_key)
<c-p> Navigate to previous test case (configurable via test_panel.prev_test_key) <c-p> Navigate to previous test case (configurable via run_panel.prev_test_key)
q Exit test panel (restore layout) q Exit test panel (restore layout)
t Toggle test panel (configurable via test_panel.toggle_key)
Diff Modes ~ Diff Modes ~

View file

@ -31,9 +31,8 @@
---@field before_debug? fun(ctx: ProblemContext) ---@field before_debug? fun(ctx: ProblemContext)
---@field setup_code? fun(ctx: ProblemContext) ---@field setup_code? fun(ctx: ProblemContext)
---@class TestPanelConfig ---@class RunPanelConfig
---@field diff_mode "vim"|"git" Diff backend to use ---@field diff_mode "vim"|"git" Diff backend to use
---@field toggle_key string Key to toggle test panel
---@field next_test_key string Key to navigate to next test case ---@field next_test_key string Key to navigate to next test case
---@field prev_test_key string Key to navigate to previous test case ---@field prev_test_key string Key to navigate to previous test case
@ -51,7 +50,7 @@
---@field debug boolean ---@field debug boolean
---@field scrapers table<string, boolean> ---@field scrapers table<string, boolean>
---@field filename? fun(contest: string, contest_id: string, problem_id?: string, config: cp.Config, language?: string): string ---@field filename? fun(contest: string, contest_id: string, problem_id?: string, config: cp.Config, language?: string): string
---@field test_panel TestPanelConfig ---@field run_panel RunPanelConfig
---@field diff DiffConfig ---@field diff DiffConfig
---@class cp.UserConfig ---@class cp.UserConfig
@ -61,7 +60,7 @@
---@field debug? boolean ---@field debug? boolean
---@field scrapers? table<string, boolean> ---@field scrapers? table<string, boolean>
---@field filename? fun(contest: string, contest_id: string, problem_id?: string, config: cp.Config, language?: string): string ---@field filename? fun(contest: string, contest_id: string, problem_id?: string, config: cp.Config, language?: string): string
---@field test_panel? TestPanelConfig ---@field run_panel? RunPanelConfig
---@field diff? DiffConfig ---@field diff? DiffConfig
local M = {} local M = {}
@ -79,9 +78,8 @@ M.defaults = {
debug = false, debug = false,
scrapers = constants.PLATFORMS, scrapers = constants.PLATFORMS,
filename = nil, filename = nil,
test_panel = { run_panel = {
diff_mode = 'vim', diff_mode = 'vim',
toggle_key = 't',
next_test_key = '<c-n>', next_test_key = '<c-n>',
prev_test_key = '<c-p>', prev_test_key = '<c-p>',
}, },
@ -108,7 +106,7 @@ function M.setup(user_config)
debug = { user_config.debug, { 'boolean', 'nil' }, true }, debug = { user_config.debug, { 'boolean', 'nil' }, true },
scrapers = { user_config.scrapers, { 'table', 'nil' }, true }, scrapers = { user_config.scrapers, { 'table', 'nil' }, true },
filename = { user_config.filename, { 'function', 'nil' }, true }, filename = { user_config.filename, { 'function', 'nil' }, true },
test_panel = { user_config.test_panel, { 'table', 'nil' }, true }, run_panel = { user_config.run_panel, { 'table', 'nil' }, true },
diff = { user_config.diff, { 'table', 'nil' }, true }, diff = { user_config.diff, { 'table', 'nil' }, true },
}) })
@ -132,31 +130,24 @@ function M.setup(user_config)
}) })
end end
if user_config.test_panel then if user_config.run_panel then
vim.validate({ vim.validate({
diff_mode = { diff_mode = {
user_config.test_panel.diff_mode, user_config.run_panel.diff_mode,
function(value) function(value)
return vim.tbl_contains({ 'vim', 'git' }, value) return vim.tbl_contains({ 'vim', 'git' }, value)
end, end,
"diff_mode must be 'vim' or 'git'", "diff_mode must be 'vim' or 'git'",
}, },
toggle_key = {
user_config.test_panel.toggle_key,
function(value)
return type(value) == 'string' and value ~= ''
end,
'toggle_key must be a non-empty string',
},
next_test_key = { next_test_key = {
user_config.test_panel.next_test_key, user_config.run_panel.next_test_key,
function(value) function(value)
return type(value) == 'string' and value ~= '' return type(value) == 'string' and value ~= ''
end, end,
'next_test_key must be a non-empty string', 'next_test_key must be a non-empty string',
}, },
prev_test_key = { prev_test_key = {
user_config.test_panel.prev_test_key, user_config.run_panel.prev_test_key,
function(value) function(value)
return type(value) == 'string' and value ~= '' return type(value) == 'string' and value ~= ''
end, end,

View file

@ -25,7 +25,7 @@ local state = {
saved_session = nil, saved_session = nil,
test_cases = nil, test_cases = nil,
test_states = {}, test_states = {},
test_panel_active = false, run_panel_active = false,
} }
local constants = require('cp.constants') local constants = require('cp.constants')
@ -149,14 +149,14 @@ local function get_current_problem()
return filename return filename
end end
local function toggle_test_panel(is_debug) local function toggle_run_panel(is_debug)
if state.test_panel_active then if state.run_panel_active then
if state.saved_session then if state.saved_session then
vim.cmd(('source %s'):format(state.saved_session)) vim.cmd(('source %s'):format(state.saved_session))
vim.fn.delete(state.saved_session) vim.fn.delete(state.saved_session)
state.saved_session = nil state.saved_session = nil
end end
state.test_panel_active = false state.run_panel_active = false
logger.log('test panel closed') logger.log('test panel closed')
return return
end end
@ -249,7 +249,7 @@ local function toggle_test_panel(is_debug)
end end
local function update_expected_pane() local function update_expected_pane()
local test_state = test_module.get_test_panel_state() local test_state = test_module.get_run_panel_state()
local current_test = test_state.test_cases[test_state.current_index] local current_test = test_state.test_cases[test_state.current_index]
if not current_test then if not current_test then
@ -262,7 +262,7 @@ local function toggle_test_panel(is_debug)
update_buffer_content(test_buffers.expected_buf, expected_lines, {}) update_buffer_content(test_buffers.expected_buf, expected_lines, {})
local diff_backend = require('cp.diff') local diff_backend = require('cp.diff')
local backend = diff_backend.get_best_backend(config.test_panel.diff_mode) local backend = diff_backend.get_best_backend(config.run_panel.diff_mode)
if backend.name == 'vim' and current_test.status == 'fail' then if backend.name == 'vim' and current_test.status == 'fail' then
vim.api.nvim_set_option_value('diff', true, { win = test_windows.expected_win }) vim.api.nvim_set_option_value('diff', true, { win = test_windows.expected_win })
@ -272,7 +272,7 @@ local function toggle_test_panel(is_debug)
end end
local function update_actual_pane() local function update_actual_pane()
local test_state = test_module.get_test_panel_state() local test_state = test_module.get_run_panel_state()
local current_test = test_state.test_cases[test_state.current_index] local current_test = test_state.test_cases[test_state.current_index]
if not current_test then if not current_test then
@ -291,7 +291,7 @@ local function toggle_test_panel(is_debug)
if enable_diff then if enable_diff then
local diff_backend = require('cp.diff') local diff_backend = require('cp.diff')
local backend = diff_backend.get_best_backend(config.test_panel.diff_mode) local backend = diff_backend.get_best_backend(config.run_panel.diff_mode)
if backend.name == 'git' then if backend.name == 'git' then
local diff_result = backend.render(current_test.expected, current_test.actual) local diff_result = backend.render(current_test.expected, current_test.actual)
@ -321,14 +321,14 @@ local function toggle_test_panel(is_debug)
end end
end end
local function refresh_test_panel() local function refresh_run_panel()
if not test_buffers.tab_buf or not vim.api.nvim_buf_is_valid(test_buffers.tab_buf) then if not test_buffers.tab_buf or not vim.api.nvim_buf_is_valid(test_buffers.tab_buf) then
return return
end end
local test_render = require('cp.test_render') local test_render = require('cp.test_render')
test_render.setup_highlights() test_render.setup_highlights()
local test_state = test_module.get_test_panel_state() local test_state = test_module.get_run_panel_state()
local tab_lines, tab_highlights = test_render.render_test_list(test_state) local tab_lines, tab_highlights = test_render.render_test_list(test_state)
update_buffer_content(test_buffers.tab_buf, tab_lines, tab_highlights) update_buffer_content(test_buffers.tab_buf, tab_lines, tab_highlights)
@ -337,7 +337,7 @@ local function toggle_test_panel(is_debug)
end end
local function navigate_test_case(delta) local function navigate_test_case(delta)
local test_state = test_module.get_test_panel_state() local test_state = test_module.get_run_panel_state()
if #test_state.test_cases == 0 then if #test_state.test_cases == 0 then
return return
end end
@ -349,22 +349,19 @@ local function toggle_test_panel(is_debug)
test_state.current_index = 1 test_state.current_index = 1
end end
refresh_test_panel() refresh_run_panel()
end end
vim.keymap.set('n', config.test_panel.next_test_key, function() vim.keymap.set('n', config.run_panel.next_test_key, function()
navigate_test_case(1) navigate_test_case(1)
end, { buffer = test_buffers.tab_buf, silent = true }) end, { buffer = test_buffers.tab_buf, silent = true })
vim.keymap.set('n', config.test_panel.prev_test_key, function() vim.keymap.set('n', config.run_panel.prev_test_key, function()
navigate_test_case(-1) navigate_test_case(-1)
end, { buffer = test_buffers.tab_buf, silent = true }) end, { buffer = test_buffers.tab_buf, silent = true })
for _, buf in pairs(test_buffers) do for _, buf in pairs(test_buffers) do
vim.keymap.set('n', 'q', function() vim.keymap.set('n', 'q', function()
toggle_test_panel() toggle_run_panel()
end, { buffer = buf, silent = true })
vim.keymap.set('n', config.test_panel.toggle_key, function()
toggle_test_panel()
end, { buffer = buf, silent = true }) end, { buffer = buf, silent = true })
end end
@ -382,14 +379,14 @@ local function toggle_test_panel(is_debug)
test_module.run_all_test_cases(ctx, contest_config) test_module.run_all_test_cases(ctx, contest_config)
end end
refresh_test_panel() refresh_run_panel()
vim.api.nvim_set_current_win(test_windows.tab_win) vim.api.nvim_set_current_win(test_windows.tab_win)
state.test_panel_active = true state.run_panel_active = true
state.test_buffers = test_buffers state.test_buffers = test_buffers
state.test_windows = test_windows state.test_windows = test_windows
local test_state = test_module.get_test_panel_state() local test_state = test_module.get_run_panel_state()
logger.log(string.format('test panel opened (%d test cases)', #test_state.test_cases)) logger.log(string.format('test panel opened (%d test cases)', #test_state.test_cases))
end end
@ -556,8 +553,8 @@ function M.handle_command(opts)
end end
if cmd.type == 'action' then if cmd.type == 'action' then
if cmd.action == 'test' then if cmd.action == 'run' then
toggle_test_panel(cmd.debug) toggle_run_panel(cmd.debug)
elseif cmd.action == 'next' then elseif cmd.action == 'next' then
navigate_problem(1, cmd.language) navigate_problem(1, cmd.language)
elseif cmd.action == 'prev' then elseif cmd.action == 'prev' then

View file

@ -12,7 +12,7 @@
---@field signal string? ---@field signal string?
---@field timed_out boolean? ---@field timed_out boolean?
---@class TestPanelState ---@class RunPanelState
---@field test_cases TestCase[] ---@field test_cases TestCase[]
---@field current_index number ---@field current_index number
---@field buffer number? ---@field buffer number?
@ -24,8 +24,8 @@ local M = {}
local constants = require('cp.constants') local constants = require('cp.constants')
local logger = require('cp.log') local logger = require('cp.log')
---@type TestPanelState ---@type RunPanelState
local test_panel_state = { local run_panel_state = {
test_cases = {}, test_cases = {},
current_index = 1, current_index = 1,
buffer = nil, buffer = nil,
@ -227,8 +227,8 @@ function M.load_test_cases(ctx, state)
test_cases = parse_test_cases_from_files(ctx.input_file, ctx.expected_file) test_cases = parse_test_cases_from_files(ctx.input_file, ctx.expected_file)
end end
test_panel_state.test_cases = test_cases run_panel_state.test_cases = test_cases
test_panel_state.current_index = 1 run_panel_state.current_index = 1
logger.log(('loaded %d test case(s)'):format(#test_cases)) logger.log(('loaded %d test case(s)'):format(#test_cases))
return #test_cases > 0 return #test_cases > 0
@ -239,7 +239,7 @@ end
---@param index number ---@param index number
---@return boolean ---@return boolean
function M.run_test_case(ctx, contest_config, index) function M.run_test_case(ctx, contest_config, index)
local test_case = test_panel_state.test_cases[index] local test_case = run_panel_state.test_cases[index]
if not test_case then if not test_case then
return false return false
end end
@ -266,16 +266,16 @@ end
---@return TestCase[] ---@return TestCase[]
function M.run_all_test_cases(ctx, contest_config) function M.run_all_test_cases(ctx, contest_config)
local results = {} local results = {}
for i, _ in ipairs(test_panel_state.test_cases) do for i, _ in ipairs(run_panel_state.test_cases) do
M.run_test_case(ctx, contest_config, i) M.run_test_case(ctx, contest_config, i)
table.insert(results, test_panel_state.test_cases[i]) table.insert(results, run_panel_state.test_cases[i])
end end
return results return results
end end
---@return TestPanelState ---@return RunPanelState
function M.get_test_panel_state() function M.get_run_panel_state()
return test_panel_state return run_panel_state
end end
return M return M

View file

@ -201,7 +201,7 @@ local function data_row(c, idx, tc, is_current)
return line, hi return line, hi
end end
---@param test_state TestPanelState ---@param test_state RunPanelState
---@return string[], table[] lines and highlight positions ---@return string[], table[] lines and highlight positions
function M.render_test_list(test_state) function M.render_test_list(test_state)
local lines, highlights = {}, {} local lines, highlights = {}, {}

View file

@ -51,7 +51,7 @@ describe('cp command parsing', function()
describe('action commands', function() describe('action commands', function()
it('handles test action without error', function() it('handles test action without error', function()
local opts = { fargs = { 'test' } } local opts = { fargs = { 'run' } }
assert.has_no_errors(function() assert.has_no_errors(function()
cp.handle_command(opts) cp.handle_command(opts)
@ -126,7 +126,7 @@ describe('cp command parsing', function()
describe('language flag parsing', function() describe('language flag parsing', function()
it('logs error for --lang flag missing value', function() it('logs error for --lang flag missing value', function()
local opts = { fargs = { 'test', '--lang' } } local opts = { fargs = { 'run', '--lang' } }
cp.handle_command(opts) cp.handle_command(opts)
@ -169,7 +169,7 @@ describe('cp command parsing', function()
describe('debug flag parsing', function() describe('debug flag parsing', function()
it('handles debug flag without error', function() it('handles debug flag without error', function()
local opts = { fargs = { 'test', '--debug' } } local opts = { fargs = { 'run', '--debug' } }
assert.has_no_errors(function() assert.has_no_errors(function()
cp.handle_command(opts) cp.handle_command(opts)
@ -177,7 +177,7 @@ describe('cp command parsing', function()
end) end)
it('handles combined language and debug flags', function() it('handles combined language and debug flags', function()
local opts = { fargs = { 'test', '--lang=cpp', '--debug' } } local opts = { fargs = { 'run', '--lang=cpp', '--debug' } }
assert.has_no_errors(function() assert.has_no_errors(function()
cp.handle_command(opts) cp.handle_command(opts)
@ -234,7 +234,7 @@ describe('cp command parsing', function()
end) end)
it('handles flag order variations', function() it('handles flag order variations', function()
local opts = { fargs = { '--debug', 'test', '--lang=python' } } local opts = { fargs = { '--debug', 'run', '--lang=python' } }
assert.has_no_errors(function() assert.has_no_errors(function()
cp.handle_command(opts) cp.handle_command(opts)
@ -242,7 +242,7 @@ describe('cp command parsing', function()
end) end)
it('handles multiple language flags', function() it('handles multiple language flags', function()
local opts = { fargs = { 'test', '--lang=cpp', '--lang=python' } } local opts = { fargs = { 'run', '--lang=cpp', '--lang=python' } }
assert.has_no_errors(function() assert.has_no_errors(function()
cp.handle_command(opts) cp.handle_command(opts)

View file

@ -74,20 +74,10 @@ describe('cp.config', function()
end) end)
end) end)
describe('test_panel config validation', function() describe('run_panel config validation', function()
it('validates diff_mode values', function() it('validates diff_mode values', function()
local invalid_config = { local invalid_config = {
test_panel = { diff_mode = 'invalid' }, run_panel = { diff_mode = 'invalid' },
}
assert.has_error(function()
config.setup(invalid_config)
end)
end)
it('validates toggle_key is non-empty string', function()
local invalid_config = {
test_panel = { toggle_key = '' },
} }
assert.has_error(function() assert.has_error(function()
@ -97,7 +87,7 @@ describe('cp.config', function()
it('validates next_test_key is non-empty string', function() it('validates next_test_key is non-empty string', function()
local invalid_config = { local invalid_config = {
test_panel = { next_test_key = nil }, run_panel = { next_test_key = nil },
} }
assert.has_error(function() assert.has_error(function()
@ -107,7 +97,7 @@ describe('cp.config', function()
it('validates prev_test_key is non-empty string', function() it('validates prev_test_key is non-empty string', function()
local invalid_config = { local invalid_config = {
test_panel = { prev_test_key = '' }, run_panel = { prev_test_key = '' },
} }
assert.has_error(function() assert.has_error(function()
@ -115,11 +105,10 @@ describe('cp.config', function()
end) end)
end) end)
it('accepts valid test_panel config', function() it('accepts valid run_panel config', function()
local valid_config = { local valid_config = {
test_panel = { run_panel = {
diff_mode = 'git', diff_mode = 'git',
toggle_key = 'x',
next_test_key = 'j', next_test_key = 'j',
prev_test_key = 'k', prev_test_key = 'k',
}, },