## Problem
Race was the only command using `:CP <action> <platform> <contest>`
syntax while every other platform+contest command uses `:CP <platform>
<contest> [flags]`.
## Solution
Make race a `--race` flag on the existing contest setup flow. Remove
`:CP race stop` — starting a new race auto-cancels any active one.
Remove `<Plug>(cp-race-stop)` keymap. Update tab completion and docs
accordingly.
## Problem
Two gaps in the plugin: (1) contest pickers have no way to know whether
a contest supports countdown (race), so the UI can't surface that
affordance; (2) `:CP submit` hardcodes a single language ID per platform
with no way to choose C++ standard or override the platform ID.
## Solution
**Race countdown** (`4e709c8`): Add `supports_countdown` boolean to
`ContestListResult` and wire it through CSES/USACO scrapers, cache, race
module, and pickers.
**Language version selection** (`b90ac67`): Add `LANGUAGE_VERSIONS` and
`DEFAULT_VERSIONS` tables in `constants.lua`. Config gains `version` and
`submit_id` fields validated at setup time. `submit.lua` resolves the
effective config to a platform ID (priority: `submit_id` > `version` >
default). Helpdocs add `*cp-submit-language*` section. Tests cover
`LANGUAGE_IDS` completeness.
## Problem
`:CP <platform> login` short-circuited on cached cookies/tokens — if an
old session was still valid, the new credentials were never tested. The
user got "login successful" even with wrong input. USACO submit also
wasted a roundtrip on `_check_usaco_login` every time.
## Solution
Always validate credentials in the login path. Remove cookie/token
loading from `_login_headless` (AtCoder), `_login_headless_cf` (CF),
`_login_headless_codechef` (CodeChef), and `login` (CSES). For USACO
submit, replace the `_check_usaco_login` roundtrip with cookie trust +
retry-on-auth-failure (the Kattis pattern). Submit paths are unchanged —
cookie fast-paths remain for contest speed.
Closes#331
## Problem
Kattis and USACO had zero offline test coverage — no fixtures, no
conftest
routers, and no entries in the test matrix. The `precision` field and
error
paths were also unverified across all platforms.
## Solution
Add HTML fixtures for both platforms and wire up `httpx.AsyncClient.get`
routers in `conftest.py` following the existing CSES/CodeChef pattern.
Extend the test matrix from 12 to 23 parametrized cases. Add a dedicated
test for the Kattis contest-vs-slug fallback path (verifying
`contest_url`
and `standings_url`), three parametrized metadata error cases, and a
targeted assertion that `extract_precision` returns a non-`None` float
for
problems with floating-point tolerance hints.
Closes#281.
## Problem
`:CP open` accepts `problem`, `contest`, and `standings` but offered no
tab completion for them.
## Solution
Add an `args[2] == 'open'` branch in the `plugin/cp.lua` completer
returning the three valid subcommands.
## Problem
No quick way to reach a platform's registration page from within Neovim.
## Solution
Add `signup` as a platform subcommand that calls `vim.ui.open` on the
platform's registration URL. URLs live in a new `SIGNUP_URLS` table in
`constants.lua`. Works even when the platform is disabled. Tab
completion and vimdoc updated.
## Problem
Supplying any `platforms` table silently dropped all unlisted platforms,
making it easy to accidentally disable platforms. Invoking a disabled
platform produced no user-facing error.
## 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.
## Problem
Kattis and USACO problem tests never extracted float precision, so
epsilon problems got no tolerance. Kattis `scrape_contest_metadata`
omitted `contest_url` and `standings_url`, breaking `:CP open
contest/standings`. Kattis submit always returned success even when the
server responded with an error (e.g. "You need to join the contest").
## Solution
Call `extract_precision` on problem HTML in both scrapers and emit it in
the JSON payload. Set `contest_url` and `standings_url` on Kattis
metadata paths. After Kattis submit, check for `Submission ID:` in the
response and surface the error text if absent.
## Problem
`codeforces.py` used `curl_cffi` to bypass Cloudflare when fetching
contest problem HTML, making it unavailable in the nix python env and
requiring an extra dependency across `pyproject.toml` and `flake.nix`.
## Solution
Rewrite `_fetch_problems_html` to use scrapling `StealthySession` with
`solve_cloudflare=True`, matching the existing CF submit pattern. Extend
`needs_browser` in `scraper.lua` to route CF `metadata` and `tests`
through the FHS env on NixOS. Remove `curl-cffi` from `pyproject.toml`,
`flake.nix`, and test mocks.
## Problem
`scrape_contest_list` made O(N) requests — one per Starters number up to
~200 — to discover division sub-contests via `child_contests`. `run_one`
also fetched problem HTML via `curl_cffi` solely to extract the memory
limit, which is unavailable in the nix python env.
## Solution
Use `/api/list/contests/all` directly: filter to `^START\d+$` codes and
map to `ContestSummary` in a single request. Remove `_fetch_html_sync`,
`MEMORY_LIMIT_RE`, and `_extract_memory_limit`; hardcode `memory_mb =
256.0` and `precision = None` in `run_one`.
## Problem
`ContestSummary.display_name` defaults to `None`, which serializes to
JSON `null` → Lua `vim.NIL`. The contest picker displayed "vim.NIL" for
every entry. Additionally, `start_time` was always stored even when
null, because `vim.NIL` is truthy in Lua.
## Solution
Pass `display_name=name` explicitly in `_parse_contests_page` so JSON
never emits `null`. In `cache.lua` `set_contest_summaries`, coerce
`display_name` via a `~= vim.NIL` guard and apply the same guard before
storing `start_time`.
## Problem
Kattis and USACO login and submit were broken in multiple ways
discovered
during manual end-to-end testing. Neither platform could successfully
authenticate or submit through the plugin.
## Solution
**Kattis:** switch login from `POST /login/email` (requires CSRF fetch)
to
`POST /login` with `script=true` (200 = success, 403 = bad credentials);
remove `_check_kattis_login` entirely since Kattis blocks all GET
requests
from httpx; add submit retry on `"Request validation failed"` to handle
expired sessions; fix language ID `"C++17"` → `"C++"`.
**USACO:** fix login field `user` → `uname`; fix success check to
`code==1`;
fix submit endpoint to `submit-solution.php`, file field to
`sourcefile`,
hidden field extraction off-by-one (`group(2)` → `group(1)`); fix
`_pick_lang_option` loop order (keywords outer, options inner) so
specific
keywords like `"c++17"` match before broad ones like `"c++"`.
**`submit.lua`:** absolutize source file path via `fnamemodify(...,
':p')`
before passing to the scraper — Python is spawned with `cwd=plugin_path`
so relative paths silently fail with `FileNotFoundError`.
**Both platforms:** remove cookie fast path from `login` subcommand so
credentials are always validated, preventing stale cookies from masking
wrong credentials.
## Problem
Login and submit were stub-only for USACO, Kattis, and CodeChef, leaving
three platforms without a working solve loop.
## Solution
Three commits, one per platform:
**USACO** — httpx-based login via `login-session.php` with cookie cache.
Submit fetches the problem page and parses the form dynamically (action
URL, hidden fields, language select) before POSTing multipart with
`sub_file[]`.
**Kattis** — httpx-based login via `/login/email` (official Kattis CLI
API). Submit is a multipart POST to `/submit`; the `contest` field is
included only when `contest_id != problem_id`. `scrape_contest_list`
URL updated to filter
`kattis_original=on&kattis_recycled=off&user_created=off`.
**CodeChef** — StealthySession browser-based login and submit following
the AtCoder/CF pattern. Login checks `__NEXT_DATA__` for current user
and fills the email/password form. Submit navigates to
`/{contest_id}/submit/{problem_id}`, selects language (standard
`<select>` first, custom dropdown fallback), sets code via
Monaco/CodeMirror/textarea JS, and clicks submit. Retry-on-relogin
pattern matches existing CF behaviour.
Language IDs added to `language_ids.py` for all three platforms.
## Problem
`open_url` automatically opened the browser on contest load and problem
change, which is now redundant with `:CP open`.
## Solution
Remove the `open_url` field from `cp.Config`, its default, its
validation, and the call site in `setup_contest`. Remove documentation
from `cp.nvim.txt`.
## Problem
There was no way to open a problem, contest, or standings page in the
browser from within the plugin.
## Solution
Add `contest_url` and `standings_url` to `MetadataResult` and persist
them in the cache. Add `cache.get_open_urls` to resolve all three URLs.
Wire up `:CP open [problem|contest|standings]` in `commands/init.lua`
to call `vim.ui.open`, warning when a URL is unavailable (e.g. CSES
has no standings). Closes#315.
## Problem
CSES `_web_login` used bare dict indexing on the API response, raising
an opaque `KeyError` on missing fields. `_check_token` swallowed all
exceptions as `False`, conflating transient network errors with invalid
tokens. CF persisted cookies unconditionally and silently swallowed
`_solve_turnstile` failures in `submit_action`.
## Solution
CSES API fields now use `.get()` with a descriptive `RuntimeError` on
absence. `_check_token` re-raises `httpx` network/timeout exceptions
so callers see real failures. CF cookie writes are guarded by an
`X-User-Handle` check (the CF auth cookie). `_solve_turnstile` errors
propagate to the outer error handler instead of being silenced.
## 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.
## 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.
## Problem
Two gaps in `commands/init.lua`. Codeforces contest IDs were passed
through raw, so full URLs (e.g.
`https://codeforces.com/contest/1933/problem/A`) or problem IDs with
trailing letters (e.g. `1933A`) caused scraper URL construction to fail.
Separately, action commands like `:CP run` silently failed when invoked
with no active contest instead of attempting to recover from the current
file's cached state.
## Solution
Add `canonicalize_cf_contest` to normalize URLs and strip trailing
problem letters in `parse_command`. Add a guard in `handle_command` that
calls `restore_from_current_file()` before dispatching any
contest-requiring action when no platform is active, returning early
only if no cached state is found.
Closes#306. Closes#308.
## Problem
`setup_problem` only cleared the output buffer when `old_problem_id ~=
problem_id`. If two different contests share a problem with the same ID
(e.g. both have `a`), the condition is false and stale output from the
previous contest remains visible.
## Solution
Clear the output buffer at the top of `proceed()` in `setup_contest`
whenever `is_new_contest` is true, before any problem setup runs.
Closes#303.
## Problem
The submit path in `codeforces.py` guarded `cookie_cache.write_text` on
the presence of a `JSESSIONID` cookie. Codeforces does not use
`JSESSIONID`, so the cookie file was never written after submit,
breaking the fast-path on subsequent submits.
## Solution
Replace the name-specific guard with a non-empty check (`if
browser_cookies:`), matching the unconditional save already used in the
login path.
Closes#301.
## Problem
`:CP <platform> login` blindly caches username/password without
server-side
validation. Bad credentials are only discovered at submit time, which is
confusing and wastes a browser session.
## Solution
Wire `:CP <platform> login` through the scraper pipeline so each
platform
actually authenticates before persisting credentials. On failure, the
user
sees an error and nothing is cached.
- CSES: reuses `_check_token` (fast path) and `_web_login`; returns API
token
in `LoginResult.credentials` so subsequent submits skip re-auth.
- AtCoder/Codeforces: new `_login_headless` functions open a
StealthySession,
solve Turnstile/Cloudflare, fill the login form, and validate success by
checking for the logout link. Cookies only persist on confirmed login.
- CodeChef/Kattis/USACO: return "not yet implemented" errors.
- `scraper.lua`: generalizes submit-only guards (`needs_browser` flag)
to
cover both `submit` and `login` subcommands.
- `credentials.lua`: prompts for username/password, passes cached token
for
CSES fast path, shows ndjson status notifications, only caches on
success.
## Problem
After the initial submit hardening, two issues remained: source code was
read in Lua and piped as stdin to the scraper (unnecessary roundtrip
since
the file exists on disk), and CF's `page.fill()` timed out on the hidden
`textarea[name="source"]` because CodeMirror owns the editor state.
## Solution
Pass the source file path as a CLI arg instead — AtCoder calls
`page.set_input_files(file_path)` directly, CF reads it with
`Path(file_path).read_text()`. Fix CF source injection via
`page.evaluate()`
into the CodeMirror instance. Extract `BROWSER_SUBMIT_NAV_TIMEOUT` as a
per-platform `defaultdict` (CF defaults to 2× nav timeout). Save the
buffer
with `vim.cmd.update()` before submitting.
## Problem
AtCoder file upload always wrote a `.cpp` temp file regardless of
language. CF submit used `solve_cloudflare=True` on the submit page,
causing a spurious "No Cloudflare challenge found" error;
`_wait_for_gate_reload` in `login_action` was dead code. Stale cookies
caused silent auth failures with no recovery path. The `uv.spawn` ndjson
path for submit had no overall timeout.
## Solution
Replace AtCoder's temp file with `page.set_input_files` using an
in-memory buffer and correct extension via `_LANGUAGE_ID_EXTENSION`.
Replace CF's temp-file/fallback dance with a direct
`textarea[name="source"]` fill and set `solve_cloudflare=False` on the
submit fetch. Add a login fast-path that skips the homepage check when
cookies exist, with automatic stale-cookie recovery via `_retried` flag
on redirect-to-login detection. Remove `_wait_for_gate_reload`. Fix
`_ensure_browser` to propagate install errors. Add a 120s kill timer to
the ndjson `uv.spawn` submit path in `scraper.lua`.
## Problem
Codeforces submit was a stub. CSES submit re-ran the full login flow on
every invocation (~1.5s overhead).
## Solution
**Codeforces**: headless browser submit via StealthySession (same
pattern as AtCoder). Solves Cloudflare Turnstile on login, uploads
source via file input, caches cookies at
`~/.cache/cp-nvim/codeforces-cookies.json` so repeat submits skip login.
**CSES**: persist the API token in credentials via a `credentials`
ndjson event. Subsequent submits validate the cached token with a single
GET before falling back to full login.
Also includes a vimdoc table of contents.
## Problem
CSES submit was a stub returning "not yet implemented".
## Solution
Authenticate via web login + API token bridge (POST `/login` form, then
POST `/api/login` and confirm the auth page), submit source to
`/api/courses/problemset/submissions` with base64-encoded content, and
poll for verdict. Uses the same username/password credential model as
AtCoder — no browser dependencies needed. Tested end-to-end with a real
CSES account (verdict: `ACCEPTED`).
Also updates `scraper.lua` to pass the full ndjson event object to
`on_status` and handle `credentials` events for future platform use.
## Problem
`setup_problem` explicitly set `swapfile = true` on provisional buffers,
overriding the user's global `noswapfile` setting. The resulting `.swp`
files triggered E325 warnings on subsequent `:e` calls — especially
during the restore path, which redundantly re-opened the current buffer.
## Solution
Remove the `swapfile` override so the user's setting is respected, and
skip the `:e` call in `setup_problem` when the current buffer already
matches the target source file.
## Problem
`_submit_sync` was a 170-line nested closure with `_solve_turnstile` and
the browser-install block further nested inside it. Status events went
to
stderr, which `run_scraper()` silently discards, leaving the user with a
10–30s silent hang after credential entry. The NDJSON spawn path also
lacked stdin support, so submit had no streaming path at all.
## Solution
Extract `_TURNSTILE_JS`, `_solve_turnstile`, `_ensure_browser`, and
`_submit_headless` to module level in `atcoder.py`; status events
(`installing_browser`, `checking_login`, `logging_in`, `submitting`) now
print to stdout as NDJSON. Add stdin pipe support to the NDJSON spawn
path in `scraper.lua` and switch `M.submit` to streaming with an
`on_status` callback. Wire `on_status` in `submit.lua` to fire
`vim.notify` for each phase transition.
## Problem
Credentials lived in a top-level `_credentials` namespace, requiring
special
preservation logic in `clear_all()` and a separate key hierarchy from
the
platform data they belong to.
## Solution
Move credentials from `_credentials.<platform>` to
`<platform>._credentials`.
Migrate v1 caches on load, skip underscore-prefixed keys when
enumerating
contest IDs and summaries, and simplify `clear_all()` now that no
special
preservation is needed.
Stacked on #292.
## Problem
`:CP credentials login/logout/clear` is verbose and inconsistent with
other
actions that are all top-level (`:CP run`, `:CP submit`, etc.). The
clear-all
subcommand is also unnecessary since re-logging in overwrites existing
credentials.
## Solution
Replace `:CP credentials {login,logout,clear}` with `:CP login
[platform]`
and `:CP logout [platform]`. Remove the clear-all command and the
credentials
subcommand dispatch — login/logout are now regular actions routed
through the
standard action dispatcher.
## Problem
The `set` and `clear` subcommands don't clearly convey their intent —
`set`
reads like a generic setter rather than an auth action, and `clear`
overloads
single-platform and all-platform semantics in one subcommand.
## Solution
Rename `set` to `login`, split `clear` into `logout` (per-platform,
defaults
to active) and `clear` (all platforms).
New API:
- `:CP credentials login [platform]` — prompt and save credentials
- `:CP credentials logout [platform]` — remove credentials for one
platform
- `:CP credentials clear` — remove all stored credentials
Problem: closing the test editor left cp://test-N-* buffers alive,
causing E95 on reopen. The nofile buftype also rejected :w, which
was counterintuitive in an editable grid.
Solution: delete all test buffers in toggle_edit teardown. Switch
buftype to acwrite with a BufWriteCmd autocmd that persists test
cases and clears the modified flag. Hoist save_all_tests above
setup_keybindings so the autocmd closure can reference it.
Problem: <c-n>/<c-p> in the I/O view buffers required the cursor
to leave the source file to work, re-ran the solution on each
press, and gave no indication of which test was active. The
workflow is better served by :CP run <n> for a specific test or
:CP panel for full inspection.
Solution: remove navigate_test, next_test_key/prev_test_key config
options, and the associated current_test_index state field.
Problem: vim.json.decode maps JSON null to vim.NIL (userdata), but
cache.set_test_cases validates precision as number|nil, causing a
type error on every scrape where precision is absent.
Solution: guard the precision field when building the callback
table, converting vim.NIL to nil.
Problem: :CP login was a poor API — no way to clear credentials without
raw Lua, and the single command didn't scale to multiple operations.
Solution: replace login with a :CP credentials subcommand following the
same pattern as :CP cache. :CP credentials set [platform] prompts and
saves; :CP credentials clear [platform] removes one or all platforms.
Add cache.clear_credentials(), rename login.lua to credentials.lua,
update parse/dispatch/tab-complete, and rewrite vimdoc accordingly.
Problem: credentials were only set implicitly on first :CP submit.
There was no way to update wrong credentials, log out, or set
credentials ahead of time without editing the cache JSON manually.
Solution: add :CP login [platform] which always prompts for username
and password and overwrites any saved credentials for that platform.
Omitting the platform falls back to the active platform. Wire the
command through constants, parse_command, handle_command, and add
tab-completion (suggests platform names). Document in vimdoc under
the SUBMIT section and in the commands reference.
Problem: credentials were stored in a separate file,
cp-nvim-credentials.json, alongside the main cp-nvim.json cache.
Two files for one plugin's persistent state was unnecessary.
Solution: add get_credentials/set_credentials to cache.lua, storing
credentials under _credentials[platform] in the shared cache. Update
clear_all() to preserve _credentials across cache wipes. Remove the
separate file, load_credentials, and save_credentials from submit.lua.
Problem: vimdoc only covered AtCoder, Codeforces, and CSES, and had no
entries for race, stress, or submit — all of which shipped in this
branch. The platform list was also stale and the workflow example
pointed users to the AtCoder website to submit manually.
Solution: add CodeChef, USACO, and Kattis to the supported platforms
list and platform-specific usage section (including Kattis's
dual single-problem/full-contest behavior). Document :CP stress,
:CP race, and :CP submit in the commands section, add their <Plug>
mappings, and add dedicated STRESS TESTING, RACE, and SUBMIT sections.
Update get_active_panel() to list its return values, add the
cp.race.status() API under the statusline section, and update the
workflow example step 8 to use :CP submit.
Problem: scrape_contest_list paginated the entire Kattis problem database
(3000+ problems) treating each as a "contest". scrape_contest_metadata
only handled single-problem access. stream_tests_for_category_async could
not fetch tests for multiple problems in a real contest.
Solution: replace the paginated problem loop with a single GET to
/contests that returns ~150 real timed contests. Add contest-aware path
to scrape_contest_metadata that fetches /contests/{id}/problems and
returns all problem slugs; fall back to single-problem path when the ID
is not a contest. Add _stream_single_problem helper and update
stream_tests_for_category_async to fan out concurrently over all contest
problem slugs before falling back to the single-problem path.
Problem: luals flagged undefined-field on uv timer methods because
race_state.timer was untyped, and undefined-field on env_extra/stdin
because they were missing from the run_scraper opts annotation.
Solution: hoist race_state.timer into a typed local before the nil
check so luals can narrow through it; add env_extra and stdin to the
opts inline type in run_scraper.
Problem: lua typecheck flagged missing start_time field on ContestSummary;
ty flagged BeautifulSoup Tag/NavigableString union on csrf_input.get(),
a 3-tuple unpack where _extract_problem_info now returns 4 values in
cses.py, and an untyped list assignment in usaco.py.
Solution: add start_time? to ContestSummary LuaDoc, guard csrf_input
with hasattr check and type: ignore, unpack precision from
_extract_problem_info in cses.py callers, and use cast() in usaco.py.
Problem: toggle_interactive() had its condition inverted — it blocked
:CP interact on non-interactive problems while showing the message "This
problem is interactive", and passed through on interactive ones. The
panel guard in toggle_panel() was also missing a nil-check on
contest_data.index_map, which could crash if the index map was absent.
Solution: invert the toggle_interactive() guard to match the symmetrical
pattern in toggle_view(), fix the error message to say "not interactive",
and add the missing index_map guard. Also handle the stress panel type
in M.disable() so :CP stress can be toggled off.
Add command parsing and dispatch for :CP race, :CP race stop, :CP stress,
and :CP submit. Add tab-completion for race (platform/contest/--lang),
stress (cwd executables at arg 2 and 3), and race stop. Add
<Plug>(cp-stress), <Plug>(cp-submit), and <Plug>(cp-race-stop) keymaps.