From 9cf9cd84411f2bdf76e733d4eb81e99e9ff6616b Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Thu, 5 Mar 2026 18:23:26 -0500 Subject: [PATCH 1/4] refactor(commands): replace hardcoded CONTEST_ACTIONS with requires_context field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: the local CONTEST_ACTIONS list in `handle_command` was a manually maintained subset of ACTIONS that could drift — `pick` was already in it incorrectly, requiring a separate hotfix. Solution: encode `requires_context` on `ParsedCommand` at each parse-site in `parse_command`, where the action's semantics are already known. `handle_command` now checks `cmd.requires_context` directly, eliminating the parallel list. --- lua/cp/commands/init.lua | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/lua/cp/commands/init.lua b/lua/cp/commands/init.lua index de6d307..364608d 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,15 @@ 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 +122,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 +235,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 +261,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 +273,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 +333,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 From 73be6b82773ef00506240b25c6596f56c02c663e Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Thu, 5 Mar 2026 18:23:39 -0500 Subject: [PATCH 2/4] fix(setup): cancel all active panels on problem navigation Problem: `navigate_problem` only called `views.disable()` for the `'run'` panel; interactive and stress terminals were left alive when stepping through problems with `:CP next/prev`. In-flight `run_io_view` callbacks were also not invalidated since `is_new_contest` stays false for same-contest navigation, so the generation-counter guard in `setup_contest` never fired. Solution: call `cancel_io_view()` unconditionally in `navigate_problem` and expand the panel dispatch to cover `'interactive'` and `'stress'` alongside `'run'`, mirroring the contest-switch logic in `setup_contest`. --- lua/cp/setup.lua | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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 From 4381b9f8614f3e7e055266eec0686a609565ba08 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Thu, 5 Mar 2026 18:23:50 -0500 Subject: [PATCH 3/4] fix(completion): deduplicate pick and cache in second-arg candidates Problem: `pick` and `cache` were inserted unconditionally into the candidate list, then inserted again via `vim.list_extend(candidates, actions)` when a contest was active, producing duplicates in the completion menu. Solution: filter `pick` and `cache` out of the `actions` extend since they are already present in the always-visible candidate set. --- plugin/cp.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugin/cp.lua b/plugin/cp.lua index 60efb7a..1484da3 100644 --- a/plugin/cp.lua +++ b/plugin/cp.lua @@ -44,7 +44,9 @@ 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) From 871fa321bd3e395761f5a5db4050bf812ad179b9 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Thu, 5 Mar 2026 18:29:07 -0500 Subject: [PATCH 4/4] ci: format --- lua/cp/commands/init.lua | 7 ++++++- plugin/cp.lua | 9 ++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/lua/cp/commands/init.lua b/lua/cp/commands/init.lua index 364608d..5006cc9 100644 --- a/lua/cp/commands/init.lua +++ b/lua/cp/commands/init.lua @@ -95,7 +95,12 @@ local function parse_command(args) elseif first == 'interact' then local inter = args[2] if inter and inter ~= '' then - return { type = 'action', action = 'interact', requires_context = true, interactor_cmd = inter } + return { + type = 'action', + action = 'interact', + requires_context = true, + interactor_cmd = inter, + } else return { type = 'action', action = 'interact', requires_context = true } end diff --git a/plugin/cp.lua b/plugin/cp.lua index 1484da3..3e3b56c 100644 --- a/plugin/cp.lua +++ b/plugin/cp.lua @@ -44,9 +44,12 @@ end, { table.insert(candidates, 'cache') table.insert(candidates, 'pick') if platform and contest_id then - vim.list_extend(candidates, vim.tbl_filter(function(a) - return a ~= 'pick' and a ~= 'cache' - end, 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)