Commit graph

1056 commits

Author SHA1 Message Date
1dcce37f51
ci: format 2026-03-07 18:14:10 -05:00
693d3bf172
feat(commands): standalone :CP login/logout with auto-restore
Problem: `:CP login` and `:CP logout` only worked as `:CP <platform>
login`. Users with an active platform or in a registered buffer had to
type the platform name redundantly.

Solution: Parse standalone `login`/`logout`/`signup` as action commands
with `requires_context = true`, resolving the platform from state (or
auto-restoring from the current buffer). Add `vim.cmd.redraw()` after
auto-restore so the screen updates before any prompt. Add `login`/
`logout` to top-level completion when a platform is active.
2026-03-07 18:13:12 -05:00
e3c81e895a
feat(credentials): re-prompt on bad credentials and improve cancel UX
Problem: After a failed login attempt, the user had to re-run
`:CP <platform> login` manually. Cancel messages lacked context, and
credential prompts in `submit.lua` used raw platform IDs instead of
display names.

Solution: `prompt_and_login` now recurses after failure so the user is
re-prompted until they cancel or succeed. `submit.lua` extracts the
scraper call into `do_submit` and re-prompts via `prompt_credentials` on
`bad_credentials`. Username prompts show `<Esc> to cancel`, and cancel
messages include the platform display name.
2026-03-07 18:12:55 -05:00
0c06b4a55a
fix(security): harden credential storage and transmission
Problem: credential and cookie files were world-readable (0644),
passwords transited via `CP_CREDENTIALS` env var (visible in
`/proc/PID/environ`), and Kattis/USACO echoed passwords back
through stdout unnecessarily.

