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 :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*
@ -155,6 +184,11 @@ Here's an example configuration with lazy.nvim:
debug = false, debug = false,
ui = { ui = {
ansi = true, 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 = { panel = {
diff_mode = 'vim', diff_mode = 'vim',
max_output_lines = 50, max_output_lines = 50,
@ -249,10 +283,20 @@ run CSES problems with Rust using the single schema:
Fields: ~ Fields: ~
{ansi} (boolean, default: true) Enable ANSI color parsing {ansi} (boolean, default: true) Enable ANSI color parsing
and highlighting in both I/O view and panel. and highlighting in both I/O view and panel.
{run} (|RunConfig|) I/O view configuration.
{panel} (|PanelConfig|) Test panel behavior configuration. {panel} (|PanelConfig|) Test panel behavior configuration.
{diff} (|DiffConfig|) Diff backend configuration. {diff} (|DiffConfig|) Diff backend configuration.
{picker} (string|nil) 'telescope', 'fzf-lua', or nil. {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* *cp.PanelConfig*
Fields: ~ Fields: ~
{diff_mode} (string, default: "none") Diff backend: "none", {diff_mode} (string, default: "none") Diff backend: "none",
@ -416,6 +460,12 @@ Usage ~
:CP run Run all tests :CP run Run all tests
:CP run 3 Run test 3 only :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 ~ Buffer Customization ~
Use the setup_io_input and setup_io_output hooks (see |cp.Hooks|) to customize 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 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

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

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

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

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 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) 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,15 +244,50 @@ 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() if cfg.hooks and cfg.hooks.setup_io_output then
if config.hooks and config.hooks.setup_io_output then pcall(cfg.hooks.setup_io_output, output_buf, state)
pcall(config.hooks.setup_io_output, output_buf, state)
end end
if config.hooks and config.hooks.setup_io_input then if cfg.hooks and cfg.hooks.setup_io_input then
pcall(config.hooks.setup_io_input, input_buf, state) 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
end end
@ -272,7 +308,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 +368,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 +388,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 +665,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