Problem: the single `do_login_and_submit` page action navigated between
pages within one `session.fetch` call, which was fragile and couldn't
leverage `solve_cloudflare` for the Turnstile gate on the submit page.
The submit button click also blocked on navigation completion, causing
timeouts when CF was slow to process.
Solution: split into three separate `session.fetch` calls (homepage
login check, `/enter` login, `/contest/{id}/submit`) with
`solve_cloudflare=True` on login and submit. Use `no_wait_after=True`
on the submit click with a doubled nav timeout. Extract `span.error`
text on submit failure instead of a generic timeout message.
Problem: `_solve_turnstile` looped 6 times with ~20s per iteration
(15s bounding_box timeout + 5s wait_for_function) when no Turnstile
iframe existed on the page, causing a 120-second delay on pages that
don't require Turnstile verification.
Solution: check for existing token and iframe presence before entering
the retry loop. `iframe_loc.count()` returns immediately when no
matching elements exist, avoiding the expensive timeout cascade.
Problem: each scraper defined its own timeout constants
(`TIMEOUT_S`, `TIMEOUT_SECONDS`) with inconsistent values (15s vs 30s)
and browser timeouts were scattered as magic numbers (60000, 15000,
5000, 500).
Solution: introduce `scrapers/timeouts.py` with named constants for
HTTP requests, browser session/navigation/element/turnstile/settle
timeouts, and submission polling. All six scrapers now import from
the shared module.
Problem: Codeforces submit was a stub returning "not yet implemented".
Solution: use StealthySession (same pattern as AtCoder) to handle
Cloudflare Turnstile on the login page, fill credentials, navigate to
the contest submit form, upload source via file input, and cache
cookies at `~/.cache/cp-nvim/codeforces-cookies.json` so repeat
submits skip the login entirely. Uses a single browser page action
that checks for the submit form before navigating, avoiding redundant
page loads and Turnstile challenges.
Problem: every `:CP submit` on CSES ran the full 5-request login flow
(~1.5 s overhead) even when the token from a previous submit was still
valid.
Solution: persist the API token in credentials via a `credentials`
ndjson event. On subsequent submits, validate the cached token with a
single GET before falling back to the full login.
## 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.
Add submit.lua that reads credentials from a local JSON store (prompting
via vim.ui.input/inputsecret on first use), reads the source file, and
delegates to scraper.submit(). Add language_ids.py with platform-to-
language-ID mappings for atcoder, codeforces, and cses.
Add stress.lua that auto-detects or accepts generator and brute solution
files, compiles C++ if needed, and launches scripts/stress.py in a
terminal buffer with session save/restore and cleanup autocmds.
Add scripts/stress.py as a standalone loop that runs generator → brute →
candidate, comparing outputs and exiting on the first mismatch.
Add race.lua with a 1-second vim.uv timer that counts down to a contest
start time and auto-calls setup.setup_contest() at T=0. Exposes
M.start(), M.stop(), and M.status() for command dispatch and statusline
integration.
Add KattisScraper and USACOScraper with contest list, metadata, and
test case fetching. Register kattis and usaco in PLATFORMS,
PLATFORM_DISPLAY_NAMES, and default platform configs.
Problem: problem pages contain floating-point precision requirements and
contest start timestamps that were not being extracted or stored. The
submit workflow also needed a foundation in the scraper layer.
Solution: add extract_precision() to base.py and propagate through all
scrapers into cache. Add start_time to ContestSummary and extract it
from AtCoder and Codeforces. Add SubmitResult model, abstract submit()
method, submit CLI case with get_language_id() resolution, stdin/env_extra
support in run_scraper, and a full AtCoder submit implementation; stub
the remaining platforms.
Problem: the tolerance field for floating-point comparison was named
`epsilon`, which is an implementation detail, not the user-visible concept.
Solution: rename to `precision` in run.lua type annotations, internal
variables, and comparison logic.
Problem: uv downloads glibc-linked Python binaries that NixOS cannot
run, causing setup_python_env to fail with exit status 127.
Solution: detect NixOS via /etc/NIXOS and bypass the uv sync path,
falling through directly to nix-based Python discovery.
Problem: the hooks API conflated distinct lifecycle scopes under a flat
table with inconsistent naming (setup_code, before_run, setup_io_input),
making it hard to reason about when each hook fires.
Solution: introduce two namespaces — hooks.setup.{contest,code,io} for
one-time initialization and hooks.on.{enter,run,debug} for recurring
events. hooks.setup.contest fires once when a contest dir is newly
created; hooks.on.enter is registered as a buffer-scoped BufEnter
autocmd and fires immediately after setup.code. The provisional buffer
setup_code callsite is removed as it ran on an unresolved temp buffer.
Problem: after apply_template writes a file's content to the buffer,
cursor positioning was left entirely to the user's setup_code hook,
forcing everyone to reimplement the same placeholder-stripping logic.
Solution: add an optional templates.cursor_marker config key. When set,
apply_template scans the written lines for the marker, strips it, and
positions the cursor there via bufwinid so it works in both the
provisional and existing-file paths.
Problem: CpPlatformOverrides lacked a template field and merge_lang()
never copied ov.template into the effective language config, so
per-platform template overrides were silently dropped.
Solution: add template? to CpPlatformOverrides and forward it in
merge_lang(), matching how extension is handled.
Problem: output comparison used exact string equality after whitespace
normalisation, causing correct solutions to fail on problems where
floating-point answers are accepted within a tolerance (e.g. 1e-6).
Solution: add an optional ui.panel.epsilon config value. When set,
actual and expected output are compared token-by-token: numeric tokens
are compared with math.abs(a - b) <= epsilon, non-numeric tokens fall
back to exact string equality. Per-problem epsilon can also be stored
in the cache and takes precedence over the global default.
Problem: new solution files were always created empty, requiring users
to manually paste boilerplate or rely on editor snippets that fire
outside cp.nvim's control.
Solution: add an optional template field to the language config. When
set to a file path, its contents are written into every newly created
solution buffer before the setup_code hook runs. Existing files are
never overwritten.
Problem: cp.nvim exposed no documentation showing how to integrate its
runtime state into a statusline. Users had to discover the state module
API by reading source.
Solution: add a STATUSLINE INTEGRATION section to the vimdoc with a
state API reference and recipes for vanilla statusline, lualine, and
heirline. Also anchors the *cp.State* help tag referenced in prose
elsewhere in the doc.
Problem: test cases were executed sequentially, each waiting for the
previous process to finish before starting the next. On problems with
many test cases this meant wall-clock run time scaled linearly.
Solution: fan out all test case processes simultaneously. A remaining
counter fires on_done once all callbacks have returned. on_each is
called per completion as before; callers that pass on_each ignore its
arguments so the index semantics change is non-breaking.
Problem: .luarc.json used the flat dotted-key format which is not the
canonical LuaLS schema. The busted library was also missing, so LuaLS
could not resolve types in test files.
Solution: rewrite .luarc.json using nested objects and add
${3rd}/busted/library to workspace.library.