From 6a6cf2c5943b65d5d87e7152be9e19e9a486a0c2 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Thu, 23 Oct 2025 23:36:09 -0400 Subject: [PATCH] feat: bindings and --debug flag --- doc/cp.nvim.txt | 49 +++++++++++++++++++++++++++++++-------- lua/cp/commands/init.lua | 43 +++++++++++++++++++--------------- lua/cp/config.lua | 18 ++++++++++++++ lua/cp/constants.lua | 2 +- lua/cp/runner/execute.lua | 10 ++++---- lua/cp/runner/run.lua | 15 +++++++----- lua/cp/state.lua | 7 ++++++ lua/cp/ui/panel.lua | 44 ++++++++++++++++++++++++++++++----- 8 files changed, 142 insertions(+), 46 deletions(-) diff --git a/doc/cp.nvim.txt b/doc/cp.nvim.txt index 2ab4ee4..3ef99be 100644 --- a/doc/cp.nvim.txt +++ b/doc/cp.nvim.txt @@ -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/.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/.run + • Debug build: commands.debug → outputs to build/.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 ~ diff --git a/lua/cp/commands/init.lua b/lua/cp/commands/init.lua index 319f952..ad612a7 100644 --- a/lua/cp/commands/init.lua +++ b/lua/cp/commands/init.lua @@ -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 diff --git a/lua/cp/config.lua b/lua/cp/config.lua index 54515ae..26767e7 100644 --- a/lua/cp/config.lua +++ b/lua/cp/config.lua @@ -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' } }, diff --git a/lua/cp/constants.lua b/lua/cp/constants.lua index 310363f..b19e06b 100644 --- a/lua/cp/constants.lua +++ b/lua/cp/constants.lua @@ -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', diff --git a/lua/cp/runner/execute.lua b/lua/cp/runner/execute.lua index bfe0178..a871d6f 100644 --- a/lua/cp/runner/execute.lua +++ b/lua/cp/runner/execute.lua @@ -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 diff --git a/lua/cp/runner/run.lua b/lua/cp/runner/run.lua index ab3d8af..c8fcd0c 100644 --- a/lua/cp/runner/run.lua +++ b/lua/cp/runner/run.lua @@ -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 diff --git a/lua/cp/state.lua b/lua/cp/state.lua index 4a3ff79..caf5044 100644 --- a/lua/cp/state.lua +++ b/lua/cp/state.lua @@ -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() diff --git a/lua/cp/ui/panel.lua b/lua/cp/ui/panel.lua index 12093d7..5954ca8 100644 --- a/lua/cp/ui/panel.lua +++ b/lua/cp/ui/panel.lua @@ -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', '', function() + navigate_test(1) + end, { buffer = output_buf, silent = true, desc = 'Next test' }) + vim.keymap.set('n', '', function() + navigate_test(-1) + end, { buffer = output_buf, silent = true, desc = 'Previous test' }) + vim.keymap.set('n', '', function() + navigate_test(1) + end, { buffer = input_buf, silent = true, desc = 'Next test' }) + vim.keymap.set('n', '', 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