feat: bindings and --debug flag

This commit is contained in:
Barrett Ruth 2025-10-23 23:36:09 -04:00
parent 038fcd36f8
commit 6a6cf2c594
8 changed files with 142 additions and 46 deletions

View file

@ -39,22 +39,25 @@ COMMANDS *cp-commands*
:CP atcoder abc324
<
View Commands ~
:CP run [n] Run tests in I/O view (see |cp-io-view|).
:CP run [--debug] [n]
Run tests in I/O view (see |cp-io-view|).
Lightweight split showing test verdicts.
Without [n]: runs all tests, shows verdict summary
With [n]: runs test n, shows detailed output
--debug: Use debug build (builds to build/<name>.dbg)
Examples: >
:CP run " All tests, verdict list
:CP run 3 " Test 3 detail
:CP run " All tests
:CP run --debug 2 " Test 2, debug build
<
:CP panel [n] Open full-screen test panel (see |cp-panel|).
:CP panel [--debug] [n]
Open full-screen test panel (see |cp-panel|).
Aggregate table with diff modes for detailed analysis.
Optional [n] focuses on specific test.
Example: >
:CP panel " All tests with diffs
:CP panel 2 " Focus on test 2
--debug: Use debug build (with sanitizers, etc.)
Examples: >
:CP panel " All tests
:CP panel --debug 3 " Test 3, debug build
<
:CP debug [n] Same as :CP panel but uses debug build configuration.
:CP pick Launch configured picker for interactive
platform/contest selection.
@ -97,13 +100,39 @@ Template Variables ~
Command templates support variable substitution using {variable} syntax:
• {source} Source file path (e.g. "abc324a.cpp")
• {binary} Output binary path (e.g. "build/abc324a.run")
• {binary} Output binary path (e.g. "build/abc324a.run" or
"build/abc324a.dbg" for debug builds)
Example template: >
build = { 'g++', '{source}', '-o', '{binary}', '-std=c++17' }
< Would expand to: >
g++ abc324a.cpp -o build/abc324a.run -std=c++17
<
Debug Builds ~
*cp-debug-builds*
The --debug flag uses the debug command configuration instead of build:
• Normal build: commands.build → outputs to build/<name>.run
• Debug build: commands.debug → outputs to build/<name>.dbg
Debug builds typically include sanitizers (address, undefined behavior) to
catch memory errors, buffer overflows, and other issues. Both binaries
coexist, so you can switch between normal and debug mode without
recompiling.
Example debug configuration: >
languages = {
cpp = {
extension = 'cc',
commands = {
build = { 'g++', '-std=c++17', '{source}', '-o', '{binary}' },
run = { '{binary}' },
debug = { 'g++', '-std=c++17', '-fsanitize=address,undefined',
'{source}', '-o', '{binary}' },
}
}
}
<
==============================================================================
CONFIGURATION *cp-config*
@ -446,7 +475,7 @@ The panel provides full-screen test analysis with diff modes for detailed
debugging. Problem time/memory limit constraints are in columns Time/Mem
respectively. Used time/memory are in columns Runtime/RSS respectively.
Access with :CP panel or :CP debug (uses debug build configuration).
Access with :CP panel or :CP panel --debug (uses debug build configuration).
Interface ~

View file

@ -53,23 +53,30 @@ local function parse_command(args)
else
return { type = 'action', action = 'interact' }
end
elseif first == 'run' then
local test_arg = args[2]
if test_arg then
local test_index = tonumber(test_arg)
if not test_index then
return {
type = 'error',
message = ("Test index '%s' is not a number"):format(test_index),
}
elseif first == 'run' or first == 'panel' then
local debug = false
local test_index = nil
for i = 2, #args do
local arg = args[i]
if arg == '--debug' then
debug = true
else
local idx = tonumber(arg)
if not idx then
return {
type = 'error',
message = ("Invalid argument '%s': expected test number or --debug"):format(arg),
}
end
if idx < 1 or idx ~= math.floor(idx) then
return { type = 'error', message = ("'%s' is not a valid test index"):format(idx) }
end
test_index = idx
end
if test_index < 1 or test_index ~= math.floor(test_index) then
return { type = 'error', message = ("'%s' is not a valid test index"):format(test_index) }
end
return { type = 'action', action = 'run', test_index = test_index }
else
return { type = 'action', action = 'run' }
end
return { type = 'action', action = first, test_index = test_index, debug = debug }
else
return { type = 'action', action = first }
end
@ -127,11 +134,9 @@ function M.handle_command(opts)
if cmd.action == 'interact' then
ui.toggle_interactive(cmd.interactor_cmd)
elseif cmd.action == 'run' then
ui.run_io_view(cmd.test_index)
ui.run_io_view(cmd.test_index, cmd.debug)
elseif cmd.action == 'panel' then
ui.toggle_panel()
elseif cmd.action == 'debug' then
ui.toggle_panel({ debug = true })
ui.toggle_panel({ debug = cmd.debug, test_index = cmd.test_index })
elseif cmd.action == 'next' then
setup.navigate_problem(1)
elseif cmd.action == 'prev' then