Solution: set 0600 permissions on `cp-nvim.json` and `cookies.json`
after every write, pass credentials via stdin pipe instead of env
var, and stop emitting passwords in ndjson from Kattis/USACO
`LoginResult` (CSES token emission unchanged).
2026-03-07 18:01:54 -05:00
Barrett Ruth
771dbc7753
fix(scrapers): bad credentials detection and error message cleanup (#367)
## Problem

Wrong credentials produced garbled error messages (`"login failed: Login
failed: bad_credentials"`) and stale credentials remained cached after
failure, causing silent re-use on the next invocation.

## Solution

Standardize all scrapers to emit `"bad_credentials"` as a plain error
code, mapped to a human-readable string via `LOGIN_ERRORS` in
`constants.lua`. Fix `credentials.lua` to clear cached credentials on
failure in both the fresh-prompt and re-prompt paths. For AtCoder and
Codeforces, replace `wait_for_url` with `wait_for_function` to detect
the login error element immediately rather than sitting the full 10s
navigation timeout. Add "Remember me" checkbox on Codeforces login.
2026-03-07 17:58:25 -05:00
Barrett Ruth
573b335646
feat: file conflict prompt, empty submit guard, and lint fixes (#366)
## Problem

Loading a problem whose source file already exists silently overwrites
user code. Submitting an empty file sends a blank submission to the
platform. Two ruff lint violations existed in the scrapers.

## Solution

- `setup.lua`: when the target source file exists on the filesystem
(`vim.uv.fs_stat`), show an inline `Overwrite? [y/N]:` prompt. Declining
keeps the existing file open and registers state normally. Skipped when
the file is already loaded in a buffer.
- `submit.lua`: resolve path to absolute, use `vim.uv.fs_stat` to verify
existence, abort with WARN if `stat.size == 0` ("Submit aborted: source
file has no content").
- `scrapers/atcoder.py`: remove unused `pathlib.Path` import (F401).
- `scrapers/base.py`: move local imports to top of file (E402).

Closes #364, #365.
2026-03-07 16:30:51 -05:00
Barrett Ruth
b7ddf4c253
fix(scrapers): cookie fast paths, centralized storage, and reauth hardening (#363)
## Problem

Scraper cookie handling was fragmented across per-platform files with no
shared access, httpx scrapers lacked `checking_login` fast paths on
login, and several re-auth edge cases (CodeChef submit, CF cookie guard,
AtCoder cookie persistence) caused unnecessary full re-logins or silent
failures.

## Solution

Centralize all cookie storage into a single `cookies.json` via helpers
in `base.py`. Add `checking_login` fast paths to `kattis.py` (using the
`x-username` response header as a session probe), `usaco.py`, and
`cses.py` login flows. Fix `kattis.py` submit to emit `checking_login`
only after loading cookies. Remove AtCoder cookie persistence from login
entirely — always do a fresh session. Harden CodeChef and CF reauth
with consistent status logging and cookie guard checks.
2026-03-07 16:10:51 -05:00
Barrett Ruth
564f9286da
fix(scraper): httpx login fast paths, proactive validation, and slug filename fix (#358)
## Problem

httpx scrapers (CSES, Kattis, USACO) always ran full login flows even
with valid cached sessions. \`credentials.lua\` always prompted before
trying cached credentials. \`default_filename\` doubled the slug for
Kattis single-problem mode (e.g. \`addtwonumbersaddtwonumbers.cc\`).

## Solution

Added token/cookie fast paths to CSES \`login()\` and USACO
\`login()\`/\`submit()\`.
Hardened Kattis reactive re-auth trigger to check status code first.
Refactored \`credentials.lua\` to try cached credentials before
prompting.
Fixed \`default_filename\` to not concatenate when \`contest_id ==
problem_id\`.
2026-03-07 15:38:12 -05:00
Barrett Ruth
eb0dea777e
fix(scrapers): login fast paths and re-auth hardening for httpx platforms (#357)
## Problem

On CSES, Kattis, and USACO, `:CP <platform> login` always prompted
for credentials and ran a full web login even when a valid session was
already cached. Submit also had weak stale-session detection.

## Solution

`credentials.lua` now tries cached credentials first before prompting,
delegating fast-path detection to each scraper. CSES `login()` checks
the cached API token and returns immediately if valid. USACO `login()`
and `submit()` call `_check_usaco_login()` upfront. Kattis `submit()`
emits `checking_login` consistently and also triggers re-auth on HTTP
400/403, not just on the `"Request validation failed"` text match.
The premature `Submitting...` log emitted by Lua before the scraper
started is removed — Python's own status events are sufficient.
2026-03-07 02:23:43 -05:00
6e9829a115
fix(race): log initial countdown on start 2026-03-07 00:00:42 -05:00
Barrett Ruth
f59c12a929
fix(race): log initial countdown on start (#356)
## Problem

Starting a race on a contest more than 1 hour away showed no feedback —
`should_notify` only fires at 15-minute intervals, so the first tick
produced no message and the UI appeared frozen during the contest list
fetch.

## Solution

Log the initial countdown immediately after the race timer is set up,
before the first `should_notify` tick fires.
2026-03-06 23:59:56 -05:00
926ca587b8
ci: format
Some checks are pending
luarocks / ci (push) Waiting to run
luarocks / publish (push) Blocked by required conditions
2026-03-06 23:42:45 -05:00
Barrett Ruth
73e5c3f3f8
fix: codechef submit fixes and atcoder cleanup (#355)
## Problem

After the initial CodeChef implementation, submit silently swallowed
"contest not available for submission" errors, and the submit flow used
blind `wait_for_timeout` delays. AtCoder had an unnecessary 500ms settle
delay after file upload.

## Solution

Replace the initial page-load sleep in CodeChef submit with
`wait_for_selector`, replace the 3s post-click sleep with a proper
`wait_for_selector` on the result dialog, and extend the practice
fallback
check to catch both dialog variants. Remove AtCoder's
`BROWSER_SETTLE_DELAY`
and the constant from `timeouts.py`.
2026-03-06 23:40:12 -05:00
Barrett Ruth
3c11d609f5
feat(codechef): implement full CodeChef support (#354)
## Problem

CodeChef had no working login, submit, or contest list. The browser
selectors were wrong, the contest list was missing present/past
contests,
and problem/contest URLs were unset.

## Solution

Fix login and submit selectors for the Drupal-based site. Paginate
`/api/list/contests/past` to collect all 228 Starters, then expand each
parent contest into individual division entries (e.g. `START228 (Div.
4)`).
Add language IDs, correct `url`/`contest_url`/`standings_url` in
metadata,
and make `:CP <platform>` open the contest picker directly.
2026-03-06 23:10:44 -05:00
c7f2af16d4
chore: remove files 2026-03-06 21:35:34 -05:00
Barrett Ruth
291de4e137
fix: expand language IDs, fix AtCoder submit, normalize logging (#353)
## Problem

Language version coverage was incomplete across all platforms, AtCoder
submit used a stale cookie fast-path that caused silent failures, and
raw
`vim.notify` calls throughout the codebase produced inconsistent or
missing `[cp.nvim]:` prefixes.

## Solution

Remove cookie persistence from AtCoder login/submit (always fresh
login),
increase the submit nav timeout to 40s, and switch to in-memory buffer
upload with the correct per-language extension from a full
`_LANGUAGE_ID_EXTENSION`
map covering all 116 AtCoder languages. Expand `LANGUAGE_VERSIONS` in
`constants.lua` with all AtCoder languages, 15 new CF languages with
full
version variants, and 50+ Kattis languages. Fix AtCoder `prolog` ID
(`6079`→`6081`, was Pony) and remove the non-existent `racket` entry.
Replace all raw `vim.notify` calls with `logger.log`. Simplify the
submit
language doc to point at `constants.lua` rather than maintaining a
static table.
2026-03-06 21:35:13 -05:00
Barrett Ruth
1ac521a126
fix: language version IDs, cache crash, and stress session restore (#352)
## Problem

Codeforces language version IDs were wrong (pointed at old compiler
versions), `LANGUAGE_VERSIONS` was incomplete for most platforms, the
contest picker crashed when `supports_countdown` was stored as a
non-table entry, and `:CP stress` restored a broken IO view on exit.

## Solution

Correct CF `programTypeId` values and default to C++20. Add
`LANGUAGE_VERSIONS` entries for all six platforms. Guard
`get_contest_summaries` against non-table cache entries. Call
`ensure_io_view()` after stress session restore. Shorten the stress
terminal buffer name to a readable `term://stress.py` format.
2026-03-06 20:22:28 -05:00
Barrett Ruth
425a8f36e9
fix: complete language version IDs for all platforms (#350)
## Problem

`LANGUAGE_VERSIONS` only covered cpp and python. Several platform IDs
were wrong — CodeChef used `C++ 17`/`Python 3` (correct: `C++`/`PYTH
3`), USACO listed nonexistent c++20/c++23 options, and CSES only had
C++17.

## Solution

Verify every platform's submit page and update all language ID tables.
Add java and rust entries where supported, fix incorrect CodeChef and
USACO IDs, and expand CSES `CSES_LANGUAGES` dict with
C++11/C++20/PyPy3/Java/Rust variants.
2026-03-06 19:28:06 -05:00
Barrett Ruth
2776aaeb21
fix: correct Codeforces language version IDs and default to C++20 (#349)
## Problem

`LANGUAGE_VERSIONS` mapped CF `c++17` to `89` (actually GNU G++20) and
defaulted to `c++17`, which didn't match any CF entry. Helpdocs listed
CF as having only `c++17`.

## Solution

Fix CF version map to `c++17 → 54`, `c++20 → 89`, `c++23 → 91`, add
`pypy3 → 70`. Change `DEFAULT_VERSIONS.cpp` to `c++20` (no behavioral
change — `89` was already the submit default). Update helpdocs. Fix
StyLua formatting in `race.lua`.
2026-03-06 19:01:55 -05:00
Barrett Ruth
8398f3428e
feat(race): notification tiers, drift correction, and T=0 retry (#348)
## Problem

Race countdown had three reliability issues:
- Notified every second regardless of remaining time (noisy for
hours-long waits)
- Never re-fetched start times (stale if contest rescheduled)
- No retry on setup failure at T=0 — if the scraper failed at the most
critical moment, the entire countdown was wasted

## Solution

- **Notification tiers**: >1h every 15m, >5m every 1m, >1m every 10s,
≤60s every 1s
- **Drift correction**: re-fetch contest list every 10 minutes, update
`start_time` if changed
- **T=0 retry**: `race_try_setup` calls `scrape_contest_metadata` with
the new `on_error` callback, retrying up to 15 times at 3s intervals
(~45s). Caches metadata on success, then calls `setup_contest` on the
fast (cached) path. Token guard invalidates stale retries after
cancellation.
- **`on_error` for `scrape_contest_metadata`**: optional 4th param,
backward-compatible
2026-03-06 18:43:12 -05:00
Barrett Ruth
bd0ae25d4b
refactor(race): replace :CP race with --race flag (#347)
## 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.
2026-03-06 18:33:42 -05:00
Barrett Ruth
58b6840815
feat: race countdown support and language version selection (#346)
## 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.
2026-03-06 18:18:21 -05:00
Barrett Ruth
592f977296
fix(login): remove cookie fast-path from login subcommand (#344)
## 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
2026-03-06 17:53:22 -05:00
Barrett Ruth
8465e70772
test: add offline fixture coverage for Kattis and USACO (#342)
## 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.
2026-03-06 16:49:49 -05:00
Barrett Ruth
9727dccc6f
feat(commands): add tab completion for :CP open (#338)
## 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.
2026-03-06 16:14:58 -05:00
Barrett Ruth
4ef00afe66
feat(commands): add :CP <platform> signup subcommand (#337)
## 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.
2026-03-06 16:14:33 -05:00
Barrett Ruth
e89c57558d
feat(config): merge platform config model and add disabled-platform guard (#336)
## 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.
2026-03-06 16:14:16 -05:00
Barrett Ruth
82640709d6
fix(kattis,usaco): precision, open URLs, and Kattis submit error surface (#335)
## 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.
2026-03-06 15:23:55 -05:00
c4be8b4f9e
fix: remove curl_cffi 2026-03-06 15:05:42 -05:00
Barrett Ruth
0b40e0f33e
fix: replace curl_cffi with scrapling in codeforces metadata (#334)
## 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.
2026-03-06 13:25:44 -05:00
Barrett Ruth
6a3d2fe4f8
fix(codechef): rewrite contest list, drop curl_cffi (#333)
## 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`.
2026-03-06 13:25:31 -05:00
Barrett Ruth
ba5ae8df69
fix(kattis): fix nil display_name in contest picker (#332)
## 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`.
2026-03-06 13:25:17 -05:00
Barrett Ruth
b6d3df03e3
fix(scrapers): fix Kattis and USACO login and submit (#330)
## 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.
2026-03-06 12:38:32 -05:00
Barrett Ruth
543480a4fe
feat: implement login and submit for USACO, Kattis, and CodeChef (#325)
## 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.
2026-03-06 00:09:16 -05:00
Barrett Ruth
2d50f0a52a
refactor: remove open_url config option in favour of :CP open (#322)
## 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`.
2026-03-05 22:55:43 -05:00
Barrett Ruth
401afb205e
feat(commands): document :CP open [problem|contest|standings] (#321)
forgot to document `:CP open`
2026-03-05 19:51:06 -05:00
Barrett Ruth
7bf4cf2538
feat(commands): implement :CP open [problem|contest|standings] (#319)
## 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.
2026-03-05 19:16:05 -05:00
Barrett Ruth
b959f29bd4
fix(scrapers): harden CSES and CF submit edge cases (#295) (#318)
## 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.
2026-03-05 19:00:35 -05:00
Barrett Ruth
6c036a7b2e
fix: systematic context guards and full panel lifecycle (#317)
## 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.
2026-03-05 18:29:41 -05:00
Barrett Ruth
7e7e135681
fix: cancel active process on contest switch (#316)
## 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.
2026-03-05 17:42:50 -05:00
Barrett Ruth
916c9174a6
fix(commands): canonicalize CF contest IDs and auto-restore on action (#314)
## 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.
2026-03-05 15:37:33 -05:00
Barrett Ruth
b3014e9c86
fix(setup): clear output buffer when switching contests (#313)
## 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.
2026-03-05 15:31:47 -05:00
Barrett Ruth
924601ce99
fix(codeforces): persist cookies after submit regardless of name (#312)
## 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.
2026-03-05 15:31:29 -05:00
Barrett Ruth
2c119774df
feat: validate credentials on :CP <platform> login (#310)
## 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.
2026-03-05 15:12:09 -05:00
Barrett Ruth
a202725cc5
fix(submit): use file path over stdin; fix CF CodeMirror textarea (#305)
## 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.
2026-03-05 14:34:14 -05:00
Barrett Ruth
127089c57f
fix(submit): harden atcoder and codeforces submit flow (#304)
## 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`.
2026-03-05 11:18:34 -05:00
Barrett Ruth
6fcb5d1bbc
feat(codeforces): implement submit; cache CSES token (#300)
## 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.
2026-03-05 10:37:39 -05:00
Barrett Ruth
e9f72dfbbc
feat(cses): implement submit via REST API (#299)
## 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.
2026-03-05 01:07:57 -05:00
Barrett Ruth
e674265527
fix(setup): prevent spurious swap file warnings on :CP (#297)
## 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.
2026-03-05 00:37:29 -05:00
Barrett Ruth
c194f12eee
feat(atcoder): extract submit helpers; add live status notifications (#294)
## 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.
2026-03-04 19:27:29 -05:00