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.
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
`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
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
`: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
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: 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: 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: M._cache = cache_data captured the initial empty table reference
at module load time. After M.load() reassigns cache_data to the decoded
JSON, M._cache is permanently stale and returns the wrong table.
Solution: remove the field assignment and expose get_raw_cache() which
closes over cache_data and always returns the current table.
Problem: vim.loop is deprecated since Neovim 0.10 in favour of vim.uv.
Five call sites across scraper.lua, setup.lua, utils.lua, and health.lua
still referenced the old alias.
Solution: replace every vim.loop reference with vim.uv directly.