View file

@ -34,8 +34,12 @@
---@field setup_io_input? fun(bufnr: integer, state: cp.State)
---@field setup_io_output? fun(bufnr: integer, state: cp.State)
---@class RunConfig
---@field width number
---@class CpUI
---@field ansi boolean
---@field run RunConfig
---@field panel PanelConfig
---@field diff DiffConfig
---@field picker string|nil
@ -116,6 +120,7 @@ M.defaults = {
filename = nil,
ui = {
ansi = true,
run = { width = 0.3 },
panel = { diff_mode = 'none', max_output_lines = 50 },
diff = {
git = {
@ -157,6 +162,11 @@ local function validate_language(id, lang)
extension = { lang.extension, 'string' },
commands = { lang.commands, { 'table' } },
})
if not lang.commands.run then
error(('[cp.nvim] languages.%s.commands.run is required'):format(id))
end
if lang.commands.build ~= nil then
vim.validate({ build = { lang.commands.build, { 'table' } } })
if not has_tokens(lang.commands.build, { '{source}', '{binary}' }) then
@ -232,6 +242,14 @@ function M.setup(user_config)
vim.validate({ user_config = { user_config, { 'table', 'nil' }, true } })
local cfg = vim.tbl_deep_extend('force', vim.deepcopy(M.defaults), user_config or {})
if not next(cfg.languages) then
error('[cp.nvim] At least one language must be configured')
end
if not next(cfg.platforms) then
error('[cp.nvim] At least one platform must be configured')
end
vim.validate({
hooks = { cfg.hooks, { 'table' } },
ui = { cfg.ui, { 'table' } },

View file

@ -1,7 +1,7 @@
local M = {}
M.PLATFORMS = { 'atcoder', 'codeforces', 'cses' }
M.ACTIONS = { 'run', 'panel', 'debug', 'next', 'prev', 'pick', 'cache', 'interact' }
M.ACTIONS = { 'run', 'panel', 'next', 'prev', 'pick', 'cache', 'interact' }
M.PLATFORM_DISPLAY_NAMES = {
atcoder = 'AtCoder',

View file

@ -160,19 +160,21 @@ function M.run(cmd, stdin, timeout_ms, memory_mb)
}
end
function M.compile_problem()
function M.compile_problem(debug)
local state = require('cp.state')
local config = require('cp.config').get_config()
local platform = state.get_platform() or ''
local platform = state.get_platform()
local language = config.platforms[platform].default_language
local eff = config.runtime.effective[platform][language]
local compile_config = eff and eff.commands and eff.commands.build
local compile_config = (debug and eff.commands.debug) or eff.commands.build
if not compile_config then
return { success = true, output = nil }
end
local substitutions = { source = state.get_source_file(), binary = state.get_binary_file() }
local binary = debug and state.get_debug_file() or state.get_binary_file()
local substitutions = { source = state.get_source_file(), binary = binary }
local r = M.compile(compile_config, substitutions)
if r.code ~= 0 then

View file

@ -100,11 +100,12 @@ local function build_command(cmd, substitutions)
end
---@param test_case RanTestCase
---@param debug boolean?
---@return { status: "pass"|"fail"|"tle"|"mle", actual: string, actual_highlights: Highlight[], error: string, stderr: string, time_ms: number, code: integer, ok: boolean, signal: string, tled: boolean, mled: boolean, rss_mb: number }
local function run_single_test_case(test_case)
local function run_single_test_case(test_case, debug)
local source_file = state.get_source_file()
local binary_file = state.get_binary_file()
local binary_file = debug and state.get_debug_file() or state.get_binary_file()
local substitutions = { source = source_file, binary = binary_file }
local platform_config = config.platforms[state.get_platform() or '']
@ -198,15 +199,16 @@ function M.load_test_cases()
end
---@param index number
---@param debug boolean?
---@return boolean
function M.run_test_case(index)
function M.run_test_case(index, debug)
local tc = panel_state.test_cases[index]
if not tc then
return false
end
tc.status = 'running'
local r = run_single_test_case(tc)
local r = run_single_test_case(tc, debug)
tc.status = r.status
tc.actual = r.actual
@ -225,8 +227,9 @@ function M.run_test_case(index)
end
---@param indices? integer[]
---@param debug boolean?
---@return RanTestCase[]
function M.run_all_test_cases(indices)
function M.run_all_test_cases(indices, debug)
local to_run = indices
if not to_run then
to_run = {}
@ -236,7 +239,7 @@ function M.run_all_test_cases(indices)
end
for _, i in ipairs(to_run) do
M.run_test_case(i)
M.run_test_case(i, debug)
end
return panel_state.test_cases

View file

@ -11,6 +11,7 @@
---@field input_buf integer
---@field output_win integer
---@field input_win integer
---@field current_test_index integer?
---@class cp.State
---@field get_platform fun(): string?
@ -127,6 +128,12 @@ function M.get_binary_file()
return base_name and ('build/%s.run'):format(base_name) or nil
end
---@return string?
function M.get_debug_file()
local base_name = M.get_base_name()
return base_name and ('build/%s.dbg'):format(base_name) or nil
end
---@return string?
function M.get_input_file()
local base_name = M.get_base_name()

View file

@ -228,7 +228,8 @@ function M.ensure_io_view()
vim.cmd.vsplit()
output_win = vim.api.nvim_get_current_win()
local width = math.floor(vim.o.columns * 0.3)
local config = config_module.get_config()
local width = math.floor(vim.o.columns * (config.ui.run.width or 0.3))
vim.api.nvim_win_set_width(output_win, width)
output_buf = utils.create_buffer_with_options()
vim.api.nvim_win_set_buf(output_win, output_buf)
@ -243,6 +244,7 @@ function M.ensure_io_view()
input_buf = input_buf,
output_win = output_win,
input_win = input_win,
current_test_index = 1,
})
local config = config_module.get_config()
@ -253,6 +255,36 @@ function M.ensure_io_view()
if config.hooks and config.hooks.setup_io_input then
pcall(config.hooks.setup_io_input, input_buf, state)
end
local function navigate_test(delta)
local io_state = state.get_io_view_state()
if not io_state then
return
end
local test_cases = cache.get_test_cases(platform, contest_id, problem_id)
if not test_cases or #test_cases == 0 then
return
end
local new_index = (io_state.current_test_index or 1) + delta
if new_index < 1 or new_index > #test_cases then
return
end
io_state.current_test_index = new_index
M.run_io_view(new_index)
end
vim.keymap.set('n', '<c-n>', function()
navigate_test(1)
end, { buffer = output_buf, silent = true, desc = 'Next test' })
vim.keymap.set('n', '<c-p>', function()
navigate_test(-1)
end, { buffer = output_buf, silent = true, desc = 'Previous test' })
vim.keymap.set('n', '<c-n>', function()
navigate_test(1)
end, { buffer = input_buf, silent = true, desc = 'Next test' })
vim.keymap.set('n', '<c-p>', function()
navigate_test(-1)
end, { buffer = input_buf, silent = true, desc = 'Previous test' })
end
utils.update_buffer_content(input_buf, {})
@ -272,7 +304,7 @@ function M.ensure_io_view()
vim.api.nvim_set_current_win(solution_win)
end
function M.run_io_view(test_index)
function M.run_io_view(test_index, debug)
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
@ -332,7 +364,7 @@ function M.run_io_view(test_index)
end
local execute = require('cp.runner.execute')
local compile_result = execute.compile_problem()
local compile_result = execute.compile_problem(debug)
if not compile_result.success then
local ansi = require('cp.ui.ansi')
local output = compile_result.output or ''
@ -352,7 +384,7 @@ function M.run_io_view(test_index)
return
end
run.run_all_test_cases(test_indices)
run.run_all_test_cases(test_indices, debug)
local run_render = require('cp.runner.run_render')
run_render.setup_highlights()
@ -629,9 +661,9 @@ function M.toggle_panel(panel_opts)
end
local execute = require('cp.runner.execute')
local compile_result = execute.compile_problem()
local compile_result = execute.compile_problem(panel_opts and panel_opts.debug)
if compile_result.success then
run.run_all_test_cases()
run.run_all_test_cases(nil, panel_opts and panel_opts.debug)
else
run.handle_compilation_failure(compile_result.output)
end