From 7e7e135681ddca3a69bc39c98c071d9683e4cd7e Mon Sep 17 00:00:00 2001 From: Barrett Ruth <62671086+barrettruth@users.noreply.github.com> Date: Thu, 5 Mar 2026 17:42:50 -0500 Subject: [PATCH] fix: cancel active process on contest switch (#316) ## Problem Switching contests while a run, interactive session, or stress test was active left orphaned callbacks and terminal jobs alive. Running `:CP run` twice also let the first run's stale output overwrite the buffer. ## Solution Replace the `io_view_running` bool in `views.lua` with a generation counter so concurrent `run_io_view` calls self-cancel via stale-gen checks in async callbacks. Add `cancel_io_view`, `cancel_interactive`, and `stress.cancel` for forceful teardown, and call them in `setup_contest` whenever `is_new_contest` is true. --- lua/cp/setup.lua | 13 ++++++++++++ lua/cp/stress.lua | 14 +++++++++++++ lua/cp/ui/views.lua | 48 ++++++++++++++++++++++++++++----------------- 3 files changed, 57 insertions(+), 18 deletions(-) diff --git a/lua/cp/setup.lua b/lua/cp/setup.lua index 7a33cd0..1609e61 100644 --- a/lua/cp/setup.lua +++ b/lua/cp/setup.lua @@ -179,6 +179,19 @@ function M.setup_contest(platform, contest_id, problem_id, language) local is_new_contest = old_platform ~= platform or old_contest_id ~= contest_id + if is_new_contest then + local views = require('cp.ui.views') + views.cancel_io_view() + local active = state.get_active_panel() + if active == 'interactive' then + views.cancel_interactive() + elseif active == 'stress' then + require('cp.stress').cancel() + elseif active == 'run' then + views.disable() + end + end + cache.load() local function proceed(contest_data) diff --git a/lua/cp/stress.lua b/lua/cp/stress.lua index 9063d0d..ec3cba5 100644 --- a/lua/cp/stress.lua +++ b/lua/cp/stress.lua @@ -232,4 +232,18 @@ function M.toggle(generator_cmd, brute_cmd) end) end +function M.cancel() + if state.stress_buf and vim.api.nvim_buf_is_valid(state.stress_buf) then + local job = vim.b[state.stress_buf].terminal_job_id + if job then + vim.fn.jobstop(job) + end + end + if state.saved_stress_session then + vim.fn.delete(state.saved_stress_session) + state.saved_stress_session = nil + end + state.set_active_panel(nil) +end + return M diff --git a/lua/cp/ui/views.lua b/lua/cp/ui/views.lua index b2ee23d..599bded 100644 --- a/lua/cp/ui/views.lua +++ b/lua/cp/ui/views.lua @@ -14,7 +14,7 @@ local utils = require('cp.utils') local current_diff_layout = nil local current_mode = nil -local io_view_running = false +local _run_gen = 0 function M.disable() local active_panel = state.get_active_panel() @@ -599,11 +599,8 @@ local function render_io_view_results(io_state, test_indices, mode, combined_res end function M.run_io_view(test_indices_arg, debug, mode) - if io_view_running then - logger.log('Tests already running', { level = vim.log.levels.WARN }) - return - end - io_view_running = true + _run_gen = _run_gen + 1 + local gen = _run_gen logger.log( ('%s tests...'):format(debug and 'Debugging' or 'Running'), @@ -619,7 +616,6 @@ function M.run_io_view(test_indices_arg, debug, mode) 'No platform/contest/problem configured. Use :CP [...] first.', { level = vim.log.levels.ERROR } ) - io_view_running = false return end @@ -627,7 +623,6 @@ function M.run_io_view(test_indices_arg, debug, mode) local contest_data = cache.get_contest_data(platform, contest_id) if not contest_data or not contest_data.index_map then logger.log('No test cases available.', { level = vim.log.levels.ERROR }) - io_view_running = false return end @@ -644,13 +639,11 @@ function M.run_io_view(test_indices_arg, debug, mode) local combined = cache.get_combined_test(platform, contest_id, problem_id) if not combined then logger.log('No combined test available', { level = vim.log.levels.ERROR }) - io_view_running = false return end else if not run.load_test_cases() then logger.log('No test cases available', { level = vim.log.levels.ERROR }) - io_view_running = false return end end @@ -671,7 +664,6 @@ function M.run_io_view(test_indices_arg, debug, mode) ), { level = vim.log.levels.WARN } ) - io_view_running = false return end end @@ -689,7 +681,6 @@ function M.run_io_view(test_indices_arg, debug, mode) local io_state = state.get_io_view_state() if not io_state then - io_view_running = false return end @@ -702,8 +693,10 @@ function M.run_io_view(test_indices_arg, debug, mode) local execute = require('cp.runner.execute') execute.compile_problem(debug, function(compile_result) + if gen ~= _run_gen then + return + end if not vim.api.nvim_buf_is_valid(io_state.output_buf) then - io_view_running = false return end @@ -723,7 +716,6 @@ function M.run_io_view(test_indices_arg, debug, mode) local ns = vim.api.nvim_create_namespace('cp_io_view_compile_error') utils.update_buffer_content(io_state.output_buf, lines, highlights, ns) - io_view_running = false return end @@ -731,35 +723,55 @@ function M.run_io_view(test_indices_arg, debug, mode) local combined = cache.get_combined_test(platform, contest_id, problem_id) if not combined then logger.log('No combined test found', { level = vim.log.levels.ERROR }) - io_view_running = false return end run.load_test_cases() run.run_combined_test(debug, function(result) + if gen ~= _run_gen then + return + end if not result then logger.log('Failed to run combined test', { level = vim.log.levels.ERROR }) - io_view_running = false return end if vim.api.nvim_buf_is_valid(io_state.output_buf) then render_io_view_results(io_state, test_indices, mode, result, combined.input) end - io_view_running = false end) else run.run_all_test_cases(test_indices, debug, nil, function() + if gen ~= _run_gen then + return + end if vim.api.nvim_buf_is_valid(io_state.output_buf) then render_io_view_results(io_state, test_indices, mode, nil, nil) end - io_view_running = false end) end end) end +function M.cancel_io_view() + _run_gen = _run_gen + 1 +end + +function M.cancel_interactive() + if state.interactive_buf and vim.api.nvim_buf_is_valid(state.interactive_buf) then + local job = vim.b[state.interactive_buf].terminal_job_id + if job then + vim.fn.jobstop(job) + end + end + if state.saved_interactive_session then + vim.fn.delete(state.saved_interactive_session) + state.saved_interactive_session = nil + end + state.set_active_panel(nil) +end + ---@param panel_opts? PanelOpts function M.toggle_panel(panel_opts) if state.get_active_panel() == 'run' then