feat(config): merge platform config model and add disabled-platform guard

Problem: Supplying any `platforms` table silently dropped all unlisted
platforms, making it easy to accidentally disable platforms. Disabled
platforms also produced no user-facing error on invocation.

Solution: Switch to a merge model — all six platforms are enabled by
default and user entries are deep-merged on top. Set a platform key to
`false` to disable it explicitly. Add a `check_platform_enabled` guard
in `handle_command` for contest fetch, login, logout, and race actions.
This commit is contained in:
Barrett Ruth 2026-03-06 16:08:11 -05:00
parent 82640709d6
commit 1c8b5cda3e
Signed by: barrett
GPG key ID: A6C96C9349D2FC81
3 changed files with 74 additions and 29 deletions

View file

@ -92,26 +92,6 @@ Configuration is done via `vim.g.cp`. Set this before using the plugin:
cpp = { extension = 'cpp', commands = { build = { ... } } } cpp = { extension = 'cpp', commands = { build = { ... } } }
}, },
}, },
atcoder = {
enabled_languages = { 'cpp', 'python' },
default_language = 'cpp',
},
codeforces = {
enabled_languages = { 'cpp', 'python' },
default_language = 'cpp',
},
codechef = {
enabled_languages = { 'cpp', 'python' },
default_language = 'cpp',
},
usaco = {
enabled_languages = { 'cpp', 'python' },
default_language = 'cpp',
},
kattis = {
enabled_languages = { 'cpp', 'python' },
default_language = 'cpp',
},
}, },
debug = false, debug = false,
ui = { ui = {
@ -137,21 +117,25 @@ Configuration is done via `vim.g.cp`. Set this before using the plugin:
< <
By default, C++ (g++ with ISO C++17) and Python are preconfigured under By default, C++ (g++ with ISO C++17) and Python are preconfigured under
'languages'. Platforms select which languages are enabled and which one is 'languages'. All six platforms are enabled by default. User-supplied
the default; per-platform overrides can tweak 'extension' or 'commands'. platform entries are merged on top of the defaults — you only need to
specify what you want to change. To disable a platform entirely, set it
to `false`.
For example, to run CodeForces contests with Python by default: For example, to run Codeforces contests with Python by default and
disable CodeChef:
>lua >lua
vim.g.cp = { vim.g.cp = {
platforms = { platforms = {
codeforces = { codeforces = {
default_language = 'python', default_language = 'python',
}, },
codechef = false,
}, },
} }
< <
Any language is supported provided the proper configuration. For example, to Any language is supported provided the proper configuration. For example, to
run CSES problems with Rust using the single schema: add Rust and use it by default on CSES:
>lua >lua
vim.g.cp = { vim.g.cp = {
languages = { languages = {
@ -175,8 +159,11 @@ run CSES problems with Rust using the single schema:
Fields: ~ Fields: ~
{languages} (table<string,|CpLanguage|>) Global language registry. {languages} (table<string,|CpLanguage|>) Global language registry.
Each language provides an {extension} and {commands}. Each language provides an {extension} and {commands}.
{platforms} (table<string,|CpPlatform|>) Per-platform enablement, {platforms} (table<string,|CpPlatform||false>) All six platforms
default language, and optional overrides. are enabled by default. Each entry is merged on top
of the platform defaults — omitted fields keep their
defaults and unmentioned platforms stay enabled. Set
a platform key to `false` to disable it entirely.
{hooks} (|cp.Hooks|) Hook functions called at various stages. {hooks} (|cp.Hooks|) Hook functions called at various stages.
{debug} (boolean, default: false) Show info messages. {debug} (boolean, default: false) Show info messages.
{scrapers} (string[]) Supported platform ids. {scrapers} (string[]) Supported platform ids.
@ -476,6 +463,14 @@ COMMANDS *cp-commands*
If [platform] is omitted, uses the active platform. If [platform] is omitted, uses the active platform.
Examples: > Examples: >
:CP logout atcoder :CP logout atcoder
<
:CP {platform} signup
Open the platform's registration page in the
browser via |vim.ui.open|. Works even if
{platform} is not enabled in your config.
Examples: >
:CP atcoder signup
:CP codeforces signup
< <
Submit Commands ~ Submit Commands ~
:CP submit [--lang {language}] :CP submit [--lang {language}]
@ -1019,6 +1014,12 @@ Credentials are stored under _credentials in the main cache file
Remove stored credentials for a platform. Remove stored credentials for a platform.
Omit [platform] to use the currently active platform. Omit [platform] to use the currently active platform.
:CP {platform} signup
Open the platform's account registration page in the browser via
|vim.ui.open|. Works even if {platform} is not enabled in your
config. {platform} is one of: atcoder, codechef, codeforces, cses,
kattis, usaco.
============================================================================== ==============================================================================
SUBMIT *cp-submit* SUBMIT *cp-submit*

View file

@ -283,7 +283,7 @@ local function parse_command(args)
message = 'Too few arguments - specify a contest.', message = 'Too few arguments - specify a contest.',
} }
elseif #args == 2 then elseif #args == 2 then
if args[2] == 'login' or args[2] == 'logout' then if args[2] == 'login' or args[2] == 'logout' or args[2] == 'signup' then
return { type = 'action', action = args[2], requires_context = false, platform = first } return { type = 'action', action = args[2], requires_context = false, platform = first }
end end
local contest = args[2] local contest = args[2]
@ -330,6 +330,22 @@ local function parse_command(args)
return { type = 'error', message = 'Unknown command or no contest context.' } return { type = 'error', message = 'Unknown command or no contest context.' }
end end
---@param platform string
---@return boolean
local function check_platform_enabled(platform)
local cfg = require('cp.config').get_config()
if not cfg.platforms[platform] then
logger.log(
("Platform '%s' is not enabled. Add it to vim.g.cp.platforms to enable it."):format(
constants.PLATFORM_DISPLAY_NAMES[platform] or platform
),
{ level = vim.log.levels.ERROR }
)
return false
end
return true
end
--- Core logic for handling `:CP ...` commands --- Core logic for handling `:CP ...` commands
---@return nil ---@return nil
function M.handle_command(opts) function M.handle_command(opts)
@ -378,6 +394,9 @@ function M.handle_command(opts)
elseif cmd.action == 'submit' then elseif cmd.action == 'submit' then
require('cp.submit').submit({ language = cmd.language }) require('cp.submit').submit({ language = cmd.language })
elseif cmd.action == 'race' then elseif cmd.action == 'race' then
if not check_platform_enabled(cmd.platform) then
return
end
require('cp.race').start(cmd.platform, cmd.contest, cmd.language) require('cp.race').start(cmd.platform, cmd.contest, cmd.language)
elseif cmd.action == 'race_stop' then elseif cmd.action == 'race_stop' then
require('cp.race').stop() require('cp.race').stop()
@ -396,9 +415,25 @@ function M.handle_command(opts)
end end
vim.ui.open(url) vim.ui.open(url)
elseif cmd.action == 'login' then elseif cmd.action == 'login' then
if not check_platform_enabled(cmd.platform) then
return
end
require('cp.credentials').login(cmd.platform) require('cp.credentials').login(cmd.platform)
elseif cmd.action == 'logout' then elseif cmd.action == 'logout' then
if not check_platform_enabled(cmd.platform) then
return
end
require('cp.credentials').logout(cmd.platform) require('cp.credentials').logout(cmd.platform)
elseif cmd.action == 'signup' then
local url = constants.SIGNUP_URLS[cmd.platform]
if not url then
logger.log(
("No signup URL available for '%s'"):format(cmd.platform),
{ level = vim.log.levels.WARN }
)
return
end
vim.ui.open(url)
end end
elseif cmd.type == 'problem_jump' then elseif cmd.type == 'problem_jump' then
local platform = state.get_platform() local platform = state.get_platform()
@ -432,6 +467,9 @@ function M.handle_command(opts)
local cache_commands = require('cp.commands.cache') local cache_commands = require('cp.commands.cache')
cache_commands.handle_cache_command(cmd) cache_commands.handle_cache_command(cmd)
elseif cmd.type == 'contest_setup' then elseif cmd.type == 'contest_setup' then
if not check_platform_enabled(cmd.platform) then
return
end
local setup = require('cp.setup') local setup = require('cp.setup')
setup.setup_contest(cmd.platform, cmd.contest, nil, cmd.language) setup.setup_contest(cmd.platform, cmd.contest, nil, cmd.language)
return return

View file

@ -107,6 +107,7 @@
---@field runtime { effective: table<string, table<string, CpLanguage>> } -- computed ---@field runtime { effective: table<string, table<string, CpLanguage>> } -- computed
---@class cp.PartialConfig: cp.Config ---@class cp.PartialConfig: cp.Config
---@field platforms? table<string, CpPlatform|false>
local M = {} local M = {}
@ -333,13 +334,18 @@ function M.setup(user_config)
vim.validate({ user_config = { user_config, { 'table', 'nil' }, true } }) vim.validate({ user_config = { user_config, { 'table', 'nil' }, true } })
local defaults = vim.deepcopy(M.defaults) local defaults = vim.deepcopy(M.defaults)
if user_config and user_config.platforms then if user_config and user_config.platforms then
for plat in pairs(defaults.platforms) do for plat, v in pairs(user_config.platforms) do
if not user_config.platforms[plat] then if v == false then
defaults.platforms[plat] = nil defaults.platforms[plat] = nil
end end
end end
end end
local cfg = vim.tbl_deep_extend('force', defaults, user_config or {}) local cfg = vim.tbl_deep_extend('force', defaults, user_config or {})
for plat, v in pairs(cfg.platforms) do
if v == false then
cfg.platforms[plat] = nil
end
end
if not next(cfg.languages) then if not next(cfg.languages) then
error('[cp.nvim] At least one language must be configured') error('[cp.nvim] At least one language must be configured')