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 :CP atcoder abc324
< <
View Commands ~ 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. Lightweight split showing test verdicts.
Without [n]: runs all tests, shows verdict summary Without [n]: runs all tests, shows verdict summary
With [n]: runs test n, shows detailed output With [n]: runs test n, shows detailed output
--debug: Use debug build (builds to build/<name>.dbg)
Examples: > Examples: >
:CP run " All tests, verdict list :CP run " All tests
:CP run 3 " Test 3 detail :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. Aggregate table with diff modes for detailed analysis.
Optional [n] focuses on specific test. Optional [n] focuses on specific test.
Example: > --debug: Use debug build (with sanitizers, etc.)
:CP panel " All tests with diffs Examples: >
:CP panel 2 " Focus on test 2 :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 :CP pick Launch configured picker for interactive
platform/contest selection. platform/contest selection.
@ -97,13 +100,39 @@ Template Variables ~
Command templates support variable substitution using {variable} syntax: Command templates support variable substitution using {variable} syntax:
• {source} Source file path (e.g. "abc324a.cpp") • {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: > Example template: >
build = { 'g++', '{source}', '-o', '{binary}', '-std=c++17' } build = { 'g++', '{source}', '-o', '{binary}', '-std=c++17' }
< Would expand to: > < Would expand to: >
g++ abc324a.cpp -o build/abc324a.run -std=c++17 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* 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 debugging. Problem time/memory limit constraints are in columns Time/Mem
respectively. Used time/memory are in columns Runtime/RSS respectively. 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 ~ Interface ~

View file

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

View file

@ -34,8 +34,12 @@
---@field setup_io_input? fun(bufnr: integer, state: cp.State) ---@field setup_io_input? fun(bufnr: integer, state: cp.State)
---@field setup_io_output? fun(bufnr: integer, state: cp.State) ---@field setup_io_output? fun(bufnr: integer, state: cp.State)
---@class RunConfig
---@field width number
---@class CpUI ---@class CpUI
---@field ansi boolean ---@field ansi boolean
---@field run RunConfig
---@field panel PanelConfig ---@field panel PanelConfig
---@field diff DiffConfig ---@field diff DiffConfig
---@field picker string|nil ---@field picker string|nil
@ -116,6 +120,7 @@ M.defaults = {
filename = nil, filename = nil,
ui = { ui = {
ansi = true, ansi = true,
run = { width = 0.3 },
panel = { diff_mode = 'none', max_output_lines = 50 }, panel = { diff_mode = 'none', max_output_lines = 50 },
diff = { diff = {
git = { git = {
@ -157,6 +162,11 @@ local function validate_language(id, lang)
extension = { lang.extension, 'string' }, extension = { lang.extension, 'string' },
commands = { lang.commands, { 'table' } }, 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 if lang.commands.build ~= nil then
vim.validate({ build = { lang.commands.build, { 'table' } } }) vim.validate({ build = { lang.commands.build, { 'table' } } })
if not has_tokens(lang.commands.build, { '{source}', '{binary}' }) then 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 } }) vim.validate({ user_config = { user_config, { 'table', 'nil' }, true } })
local cfg = vim.tbl_deep_extend('force', vim.deepcopy(M.defaults), user_config or {}) 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({ vim.validate({
hooks = { cfg.hooks, { 'table' } }, hooks = { cfg.hooks, { 'table' } },
ui = { cfg.ui, { 'table' } }, ui = { cfg.ui, { 'table' } },

View file

@ -1,7 +1,7 @@
local M = {} local M = {}
M.PLATFORMS = { 'atcoder', 'codeforces', 'cses' } 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 = { M.PLATFORM_DISPLAY_NAMES = {
atcoder = 'AtCoder', atcoder = 'AtCoder',

View file

@ -160,19 +160,21 @@ function M.run(cmd, stdin, timeout_ms, memory_mb)
} }
end end
function M.compile_problem() function M.compile_problem(debug)
local state = require('cp.state') local state = require('cp.state')
local config = require('cp.config').get_config() 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 language = config.platforms[platform].default_language
local eff = config.runtime.effective[platform][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 if not compile_config then
return { success = true, output = nil } return { success = true, output = nil }
end 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) local r = M.compile(compile_config, substitutions)
if r.code ~= 0 then if r.code ~= 0 then

View file

@ -100,11 +100,12 @@ local function build_command(cmd, substitutions)
end end
---@param test_case RanTestCase ---@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 } ---@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 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 substitutions = { source = source_file, binary = binary_file }
local platform_config = config.platforms[state.get_platform() or ''] local platform_config = config.platforms[state.get_platform() or '']
@ -198,15 +199,16 @@ function M.load_test_cases()
end end
---@param index number ---@param index number
---@param debug boolean?
---@return boolean ---@return boolean
function M.run_test_case(index) function M.run_test_case(index, debug)
local tc = panel_state.test_cases[index] local tc = panel_state.test_cases[index]
if not tc then if not tc then
return false return false
end end
tc.status = 'running' tc.status = 'running'
local r = run_single_test_case(tc) local r = run_single_test_case(tc, debug)
tc.status = r.status tc.status = r.status
tc.actual = r.actual tc.actual = r.actual
@ -225,8 +227,9 @@ function M.run_test_case(index)
end end
---@param indices? integer[] ---@param indices? integer[]
---@param debug boolean?
---@return RanTestCase[] ---@return RanTestCase[]
function M.run_all_test_cases(indices) function M.run_all_test_cases(indices, debug)
local to_run = indices local to_run = indices
if not to_run then if not to_run then
to_run = {} to_run = {}
@ -236,7 +239,7 @@ function M.run_all_test_cases(indices)
end end
for _, i in ipairs(to_run) do for _, i in ipairs(to_run) do
M.run_test_case(i) M.run_test_case(i, debug)
end end
return panel_state.test_cases return panel_state.test_cases

View file

@ -11,6 +11,7 @@
---@field input_buf integer ---@field input_buf integer
---@field output_win integer ---@field output_win integer
---@field input_win integer ---@field input_win integer
---@field current_test_index integer?
---@class cp.State ---@class cp.State
---@field get_platform fun(): string? ---@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 return base_name and ('build/%s.run'):format(base_name) or nil
end 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? ---@return string?
function M.get_input_file() function M.get_input_file()
local base_name = M.get_base_name() local base_name = M.get_base_name()

View file

@ -228,7 +228,8 @@ function M.ensure_io_view()
vim.cmd.vsplit() vim.cmd.vsplit()
output_win = vim.api.nvim_get_current_win() 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) vim.api.nvim_win_set_width(output_win, width)
output_buf = utils.create_buffer_with_options() output_buf = utils.create_buffer_with_options()
vim.api.nvim_win_set_buf(output_win, output_buf) vim.api.nvim_win_set_buf(output_win, output_buf)
@ -243,6 +244,7 @@ function M.ensure_io_view()
input_buf = input_buf, input_buf = input_buf,
output_win = output_win, output_win = output_win,
input_win = input_win, input_win = input_win,
current_test_index = 1,
}) })
local config = config_module.get_config() 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 if config.hooks and config.hooks.setup_io_input then
pcall(config.hooks.setup_io_input, input_buf, state) pcall(config.hooks.setup_io_input, input_buf, state)
end 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 end
utils.update_buffer_content(input_buf, {}) utils.update_buffer_content(input_buf, {})
@ -272,7 +304,7 @@ function M.ensure_io_view()
vim.api.nvim_set_current_win(solution_win) vim.api.nvim_set_current_win(solution_win)
end end
function M.run_io_view(test_index) function M.run_io_view(test_index, debug)
local platform, contest_id, problem_id = local platform, contest_id, problem_id =
state.get_platform(), state.get_contest_id(), state.get_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 if not platform or not contest_id or not problem_id then
@ -332,7 +364,7 @@ function M.run_io_view(test_index)
end end
local execute = require('cp.runner.execute') 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 if not compile_result.success then
local ansi = require('cp.ui.ansi') local ansi = require('cp.ui.ansi')
local output = compile_result.output or '' local output = compile_result.output or ''
@ -352,7 +384,7 @@ function M.run_io_view(test_index)
return return
end end
run.run_all_test_cases(test_indices) run.run_all_test_cases(test_indices, debug)
local run_render = require('cp.runner.run_render') local run_render = require('cp.runner.run_render')
run_render.setup_highlights() run_render.setup_highlights()
@ -629,9 +661,9 @@ function M.toggle_panel(panel_opts)
end end
local execute = require('cp.runner.execute') 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 if compile_result.success then
run.run_all_test_cases() run.run_all_test_cases(nil, panel_opts and panel_opts.debug)
else else
run.handle_compilation_failure(compile_result.output) run.handle_compilation_failure(compile_result.output)
end end