Merge pull request #164 from barrett-ruth/feat/debug

`--debug` flag
This commit is contained in:
Barrett Ruth 2025-10-23 23:59:09 -04:00 committed by GitHub
commit f9a1f79aef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 192 additions and 54 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*
@ -155,6 +184,11 @@ Here's an example configuration with lazy.nvim:
debug = false,
ui = {
ansi = true,
run = {
width = 0.3,
next_test_key = '<c-n>', -- or nil to disable
prev_test_key = '<c-p>', -- or nil to disable
},
panel = {
diff_mode = 'vim',
max_output_lines = 50,
@ -249,10 +283,20 @@ run CSES problems with Rust using the single schema:
Fields: ~
{ansi} (boolean, default: true) Enable ANSI color parsing
and highlighting in both I/O view and panel.
{run} (|RunConfig|) I/O view configuration.
{panel} (|PanelConfig|) Test panel behavior configuration.
{diff} (|DiffConfig|) Diff backend configuration.
{picker} (string|nil) 'telescope', 'fzf-lua', or nil.
*RunConfig*
Fields: ~
{width} (number, default: 0.3) Width of I/O view splits as
fraction of screen (0.0 to 1.0).
{next_test_key} (string|nil, default: '<c-n>') Keymap to navigate
to next test in I/O view. Set to nil to disable.
{prev_test_key} (string|nil, default: '<c-p>') Keymap to navigate
to previous test in I/O view. Set to nil to disable.
*cp.PanelConfig*
Fields: ~
{diff_mode} (string, default: "none") Diff backend: "none",
@ -416,6 +460,12 @@ Usage ~
:CP run Run all tests
:CP run 3 Run test 3 only
Navigation ~
While in the I/O view buffers, use the configured keymaps to cycle through tests:
<c-n> Next test (default, see |RunConfig|.next_test_key)
<c-p> Previous test (default, see |RunConfig|.prev_test_key)
Buffer Customization ~
Use the setup_io_input and setup_io_output hooks (see |cp.Hooks|) to customize
@ -446,7 +496,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

@ -17,6 +17,7 @@ local actions = constants.ACTIONS
---@field problem_id? string
---@field interactor_cmd? string
---@field test_index? integer
---@field debug? boolean
--- Turn raw args into normalized structure to later dispatch
---@param args string[] The raw command-line mode args
@ -53,23 +54,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
@ -122,16 +130,14 @@ function M.handle_command(opts)
restore.restore_from_current_file()
elseif cmd.type == 'action' then
local setup = require('cp.setup')
local ui = require('cp.ui.panel')
local ui = require('cp.ui.views')
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,14 @@
---@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
---@field next_test_key string|nil
---@field prev_test_key string|nil
---@class CpUI
---@field ansi boolean
---@field run RunConfig
---@field panel PanelConfig
---@field diff DiffConfig
---@field picker string|nil
@ -116,6 +122,7 @@ M.defaults = {
filename = nil,
ui = {
ansi = true,
run = { width = 0.3, next_test_key = '<c-n>', prev_test_key = '<c-p>' },
panel = { diff_mode = 'none', max_output_lines = 50 },
diff = {
git = {
@ -157,6 +164,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 +244,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' } },
@ -260,6 +280,20 @@ function M.setup(user_config)
'positive integer',
},
git = { cfg.ui.diff.git, { 'table' } },
next_test_key = {
cfg.ui.run.next_test_key,
function(v)
return v == nil or (type(v) == 'string' and #v > 0)
end,
'nil or non-empty string',
},
prev_test_key = {
cfg.ui.run.prev_test_key,
function(v)
return v == nil or (type(v) == 'string' and #v > 0)
end,
'nil or non-empty string',
},
})
for id, lang in pairs(cfg.languages) do

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

@ -203,7 +203,7 @@ function M.setup_problem(problem_id, language)
state.get_problem_id() or '',
lang
)
require('cp.ui.panel').ensure_io_view()
require('cp.ui.views').ensure_io_view()
end
state.set_provisional(nil)
return
@ -271,7 +271,7 @@ function M.navigate_problem(direction)
local active_panel = state.get_active_panel()
if active_panel == 'run' then
require('cp.ui.panel').disable()
require('cp.ui.views').disable()
end
M.setup_contest(platform, contest_id, problems[new_index].id)

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 cfg = config_module.get_config()
local width = math.floor(vim.o.columns * (cfg.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,15 +244,50 @@ 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()
if config.hooks and config.hooks.setup_io_output then
pcall(config.hooks.setup_io_output, output_buf, state)
if cfg.hooks and cfg.hooks.setup_io_output then
pcall(cfg.hooks.setup_io_output, output_buf, state)
end
if config.hooks and config.hooks.setup_io_input then
pcall(config.hooks.setup_io_input, input_buf, state)
if cfg.hooks and cfg.hooks.setup_io_input then
pcall(cfg.hooks.setup_io_input, input_buf, state)
end
local function navigate_test(delta)
local io_view_state = state.get_io_view_state()
if not io_view_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_view_state.current_test_index or 1) + delta
if new_index < 1 or new_index > #test_cases then
return
end
io_view_state.current_test_index = new_index
M.run_io_view(new_index)
end
if cfg.ui.run.next_test_key then
vim.keymap.set('n', cfg.ui.run.next_test_key, function()
navigate_test(1)
end, { buffer = output_buf, silent = true, desc = 'Next test' })
vim.keymap.set('n', cfg.ui.run.next_test_key, function()
navigate_test(1)
end, { buffer = input_buf, silent = true, desc = 'Next test' })
end
if cfg.ui.run.prev_test_key then
vim.keymap.set('n', cfg.ui.run.prev_test_key, function()
navigate_test(-1)
end, { buffer = output_buf, silent = true, desc = 'Previous test' })
vim.keymap.set('n', cfg.ui.run.prev_test_key, function()
navigate_test(-1)
end, { buffer = input_buf, silent = true, desc = 'Previous test' })
end
end
@ -272,7 +308,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 +368,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 +388,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 +665,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