From 6c036a7b2e03db01c2e49ba6b919526019b86acb Mon Sep 17 00:00:00 2001 From: Barrett Ruth <62671086+barrettruth@users.noreply.github.com> Date: Thu, 5 Mar 2026 18:29:41 -0500 Subject: [PATCH] fix: systematic context guards and full panel lifecycle (#317) ## Problem The `CONTEST_ACTIONS` list in `handle_command` was a manually maintained parallel of `ACTIONS` that had already drifted (`pick` was incorrectly included, breaking `:CP pick` cold). `navigate_problem` only cancelled the `'run'` panel on `next`/`prev`, leaving interactive and stress terminals alive and in-flight `run_io_view` callbacks unguarded. `pick` and `cache` also appeared twice in tab-completion when a contest was active. ## Solution Replace `CONTEST_ACTIONS` with a `requires_context` field on `ParsedCommand`, set at each parse-site in `parse_command` so the semantics live with the action definition. Expand `navigate_problem` to call `cancel_io_view()` and dispatch `cancel_interactive`/`stress.cancel` for all panel types, mirroring the contest-switch logic. Filter already-present `pick` and `cache` from the context-conditional completion candidates. --- lua/cp/commands/init.lua | 36 +++++++++++++++++++++++++++--------- lua/cp/setup.lua | 8 +++++++- plugin/cp.lua | 7 ++++++- 3 files changed, 40 insertions(+), 11 deletions(-) diff --git a/lua/cp/commands/init.lua b/lua/cp/commands/init.lua index de6d307..5006cc9 100644 --- a/lua/cp/commands/init.lua +++ b/lua/cp/commands/init.lua @@ -11,6 +11,7 @@ local actions = constants.ACTIONS ---@field type string ---@field error string? ---@field action? string +---@field requires_context? boolean ---@field message? string ---@field contest? string ---@field platform? string @@ -71,7 +72,7 @@ local function parse_command(args) end elseif first == 'race' then if args[2] == 'stop' then - return { type = 'action', action = 'race_stop' } + return { type = 'action', action = 'race_stop', requires_context = false } end if not args[2] or not args[3] then return { @@ -86,6 +87,7 @@ local function parse_command(args) return { type = 'action', action = 'race', + requires_context = false, platform = args[2], contest = args[3], language = language, @@ -93,14 +95,20 @@ local function parse_command(args) elseif first == 'interact' then local inter = args[2] if inter and inter ~= '' then - return { type = 'action', action = 'interact', interactor_cmd = inter } + return { + type = 'action', + action = 'interact', + requires_context = true, + interactor_cmd = inter, + } else - return { type = 'action', action = 'interact' } + return { type = 'action', action = 'interact', requires_context = true } end elseif first == 'stress' then return { type = 'action', action = 'stress', + requires_context = true, generator_cmd = args[2], brute_cmd = args[3], } @@ -119,7 +127,7 @@ local function parse_command(args) end test_index = idx end - return { type = 'action', action = 'edit', test_index = test_index } + return { type = 'action', action = 'edit', requires_context = true, test_index = test_index } elseif first == 'run' or first == 'panel' then local debug = false local test_indices = nil @@ -232,10 +240,22 @@ local function parse_command(args) return { type = 'action', action = first, + requires_context = true, test_indices = test_indices, debug = debug, mode = mode, } + elseif first == 'pick' then + local language = nil + if #args >= 3 and args[2] == '--lang' then + language = args[3] + elseif #args >= 2 and args[2] ~= nil and args[2]:sub(1, 2) ~= '--' then + return { + type = 'error', + message = ("Unknown argument '%s' for action '%s'"):format(args[2], first), + } + end + return { type = 'action', action = 'pick', requires_context = false, language = language } else local language = nil if #args >= 3 and args[2] == '--lang' then @@ -246,7 +266,7 @@ local function parse_command(args) message = ("Unknown argument '%s' for action '%s'"):format(args[2], first), } end - return { type = 'action', action = first, language = language } + return { type = 'action', action = first, requires_context = true, language = language } end end @@ -258,7 +278,7 @@ local function parse_command(args) } elseif #args == 2 then if args[2] == 'login' or args[2] == 'logout' then - return { type = 'action', action = args[2], platform = first } + return { type = 'action', action = args[2], requires_context = false, platform = first } end local contest = args[2] if first == 'codeforces' then @@ -318,9 +338,7 @@ function M.handle_command(opts) local restore = require('cp.restore') restore.restore_from_current_file() elseif cmd.type == 'action' then - local CONTEST_ACTIONS = - { 'run', 'panel', 'edit', 'interact', 'stress', 'submit', 'next', 'prev', 'pick' } - if vim.tbl_contains(CONTEST_ACTIONS, cmd.action) and not state.get_platform() then + if cmd.requires_context and not state.get_platform() then local restore = require('cp.restore') if not restore.restore_from_current_file() then return diff --git a/lua/cp/setup.lua b/lua/cp/setup.lua index 1609e61..a954d62 100644 --- a/lua/cp/setup.lua +++ b/lua/cp/setup.lua @@ -477,9 +477,15 @@ function M.navigate_problem(direction, language) logger.log(('navigate_problem: %s -> %s'):format(current_problem_id, problems[new_index].id)) + local views = require('cp.ui.views') + views.cancel_io_view() local active_panel = state.get_active_panel() if active_panel == 'run' then - require('cp.ui.views').disable() + views.disable() + elseif active_panel == 'interactive' then + views.cancel_interactive() + elseif active_panel == 'stress' then + require('cp.stress').cancel() end local lang = nil diff --git a/plugin/cp.lua b/plugin/cp.lua index 60efb7a..3e3b56c 100644 --- a/plugin/cp.lua +++ b/plugin/cp.lua @@ -44,7 +44,12 @@ end, { table.insert(candidates, 'cache') table.insert(candidates, 'pick') if platform and contest_id then - vim.list_extend(candidates, actions) + vim.list_extend( + candidates, + vim.tbl_filter(function(a) + return a ~= 'pick' and a ~= 'cache' + end, actions) + ) local cache = require('cp.cache') cache.load() local contest_data = cache.get_contest_data(platform, contest_id)